Describing the Problem as a Process in Python

The first step is to write down a step-by-step description of how the pro­gram might operate. This may be changed as it is expanded, but we have to start someplace. A problem occurs almost immediately: is the program to be a class? Functions? Does it use pygame?

This decision can be postponed a little while, but in most cases, a program is not a class. It is more likely to be a collection of classes operated by a mail program. However, if object orientation is a logical structure, and it often is, it should evolve naturally from the way the problem is organized and not impose itself on the solution.

The game consists of multiple things that interact. Play is a consequence of the behavior of those things. For example, the ball will collide with a tile resulting in some points, the tile disappearing, and a bounce. The next event may be that the ball collides with a wall, or bounces off of the paddle. The game is a set of managed events and consequences. This makes it appear as if an object oriented design and implementation would be suitable. The ball, each time, and the paddle could be objects (class instances) and could interact with each other under the supervision of a main program which kept track of all objects, time, scores, and other details.

Let’s just focus on the gameplay part of the game, and ignore the introduc­tory windows and high score lists and other parts of a real game. The game starts with an initial set up of the objects. The tiles are placed in their start locations, the paddle is placed, the locations of the walls are defined, and then these will be drawn. The initial setup was originally drawn on paper and then a sample render­ing was made, shown in Figure 12.1. The code that draws this is as follows:

import pygame

width = 400 height = 800

screen = pygame.display.set mode((width, height))

clock = pygame.time.Clock()

pygame.init()

FPS = 30

for i in range(0, 12):

pygame.draw.circle(screen, (100, 100, 240), (i*30+15,30), 15)

for i in range(0, 12):

pygame.draw.circle(screen, (220, 220, 90), (i*30+15,60), 15)

for i in range (0, 12):

pygame.draw.circle(screen, (220, 0, 0), (i*30+15, 90),15)

for i in range(0, 12):

pygame.draw.circle(screen, (180, 120, 30), (i*30+15,120), 15)

for i in range(0, 12):

pygame.draw.circle(screen, (90, 220, 8), (i*30+15,150), 15)

pygame.draw.rect(screen, (0,0,0), (180, 350, 90, 10))

while True:

clock.tick(FPS)

for event in pygame.event.get():

if event.type == pygame.QUIT:

quit()

pygame.display.update()

This code is just for a visual examination of the potential play area. The first one is always wrong, and this one is too, but it allows us to see why it is wrong and to define a more reasonable set of parameters. In this case, the tiles don’t fully occupy the horizontal region and the tile groups are too close to the top, because we want to allow a ball to bounce between the top row and the top of the play area. The play area is too large vertically. Fixing these problems is a simple matter of modifying the coordinates of some of the objects. This code is not a part of the final result. It’s common, especially in programs involving a lot of graph­ics, to test the visual results periodically, and to write some testing programs to help with this.

This program already has some obvious objects: a tile is an object, and so are the paddle and the ball. These objects have some obvious properties too: a tile has a position in x,y coordinates, and it has a color and a size. It has a method to draw it on the screen, and a way to tell if it has been removed or if it still active. The paddle has a position and size, and so does the ball, although the ball has not been seen yet.

What does the main program look like if these are the principal objects in the design? The first sketch is abstract and depends on many functions that have not been written. This code shows the way the classes and the remainder of the code will interact and partly defines the methods they will implement. The initializa­tion step involves creating rows of tiles that will appear much like those in the ini­tial rendering above, but actually consist of five rows of tile objects. This will be done from the function initialize(), but each row should be created in a for loop:

for i in range (0,  12):

tiles = tiles + tile(i*30+15, y, thiscolor, npoints)

where the tile will be created and is passed its x,y position, color, and number of points. The entire collection of tiles is placed into a tuple named tiles. The ball will be created at a random location and with a random speed within the space between the paddle and the tiles, and the paddle will be created so that is initially is drawn in the horizontal center near the bottom of the window.

def initialize ():

score = 0

nballs = 2

b = ball ()                 # Create the ball

p = paddle ()               # create the paddle

thiscolor = (100,100,240)   # Blue

npoints = 5                 # Top row is 5 points each

for i in range (0,  12):

tiles = tiles + tile(i*30+15, y, thiscolor, npoints)

# and so on for 4 more rows

The main draw() function calls the draw() methods of each of the class in­stances, and they draw themselves:

def draw():

global tiles,p,b

screen.fill((200, 200, 200))

# Tiles

for k in tiles:

k.draw()

