Consider an integer. How can it be described so that a person who has not used one before can implement something that looks and acts like an integer? This is a specific case of the general problem faced when using computers – to describe a problem in enough detail so that a machine can solve it. The definition could start with the idea that integers can be used for counting things. They are numbers that have no fractional part, and that have been extended so that they can be positive or negative.
When designing programs that use classes, it is likely that the classes represent types, although they may not be completely implemented. The design scheme is to sketch a high-level solution and observe what components of that solution behave like types. Those components can be implemented as classes. The remainder of the solution has a structure imposed on it by virtue of the fact that these other types exist and are defined to be used in specific ways. Types can hide their implementation, for example. The underlying nature of an integer does not matter much to a programmer most times, and so it can be hidden behind the class boundary. This has the added feature that it encourages portability: if the implementation has to change, the class can be re-written while providing the same interface to the programmer.
As noted previously, the operations on the type are implemented as methods. The methods can access the internal structure of the class while providing the desired view of the data and ways of manipulating it. The underlying representation of an integer can be unknown to a user of this class. All that is known is the interface, described as methods. If the interface is well documented, then that’s all a programmer needs to know. In fact, exposing too much of the class to a programmer can compromise it.
1. Example: A Deck of Cards
Traditional playing cards have red and black colors, four suits, and a total of 52 cards, 13 in each suit. Individual cards are components of a deck, and can be sorted: a 2 is less than a 3, and a jack less than a king. The ace is a problem: sometimes it is the high card, and sometimes it is the low card. A card possesses the characteristics suit and value. When playing card games, the cards are dealt from the deck into hands of some number of cards (for example, 13 cards for bridge and 5 for most poker games). The value of a card usually matters. Sometimes cards are compared against each other (poker), sometimes the sum of the values is important (as in blackjack or cribbage), and sometimes the suit matters. These uses of a deck of cards can be used to define how classes are created to implement card games on a computer.
Operations on a card could include to view it (it could be face up or face down) and to compare it against another card. Comparison operations could include a set of complex specifications to allow for aces being high or low and for some cards having special values (as in spades or baccarat), so a definition step might be very important.
A deck is a collection of cards. There are usually one of each card in a deck, but in some places, such as Las Vegas, there could be four or more complete decks used when playing blackjack. Operations on a deck would include to shuffle, to replace the entire deck, and to deal a card or a hand. With these things in mind, a draft of some Python classes for implementing a card deck can be created:
The way that the methods are implemented depends on the underlying representation. When the programmer calls deal(), they expect the method to return a card, which is an instance of the card class. How that happens is not relevant to them, but it is relevant to the person who implements the class. In addition, how it happens may be different on different computers, and as long as the result is the same, it does not matter.
For example, a card could be a constant value r that represented one of the 52 cards in the deck. The class could contain a set of values for these cards and provide them to programmers as a reference:
CLUBS_1 = 1 DIAMONDS 1 = 2
HEARTS_ACE = 51
SPADES_ACE = 52
def __init__ (self, face, suit):
The variables for the cards, such as CLUBS_1 and DIAMONDS_1, are accessible in all instances of the card class and have the appropriate value. Variables defined in this way have one instance only and are shared by all instances.
A second implementation could be as a tuple. The ace of clubs would be (Clubs, 1), for instance. Each has advantages, but these will not be apparent to the user of the class. For example, the tuple implementation makes it easier to determine the suit of a card. This matters to games that have trump suits. The integer value implementation makes it easier to determine values and do specific comparisons. The value of a card could be stored in a tuple named ranks, for example, and ranks[r] would be a numerical value associated with the specific card.
2. A Bouncing Bail
Animations and computer simulations see the world as a set of samples captured at discrete times. An animation, for example, is a set of images of some scene taken at fixed time intervals, generally 1/24th of a second or 1/30th of a second. Simulations use time intervals that are appropriate to the thing being simulated. This example is a simulation and animation of a bouncing ball, first in one dimension and then in two dimensions.
A ball dropped from a height h falls to the ground when released. Its speed increases as it falls, because it is being pulled downwards by gravity. The basic equation governing its movement is as follows:
s = 1/2at2 + v0t (6.1)
where s is the distance fallen at time t, v0 is the velocity the object had at time t=0, and a is the value of the acceleration. For an object at the Earth’s surface, the value of a is 32 feet/second2 = 9.8 meters/second2. For a ball being dropped, v0 is 0, since it is stationary initially. The distances at successive time intervals of 0.5 seconds are shown in Table 6.2:
A class could be made that would represent a ball. It would have a position and a speed at any given time, and could even be drawn on a computer screen. Making it bounce would be a matter of giving the ball a value that indicated how much of its energy would be lost each time it bounced, meaning that it would eventually stop moving. Writing the code for the class Ball could begin with the initialization (the constructor):
def init (self, height, elasticity):
self.height = height
self.e = e
self.speed = 0.0
self.a = 32.0
This creates and initializes four variables named height, e, a, and speed that are local to the class. Remember, the parameter self refers to the class itself, and any variable that begins with “self” is a part of the class. A variable within the function__ init__ that did not begin with “self” and was not global would belong to the function, and would be created and destroyed each time that function was called.
A method (function) that calculates the height of the ball at a specific time is something else that the Ball class should provide. This is simply the value of the class local variable height:
The self parameter has to be passed, otherwise the function cannot access the local variable height. The simulation needs values of height as a function of time, and time increases in discrete chunks. This could be implemented in several ways: the class could keep track of the time since it was dropped or it could use the time increment to determine the next speed and position. If the former, then a new class variable must be used to store the time; if the latter, then it means it has to be found to increment the speed rather than using total duration. This second idea is simpler than it sounds. The equation of motion s = 1/2at2 + v0t can use a time increment in place of t, and v0 is the velocity at the start of the time interval; this yields the new position. The new velocity can be found from a related equation of motion, which is
v = at + v0 (6.2)
where t is again the time increment and v0 is the speed at the beginning of the interval.
The function that updates the speed and position in this manner is called delta:
def delta (self, dt):
s = 0.5*self.a*dt*dt + self.speed*dt
height = height – s
self.speed = self.speed + self.a*dt
Here, the parameter dt is the time interval, and so it can be varied to get the position values at various resolutions.
For now, this is the Ball class. Some code is needed to test this class and show how well (or whether) it works, and this is the main part of the program. An instance of Ball has to be created and then the delta method is called repeatedly at time increments of, for an example, 0.1 seconds. A table of height and time can be constructed in this way, and it is a simple matter to see whether the numbers correct. The main program is as follows:
b = Ball (12.0, 0.5)
for i in range (0, 20): b.delta (0.1)
print (“At time “, i*0.1, ” the ball has fallen to”, b.height(), ” Feet”)
The results are what should be expected, showing that this class functions correctly:
At time 0.0 the ball has fallen to 12.0 Feet
At time 0.5 the ball has fallen to 7.999999999999997 Feet
At time 1.0 the ball has fallen to -4.0000000000000036 Feet
At time 1.5 the ball has fallen to -24.000000000000004 Feet
At time 2.0 the ball has fallen to -52.000000000000014 Feet
At time 2.5 the ball has fallen to -88.00000000000003 Feet
Because the initial height was 12 feet, the distance fallen is 12 minus the value given above (4, 16, 36, 64, and 100 feet), which is in agreement with the initial table for the times listed. It appears to work correctly.
This code does not yet do the bounce, though. When the height reaches 0, the ball is at ground level. It should then bounce, begin moving in the reverse direction, with a speed equal to its former downward speed multiplied by the elasticity value. This does not seem challenging until it is realized that the ball is not likely to reach a height of 0 exactly at a time increment’s boundary. At one point, the ball will be above 0 and then after the next time unit, the ball will be below 0. When does it actually hit the ground, and where will be the ball actually be at the end of the time increment? This is not a programming issue so much as an algorithmic or mathematical one, but it is a detail that is important to the correctness of the results.
It seems clear that the bounce computation should be performed in the method delta(). The height value in the class begins at a positive value and decreases towards 0 as the ball falls. During some specific call to delta(), the ball has a positive height at the beginning of the call and a negative one at the end; this means a bounce happened. At that time, the height of the ball is negative. The height of the bounced ball at the end of the time interval is the negated value of the height, so it is positive again, multiplied by the elasticity.
The speed that should be used in the bounce is based not the final speed, but the speed the ball was traveling at the time when the height was 0. This happens when self.height-s is zero, or when
self.height – s = 0.5*self.a*dt*dt + self.speed*dt
Solve this for the time xt that makes the equation work out, which is the standard solution to a quadratic equation that is taught in high school:
The value of xt is between 0 and dt, and is the time within the increment at which the ball struck the ground. At this time the ball will be moving with speed (self.speed + self.a*xt) instead of (self.speed + self.a*dt) for a normal time interval. The ball will reverse direction and reduce speed by the value of elasticity. Now the ball is moving upwards.
The ball is slowed by gravity until it stops on its upward path and drops down again. At the top of the path, its speed is 0; at the beginning of the time interval, the speed is negative, and at the end, it is positive, and that’s how the peak is detected. This situation is much simpler than the bounce.
The annotated program is as follows:
b = Ball (12.0, 0.5) # Initial height 12 feet, elasticity is 0.5
s = Screen (20, 40)
for i in range (0, 50):
b.delta (0.1) # Time increment is 0.1 seconds
How can this program be effectively tested? The computed values could be compared against hand calculations, but this is time consuming. It was done for a few cases and the simulation was accurate. For this example, another program was written in a different programming language to calculate the same values and the result from the two programs was compared – they were nearly exactly the same. This is not definitive, but is certainly a good indication that this simulation is working properly. In both programs, similar approximations were made, and the numbers agreed to seven decimal places.
Early in the development of personal computers, a simple game was created that involved shooting cannons. The player would set an angle and a power level and a cannonball would be fired towards the opposing cannon. If the ball struck the cannon, then it would be destroyed, but if not, then the opposing player (or the computer) would fire back at the player’s cannon. This process would continue until one or the other cannon was destroyed. This game evolved with time, with more complex graphics, mountainous terrain, and complexity. Its influence can be seen in modern games like Angry Birds.
A variation of this game is proposed as an example of how classes can be used. The basic idea is to eliminate a mouse that is eating your garden by firing cats at it; hence the name cat-a-pult. The game uses text as input and output, because no graphics facility is available yet. A player types the angle and the power level and the computer fires a cat at the mouse. The location where the cat lands is marked on a simple character display and the player can try again. The goal is to hit the mouse with as few tries as possible.
Before writing any code, one needs to consider the items in this game and the actions they can take. The items are classes, and the actions are methods. There
seem to be two items: a cannonball (a cat) and a cannon. The target (the mouse) could be a class, too. The cannon has a location, an angle, and a power or force with which the cannonball will be ejected. Both of the last two factors affect the distance the ball travels. The cannon is given a target as a parameter – in this example, the target is another cannon, basically to avoid making yet another class definition.
The action a cannon can perform is to be fired. This involves releasing a cannonball with a particular speed and direction from the location of the cannon. In this implementation, an instance of the cannonball class is created when the cannon is fired and is given the angle and velocity as initial parameters; the ball is independent from then on. As a class, the ball has a position (x,y) and a speed (dx, dy). The action that it can perform is to move, which is accomplished using a method named step(), and to collide with something, accomplished by the method testCollision().
In the metaphor of this game, the cannonball is a cat and the target is a mouse, but to the program, these details are not important. Here’s what is important:
All of the Has aspects are class local variables, and in this design, they are initialized within the __init__method of each class. This would entail the following:
The game is essentially one-dimensional. The cannonball lands at a specific x coordinate, and if that is near enough to the x coordinate of the target, then the target is destroyed and the game is over. Without a way to draw proper graphics, this can be imagined as a simple text display with the cannon on one side of the screen and the target on the other, something like that seen in Figure 6.1.
The slash character (/) on the left represents the cannon, and the “Y” represents the mouse, which is the target. The cannon is at horizontal coordinate 12, and the mouse is at 60; both vertical coordinates are 0.
All of the Does aspects represent actions, or things the class object can do. When the cannon is fired, the ball is created at the cannon coordinates (12, 0) and is given a speed that is related to the angle and power level using trigonometric calculations (Figure 6.2).
dy = sin(angle * 3.1415/180.0)
dx = cos(angle * 3.1415/180.0)
The angles passed to sin and cos must be in radians, so the value PI/180 is used to convert degrees into radians. The coordinates in this case have y increasing as the ball moves upwards. When the cannon is fired, a ball is created that has the x and y coordinates of the cannon and the dx and dy values determined as above. This is accomplished by a method named fire():
Fire: takes an angle and a power
Angle is in degrees, between 0 and 360 Power is between 0 and 100 (a percentage)
- Compute values for dx and dy from angle and power, where max power is 0.1.
- Create an instance of Ball giving it x, y, dx, dy, a name (“cat”), and a target (the mouse)
The simulation makes time steps of a fixed duration and calculates positions of objects at the end of that step. Each object should have a method that updates the time by one interval, and it will be named step(). The cannon does not move, but sometimes has a cannonball that it has fired, so updating the status of the cannon should update the status of the ball as well:
Step 1: Make one-time step for this object in the simulation. No parameters.
- If a ball has been fired, then update its position. This is done by calling the step() method of the ball.
This defines the cannon.
The ball must also possess a step() method, and it will update the ball’s position based on its current speed and location. The x position is increased by dx, and the y is increased by dy. Gravity pulls down on the ball, effectively decreasing the vertical speed of the ball during each interval. After some trials, it was determined that the value of dy should be decreased by the value of gravity during each interval. If the ball strikes the ground, it should stop moving. When does this happen? When y becomes smaller than 0. When this occurs, set dx and dy to 0, and check to see if the impact location is near to the target.
Step 2: Make one-time step for this object in the simulation. No parameters.
- Let x = x + dx, changing the x position.
- Let y = y + dy, changing the y position.
- Decrease dy by gravity (dy = dy – gravity)
- If the ball has struck the ground
- Let dx = dy = gravity = 0
- Check for collision with target
Checking to see if the ball hit the target is a matter of looking at the x value of the ball and the x value of the target. If the difference is smaller than some predefined value, say 1.0, then the target was hit. This is determined by a method called testCollision(). If the collision occurred, then success has been achieved by the player, so set a flag that ends the game.
testCollision: Check to see if the ball has hit the target, and if so, set a flag to True.
- Subtract the x position of the ball from the x position of the target. Call this d.
- If d <= 1.0, then set a flag done to True.
This defines the class Ball and completes the two major classes.
The main program that uses these classes could look something like this:
Actual code for most of this example is shown in Figure 6.4, and the entire program is on the accompanying disk. Included in the disk version is an extra class that draws each state of the game as character graphics that can be displayed in the Python output window; the example in the figure does not include any output, and is unsatisfying to execute The program on the disk generates a numeric and graphical representation of the state, showing the axes, the cannon, the ball, and the target after each step. These can be made into distinct text files and can be made into an animation using MovieMaker on a Windows computer or Final Cut on a Mac. Such an animation is also included on the disk, and is named catapult.mp4.
The process above loosely defines a way to design and code a program that uses classes.
Source: Parker James R. (2021), Python: An Introduction to Programming, Mercury Learning and Information; Second edition.