#Paddle

p.draw()

# Ball

b.draw()

When this function is called (many times each second), the ball is placed in its new position, possibly following a bounce, and then it is drawn. The paddle is drawn, and if it is to be moved it will be done through the user pressing a key. Then the active tiles are drawn, and the messages are drawn on the screen. The structure of the main part of the program is defined by the organization of the classes.

1.  Initial Coding for a Tile

A tile has a graphical representation on the screen, but it is more complex than that. It can collide with a ball and has a color and a point value. All of these aspects of the tile have to be coded as a part of its class. In addition, a tile can be active, meaning that it appears on the screen and can collide with the ball, or inactive, meaning that the ball has hit it and it is out of play for all intents and purposes. Here’s an initial version:

class tile:

def init (self, x, y, color, points):

self.x = x

self.y = y

self.color = color

self.points = points

self.active = True

self.size = 30

def draw(self):

if self.active:

pygame.draw.circle(screen, (self.color[0],

self.color[1], self.color[2]), (int(self.x),

int(self.y)), self.size // 2)      # Ball is a circle

At the beginning of the game, every tile must be created and initialized with its proper position, color, and point value. Then the draw() function for the main program calls the draw() method of every tile during every small time in­terval, or frame. According to the prior code, if the tile is not active, then it will not be drawn. Let’s test this.

Rule: Never write more than 20-30 lines of code without testing at least part of it. That way you have a clearer idea where any problems you introduce may be.

A suitable test program to start with could be as follows:

def draw():

global tiles for k in tiles: k.draw()

tiles = ()

red = (250, 0, 0)

for i in range (0, 12):

tiles = tiles + (tile(i*30+15, 90, red, 15),)

which places some tiles on the screen in a row, passing a color and point value. This almost works, but the first tile is cut in half by the left boundary. If the ini­tialization becomes

tiles = tiles + (tile(i*30+15, 90, red, 15),)

then a proper row of 12 red circles is drawn. Modifications will be made to this class once we see more clearly how it will be used.

2. Initial Coding for the Paddle

The paddle is represented as a rectangle on the screen, but its role in the game is much more profound: it is the only way the player has to participate in the game. The player types keys to control the position of the paddle so as to keep the ball from falling out of the area. The ball has to be drawn, as the tiles do, but it also must be moved (i.e., change the X position) in accordance with the player’s wishes. The paddle class initially has a few basic operations:

class paddle:

def_init_(self, x, y):

self.x = x

self.y = y

self.speed = 3

self.width = 90

self.height = 10

def draw(self):

pygame.draw.rect (screen, (self.color[0], self.

color[1], self.color[2]), (self.x, self.y, self.width, self.height))

def moveleft(self):

if self.x <= self.speed: self.x = 0

else:

self.x = self.x – self.speed

def moveright (self):

if self.x > width-self.width-self.speed:

self.x = width-self.width

else:

self.x = self.x + self.speed

When the right arrow key is pressed, a flag is set to True, and the paddle moves to the right (i.e., its x coordinate increases) each time interval, or frame. When the key is released, the flag is set to False and the movement stops as a result. Movement is accomplished by calling moveleft() and moveright(), and these functions enforce a limit on motion: the paddle cannot leave the play area. This is done within the class so that the outside code does not need to know any­thing about how the paddle is implemented. It is important to isolate details of the class implementation to the class only, so that modifications and debugging can be limited to the class itself.

The paddle is simply a rectangle, as far as the geometry is concerned, and presents a horizontal surface from which the ball will bounce. It is the only means by which the player can manipulate the game, so it is important to get the paddle
operations and motion correct. Fortunately, moving a rectangle left and right is an easy thing to do.

3. Initial Coding for the Ball

The ball really does much of the actual work in the game. Yes, the bounces are controlled by the user through the paddle, but once the ball bounces off of the paddle, it has to behave properly and do the works of the game: destroying tiles. According to the standard class model of this program, the ball should have a draw() method that places it into its proper position on the screen. But the ball is moving, so its position has to be updated each frame. It also has to bounce off of the sides and top of the playing area, and the draw() method can make this hap­pen. The essential code for doing this is as follows:

class ball():

def_init_(self, x, y):

self.x = x

self.y = y

self.dx = 3

self.dy = -4

self.active = True

self.color = (230, 0, 230)

self.size = 9

def draw(self):

if not self.active: return

pygame.draw.circle(screen, (self.color[0],

self.color[1], self.color[2]), (int(self.x), int(self.y)), self.size // 2)

# Ball is a circle

self.x = self.x + self.dx

self.y = self.y + self.dy

if (self.x <= self.size/2) or \

(self.x >= width-self.size/4):

self.dx = -self.dx if

self.y <= self.size/2:

self.dy = -self.dy

elif self.dy >= height:

self.active = False

This version only bounces off of the sides and top, and passes through the bottom.

4. Collecting the Classes

A next step is to test all three classes running together. This will ensure that there are no problems with variable, method, and function names, and that inter­actions between the classes are isolated. All three should work together, creat­ing the correct visual impression on the screen. The code for the three classes was copied to one file for this test. The main program simply creates instances of each class as appropriate, really doing what the original test program did in each case:

red = (250, 0, 0) print (red) tiles = ()

for i in range (0,  12):

tiles = tiles + (tile(i*30+15, 90, red, 15),)

f = True

p = paddle (130)

b = ball (300, 300)

The draw() function calls the draw() methods for each class instance and moves the paddle randomly as before:

def draw():     # 07-classes-01-20.py

global tiles,p,f,b,movingleft,movingright

screen.fill((200, 200, 200))

# Tiles

for k in tiles:
k.draw()

# Paddle

if movingleft:

p.moveleft() elif movingright: p.moveright() p.draw()

# Ball

b.draw()

The result was that all three classes functioned together the first time it was attempted. The game itself depends on collision, which will be implemented next, but at the very least the classes need to cooperate, or at least not interfere with each other. That’s true at this point in the development.

5. Developing the Paddle

Placing the paddle under control of the user is the next step. When a key is pressed, then the paddle state changes, from still to moving, and vice versa when released. This is accomplished using the keypressed() and keyreleased() func­tions. They set or clear a flag, respectively, that causes the paddle to move by calling the moveleft() and moveright() methods. The flag movingleft results in a decrease in the paddle’s x coordinate each time draw() is called; movingright does the same for the +x direction:

def keyPressed (k):

global movingleft, movingright

if k.key == pygame.K LEFT:

movingleft = True

elif k.key == pygame.K RIGHT:

movingright = True

def keyReleased (k):

global movingleft, movingright

if k.key == pygame.K LEFT:

movingleft = False

elif k.key == pygame.K RIGHT:

movingright = False

From the user’s perspective, the paddle moves as long as the key is depressed. Inside of the global draw() function, the flags are tested at each iteration and the paddle is moved if necessary:

def draw():      # 07-classes-01-20.py

global … movingleft,movingright

if movingleft:

p.moveleft()

elif movingright:

p.moveright()

p.draw()

The other thing the paddle has to do is serve as a bounce platform for the ball. A question surrounds the location of collision detection; is this the job of the ball or the paddle? It does make sense to perform most of this task in the ball class, because the ball is always in motion and is the thing that bounces. However, the paddle class can assist by providing necessary information. Of course, the paddle class can allow other classes to examine and modify its position and velocity, and thus perform collision testing, but if those data are to be hidden, the option is to have a method that tests whether a moving object might have collided with the paddle. The y position of the paddle is fixed and is stored in a global vari­able paddle, so that is not an issue. A method in paddle that returns True if the x coordinate passed to it lies between the start and end of the paddle is as follows:

def inpaddle(self, x):

if x < self.x:

return False

if x > self.x + self.width:

return False

return True

The ball class can now determine whether it collides with the paddle by checking its own y coordinate against the paddle and by calling inpaddle() to see if the ball’s x position lies within the paddle. If so, it should bounce. The method hitspaddle() in the ball class returns True if the ball hits the paddle:

def hitspaddle (self):       # 08classes-01-21.py

if self.y<=paddleY+2 and self.y>=paddleY-2:

if p.inpaddle(self.x):

return True

return False

The most basic reaction to hitting the paddle is to change the direction of dy from down to up (dy = -dy).

6. Ball and Tile Collisions

The collision between a ball and a tile is more difficult to do correctly than any of the other collisions. Yes, determining whether a collision occurs is a similar process, and then points are collected and the tile is deactivated. It is the bounce of the ball that is hard to figure out. The ball may strike the tile at nearly any angle and at nearly any location on the circumference. This is not a problem in the original game, where the tiles were rectangular, because the ball was always bouncing off of a horizontal or vertical surface. Now there’s some thinking to do.

The correct collision could be calculated, but would involve a certain amount of math. The specification of the problem does not say that mathematically cor­rect bounces are required. This is a game design choice, perhaps not a program­ming choice. What does the game look like if a simple bounce is implemented? That could involve simply changing dy to -dy.

This version of the game turns out to be playable, but the ball always keeps the same x direction when it bounces. What would it look like if it bounced in roughly the right direction, and how difficult would that be? The direction of the bounce would be dictated by the impact location on the tile, as seen in Figure 12.3. This was determined after a few minutes with a pencil and paper, and it is intui­tive rather than precise.

We need to find where the ball hits the tile, determine which of the four parts of the tile this lies in, and then create the new dx and dy values for the ball. A key aspect of the solution being developed is to avoid too much math that has to be done by the program. Is this possible?

The first step is to find the impact point. We could use a little bit of analytic geometry, or we could approximate. The fact is that the ball is not moving very fast, and the exact coordinates of the impact point are not required. At the begin­ning of the current frame, the ball was at (x, y) and at the beginning of the next, it is at (x + dx, y + dy). A good estimate of the point of impact is the mean value of these two points, or (x + dx/2, y + dy/2).

Within which of the four regions defined in Figure 12.3 is the impact point? The regions are defined by lines at 45 degrees and -45 degrees. The atan() func­tion will, when using screen coordinates, have the -dx points between -45 and +45 degrees. The -dy points, where the direction of Y motion changes, involve the remaining collisions. What needs to be done is to find the angle of the line from the center of the to the ball and then compare that to -45 … +45.

Here is an example method named bounce() that does exactly this.

# Return the distance squared between the two points

def distance2 (self, x0,y0, x1, y1):

return (x0-x1)*(x0-x1) + (y0-y1)*(y0-y1)

def bounce (self, t):

dd = t.size/2 + self.size/2  # Bounce occurs when the

                             # distance

dd = dd * dd                 # Between ball and tile <

                             # radii squared

collide = False

if self.distance2 (self.x, self.y, t.x, t.y) >= dd and \

self.distance2 (self.x+self.dx, self.y+self.dy, t.x, t.y) < dd:

self.x = self.x + self.dx/2   # Estimated impact point on

                              # circle

self.y = self.y + self.dy/2

collide = True

elif self.distance2 (self.x, self.y, t.x, t.y) < dd:

collide = True       # Ball is completely inside the time

if not collide:
return

# If the ball is inside the tile, back it out.

while self.distance2 (self.x, self.y, t.x, t.y) < dd:

self.x = self.x – self.dx*0.5

self.y = self.y – self.dy*0.5

if self.x != t.x:                # Compute the ball-tile

                                 # angle

a = atan ((self.y-t.x)/(self.x-t.y))

a = a * 180./3.1415

else:                         # If dx = 0 the tangent is

                              # infinite

a = 90.0

if a >= -45.0 and a<=45.0:     # The x speed change

self.dx = -self.dx

else:

self.dy = -self.dy         # The y speed changes

After testing the code, we include

# If the ball is inside the tile, back it out.

while self.distance2 (self.x, self.y, t.x, t.y) < dd:

self.x = self.x – self.dx*0.5

self.y = self.y – self.dy*0.5

It was found that if the ball was too far inside the tile, then its motion was very odd; as it moved through the tile, it constantly changed direction because the program determined that it was always colliding.

7. Ball and Paddle Collisions

Let’s examine the collision between the ball and the paddle. The paddle seems to be flat, and colliding with any location on the paddle should have the same result. What if the ball hits the paddle very near to one end? There is a corner, and maybe hitting too near to the corner would yield a different bounce. This was the case in the original games. If the ball struck the near edge of the paddle on the corner, it could actually bounce back in the original direction to a greater or lesser degree. This gives the player a greater degree of control, once they understand the situation. Otherwise, the game is pre-determined if the play­er merely places the paddle in the way of the ball. It will always bounce in exactly the same manner.

The proposed idea is to bounce at a different angle depending on where the ball strikes the paddle. We need to decide how near and how intense the effect will be. If the ball hits the paddle near the center, then it will bounce so that the incoming angle is the same as the outgoing angle. When it hits the near end of the paddle, it will bounce somewhat back in the incoming direction, and when it strikes the far end, the bounce angle will be a shallower bounce from the center.

Let’s say that if the ball hits the first pixel on the paddle, it will bounce back in the original direction, meaning that dx = -dx and dy = -dy. A bounce from the center does not change dx but does set dy = -dy. If the relationship is linear across the paddle, the implication would be that striking the final pixel would set dx = 2*dx and dy = -dy. Striking any pixel in between would divide the change in dx by the number of pixels in the paddle, initially 90. If the ball hits pixel n, the result is as follows:

delta = 2*dx/90.0

dx = -dx + n*delta

A problem here is that the dx value decreases continuously until the ball is bouncing up and down. Perhaps the incoming angle should not be considered. The bounce angle of the ball could be completely dependent on where it hits the paddle and nothing else. If dx is -5 on the near end of the paddle and +5 on the far end, then,

dx = -5 + n*10.0/90.0

The code in the draw() method of the ball class is modified to read:

if self.hitspaddle():

self.dy = -self.dy

self.dx = -5 + (1./9.)*(self.x-p.x)

The user now has more control. The game does appear slow, though. In addi­tion, there is only one ball. Once that is lost, the game is over.

8. Finishing the Game

What remains to be done is to implement multiple balls. Multiple balls are tricky because there are timing issues. When the ball disappears through the bottom of the play area, it should reappear someplace, and at a random place. It should not appear immediately, though, because the player needs some time to respond; let’s say three seconds. Meanwhile, the screen must continue to be dis­played. It’s time to introduce states.

A state is a situation that can be described by a characteristic set of param­eters. A state can be labeled with a simple number, but represents something complex. In this instance, specifically there will be a play state, in which the paddle can be moved and the ball can score points, and a pause state, which hap­pens after a ball is lost. The draw() function is the place where each step of the program is performed at a high level, and so will be responsible for the manage­ment of states.

The current stage of the implementation has only the play state, and all of the code that manages that is in the draw() function already. Change the name of draw() to state0() and create a state variable state that can have values 0 or 1: play is 0, pause is 1. The new draw() function is now created:

def draw ():

global playstate, pausestate

if state == playstate:

state0()

elif state == pausestate:

state1()

where

playstate = 0

pausestate = 1

The program should still be playable as it was before as long as state == playstate. What happens in the pause state? The controls of the paddle should be disabled, and no ball is drawn. The goal of the pause state is to allow some time for the user to get ready for the next ball, so some time is allowed to pass. Perhaps the player should be permitted to start the game again with a new ball when a key is pressed. This eliminates the need for a timer, which are generally to be avoided. The pause state is entered when the ball departs the field of play. The game remains in the pause state until the player presses a key, at which point a new ball is created and the game enters the play state.

Entering the pause state means modifying the code in the ball class a little. There is a line of code at the end of the draw() method of the ball class that looks like this:

elif self.dy >= height:

self.active = False

This is where the class detects the ball leaving the play area. We need to add to this code:

if self.y >= height:                #   LEaves the play area?

if balls remaining>0:               #     Yes. Balls left?

state = pausestate             # Yes. Pause

balls remaining = balls remaining-1

                                    # One less ball …

else:

state = gameoverstate          # No. Game over

while, of course, making certain that the variables needed are listed as global. This did not do as was expected until it was noted that the condition should have been if self.y >= height. The comparison with dy was an error in the initial cod­ing that had not been noticed. It also seems like the active variable in the ball class was not useful, so it was removed.

Now, in the keyPressed() function, we allow a key press to change from the pause to the play state. Any key will do:

if state = pausestate: resume()

The resume() function must do two things. First, it must change state back to play. Next it must reposition the ball to a new location.

def resume():

global state, playstate

b.x = randrange (30, width-30)

b.y = 250

state = playstate

This works fine. The game is to only have a specified number of balls, though, and this number was to be displayed on the screen. When in the play state and a ball is lost, the count of remaining balls (balls_remaining) is decreased by one. If there are any balls remaining, then the pause state is entered. Otherwise, the game is over. Perhaps that should be a third state: game over.

The game-over state is entered when the ball leaves the play area and no balls are left (in the ball class draw() method). In the global draw() function, the third state determines if the game is won or lost and renders an appropriate screen:

if score >= maxscore:           # The gameover state. Win?

screen.fill((0,230, 0))     # Yes

text (“You Win”, 200, 200)

else:

screen.fill((200, 10, 10)) # Lose, there are btiles left.

text (“You Lose”, 200, 200)

text (“Score:”+str(score), 10, 30)

Screen shots from the game in various states are shown in Figure 12.4 (14playble3.py).

Source: Parker James R. (2021), Python: An Introduction to Programming, Mercury Learning and Information; Second edition.

Leave a Reply

Your email address will not be published. Required fields are marked *