Ping - Software Projects - Raspberry Pi Projects (2014)

Raspberry Pi Projects (2014)

Part II. Software Projects

Chapter 5. Ping

by Mike Cook

In This Chapter

• Describe movement to a computer

• Display a dynamic game

• Discover one way to detect collisions between on-screen objects

• Handle the physics of reflection

• Make single-player and two-player versions of the game

A version of Ping-Pong, or table tennis, was one of the early electronic games; it was first produced by Willy Higginbotham in 1958 and used an oscilloscope as a display. This was long before the advent of microcomputers. You could make a version of the game by using just logic gates, with no processors involved. I made one back in 1970 that generated its own TV signal. It was, by today’s standards, a weird hybrid of an analogue and digital circuit – with the path of the ball being driven by two analogue integrators, and those voltages triggering comparators against an analogue sawtooth TV time base. By 1975 all the logic had been combined into one chip, the AY-3-8500, making it a much easier circuit to build. It even had a choice of games which were simple variants on the same theme. This chip appeared in hundreds of products and was the first low-cost home TV console. I even built a console using this chip as well. A few years after that, home microcomputer systems came along, and table tennis was one of the first graphics games to be implemented.

Early Commercial Products

On the commercial side, the Magnavox Odyssey, designed by Ralph Baer, was the first game console to go on sale to the general public. This was first demonstrated in 1968, but was not commercially available until 1972. It was seen before its launch by Nolan Bushnell, who cofounded Atari, so he assigned the newly appointed engineer Allan Alcorn to implement the game as a training exercise. The results were so impressive that Bushnell decided to launch it as an video arcade game using the name Pong. This name sounded odd to U.K. ears, as it is a slang word for a very bad smell. Just a few days into the testing of the first prototype in a bar, the owner rang up to say that the game had stopped working. On investigation they found that the money had backed up and jammed the mechanism. So the first upgrade was to fit a bigger coin box – something Bushnell later said he was “happy to live with”. Inevitably Magnavox and Atari ended up in court, but out-of-court settlements were reached. Pong then went on to be released as a home TV console.

So the game has a honored place in the history of computing and serves as an interesting introduction into arcade type games on the Raspberry Pi. You might think that being a game from the early days of computing it will be simple, and it is, but it is not as simple as you might hope. The early games were written in Assembler language; with today’s computing power you can write a table tennis game in a high-level language like Python.

The Ping Game

Basically, what I am calling Ping is a copy of the bat-and-ball game which spawned a whole generation of computer games. What you are going to implement here are two games, a one-player game and a two-player game. Figure 5.1 shows the screen display of the two-player game, but they look very similar. As with all projects, it is best to start implementing features one at a time and building up the sophistication and complication as you go. However, first you need a bit of theory.

Figure 5-1: The screen display of the Ping game.

image

On-screen Movement

Movement on the computer screen is created by drawing a number of separate pictures in quick succession with one or more elements moving between each frame. In the case of a ball moving across the screen, there are two components to the movement: a change in the X coordinate and a change in the Y coordinate. Whenever you are dealing with changes there is a special word scientists use – delta, which is represented by the Greek letter delta (∆) and just means “change”. So to describe the path of an object moving in a straight line, all you need is two quantities – ∆X and ∆Y. On each successive frame ∆X and ∆Y are added to the X and Y coordinates to get the new position to plot the object. This defines the angle of movement as shown in Figure 5.2.

Figure 5-2: The movement angle defined by the two values ∆X and ∆Y.

image

If you want to define the exact angle Θ (angles are always called theta), you can apply the formula shown in Figure 5.2. However, for this project there is no need to work in terms of angles, basically because all reflections are from orthogonal surfaces. This means that you are considering only horizontal or vertical surfaces to reflect from. Take a look at Figure 5.3; here you see a block bouncing off, or being reflected from, a vertical surface. The angle it is reflected at is equal to the incoming angle or incident angle. But the point is that you don’t have to know the actual angle – in fact, you don’t care. All that you need to do is reverse the sign of ∆X – that is, make it negative. The same goes if the block is approaching the reflecting surface from the other direction. ∆X will be negative in that case, and all you need to do is to reverse the sign. If you make a negative number negative, you end up with a positive.

Figure 5-3: The reflection from a vertical surface by negating ∆X.

image

I think you can see that exactly the same applies for reflections off a horizontal surface – only this time it is ∆Y that is negated. So when the time comes to bounce off a surface all that you need to do is to invert the appropriate delta value.

Detecting Collisions

Now all you need to get some bouncy action is to work out when things collide. This is easy for humans to spot but can be a bit tricky for a computer. Every object you draw on the screen will be put there by specifying one or more coordinates. However, that describes only one point on the object. Take a rectangle, for example: You specify the X and Y coordinates of the top-left corner and the width and height, as well as the line thickness. When you draw a line, you specify the X and Y coordinates of the start of the line, and the coordinates of where you want it to finish, along with the line thickness. There are two ways of detecting if these overlap: The first is to look at what is already drawn on the screen to see if anything is in the place you are going to draw the next block. The second, and the one you shall use here, is to compute the extent of the objects and see if there is an overlap. Figure 5.4 shows this calculation for a rectangle and a line. Note the difference in how the line thickness is handled. For a line the thickness is built up by drawing in pixels either side of the required line, whereas for a rectangle the thickness is built up by drawing in pixels inside the rectangle. This is the way that Pygame handles the graphic coordinates; other systems such as Apple’s QuickTime take a different approach, with the defined line being in between the pixels and any line thickness being below and to the right of the line. There are many ways to implement graphics drawing routines.

Figure 5-4: Calculating limits for a collision.

image

You will see from Figure 5.4 that the limits set depend on the direction of approach the object has, so when going from left to right it is different than going from right to left.

The Test Bounce

Armed with this information, you can now get some coding done and set the framework for your game. Type in the code in Listing 5.1, and it will bounce a square around a window at high speed.

Listing 5.1 Bounce Test 1

#!/usr/bin/env python

"""

Bounce

A Raspberry Pi test

"""

import time # for delays

import os, pygame, sys

pygame.init() # initialise graphics interface

os.environ['SDL_VIDEO_WINDOW_POS'] = 'center'

pygame.display.set_caption("Bounce")

screenWidth = 400

screenHeight =400

screen = pygame.display.set_mode([screenWidth, image

screenHeight],0,32)

background = pygame.Surface((screenWidth,screenHeight))

# define the colours to use for the user interface

cBackground =(255,255,255)

cBlock = (0,0,0)

background.fill(cBackground) # make background colour

dx = 5

dy = 10

def main():

X = screenWidth / 2

Y = screenHeight /2

screen.blit(background,[0,0])

while True :

checkForEvent()

#time.sleep(0.05)

drawScreen(X,Y)

X += dx

Y += dy

checkBounds(X,Y)

def checkBounds(px,py):

global dx,dy

if px > screenWidth-10 or px <0:

dx = -dx

if py > screenHeight-10 or py < 0:

dy = - dy

def drawScreen(px,py) : # draw to the screen

screen.blit(background,[0,0]) # set background colour

pygame.draw.rect(screen,cBlock, (px, py, 10 ,10), 0)

pygame.display.update()

def terminate(): # close down the program

print ("Closing down please wait")

pygame.quit() # close pygame

sys.exit()

def checkForEvent(): # see if you need to quit

event = pygame.event.poll()

if event.type == pygame.QUIT :

terminate()

if event.type == pygame.KEYDOWN and image

event.key == pygame.K_ESCAPE :

terminate()

if __name__ == '__main__':

main()

You will see that the direction of the block is defined by the two variables dx and dy (for delta X and delta Y) and is fixed at five pixels in the X direction and ten in the Y direction per frame. The square is drawn in solid black on a white background.

The main function first of all defines the square in the center of the screen, and goes into an endless loop in which it checks the keyboard for a quit and draws the screen. Then it updates the square’s position and checks for any collision between the square and the sides of the window. Note as the sides of the window are not a drawn line, there is no need to account for the line thickness here.

In the main loop there is a sleep command, but this is commented out with a #, which means that it is ignored. Try removing the # and see how much slower the block will move around. Also experiment with changing the values of dx and dy and see how this changes both the speed and the angle of trajectory of the block. Finally, have a play with adjusting the screenWidth and screenHeight variables.

Improving the Ping Game

Well, Listing 5.1 is a good start, but a number of things are not quite right in it. First of all, the object bouncing about is a square, and although that is true to the original game, you can do much better these days. So let’s make the object a circle instead of a square. This will change the collision calculations because a circle is drawn by defining its center point. Also, if you rely on simply detecting when the position of the ball is greater than the collision limits you calculated, you will most times draw the ball actually overlapping the line. This will look like the ball has penetrated into the reflecting object. It would be much better if, when you detect a collision, the position of the ball is adjusted to sit on the colliding surface. This is shown in Figure 5.5; the ball’s position is shown in successive frames, and when the collision occurs the position of the ball would normally be behind the reflecting object. This does not look good, so it is hauled back to sit on the line. This has the effect of slowing down the ball at the point of collision, but you do not perceive this as you are expecting a collision to occur at the surface and the brain’s expectation overrules anything else.

Figure 5-5: Snapping the ball to the colliding object.

image

To make the bouncing a bit more interesting, a sound should be produced at the moment of impact, something that did not occur in the first incarnations of the game. Finally, the code makes the bounding rectangle (the rectangle containing the ball) adjustable from the cursor keys of the keyboard. All this is added to produce the code in Listing 5.2.

Listing 5.2 The Improved Bounce Code

#!/usr/bin/env python

"""

Bounce with sound

A Raspberry Pi test

"""

import time # for delays

import os, pygame, sys

pygame.init() # initialise graphics interface

pygame.mixer.quit()

pygame.mixer.init(frequency=22050, size=-16, image

channels=2, buffer=512)

bounceSound = pygame.mixer.Sound("sounds/bounce.ogg")

os.environ['SDL_VIDEO_WINDOW_POS'] = 'center'

pygame.display.set_caption("Bounce2")

screenWidth = 400

screenHeight =400

screen = pygame.display.set_mode([screenWidth,image

screenHeight],0,32)

background = pygame.Surface((screenWidth,screenHeight))

# define the colours to use for the user interface

cBackground =(255,255,255)

cBlock = (0,0,0)

background.fill(cBackground) # make background colour

box = [screenWidth-80,screenHeight-80]

delta = [5,10]

hw = screenWidth / 2

hh = screenHeight /2

position = [hw,hh] # position of the ball

limit = [0, 0, 0, 0] #wall limits

ballRad = 8 # size of the ball

def main():

global position

updateBox(0,0) # set up wall limits

screen.blit(background,[0,0])

while True :

checkForEvent()

time.sleep(0.05)

drawScreen(position)

position = moveBall(position)

def moveBall(p):

global delta

p[0] += delta[0]

p[1] += delta[1]

if p[0] <= limit[0] :

bounceSound.play()

delta[0] = -delta[0]

p[0] = limit[0]

if p[0] >= limit[1] :

bounceSound.play()

delta[0] = -delta[0]

p[0] = limit[1]

if p[1] <= limit[2] :

bounceSound.play()

delta[1] = - delta[1]

p[1] = limit[2]

if p[1] >= limit[3] :

bounceSound.play()

delta[1] = - delta[1]

p[1] = limit[3]

return p

def drawScreen(p) : # draw to the screen

screen.blit(background,[0,0]) # set background colour

pygame.draw.rect(screen,(255,0,0), (hw - image

(box[0]/2),hh - (box[1]/2),box[0],box[1]), 2)

pygame.draw.circle(screen,cBlock, (p[0], p[1]),ballRad, 2)

pygame.display.update()

def updateBox(d,amount):

global box, limit

box[d] += amount

limit[0] = hw - (box[0]/2) +ballRad #leftLimit

limit[1] = hw + (box[0]/2) -ballRad #rightLimit

limit[2] = hh - (box[1]/2) + ballRad #topLimit

limit[3] = (hh + (box[1]/2))-ballRad #bottomLimit

def terminate(): # close down the program

print ("Closing down please wait")

pygame.quit() # close pygame

sys.exit()

def checkForEvent(): # see if you need to quit

event = pygame.event.poll()

if event.type == pygame.QUIT :

terminate()

if event.type == pygame.KEYDOWN :

if event.key == pygame.K_ESCAPE :

terminate()

if event.key == pygame.K_DOWN : image

# expand / contract the box

updateBox(1,-2)

if event.key == pygame.K_UP :

updateBox(1,2)

if event.key == pygame.K_LEFT :

updateBox(0,-2)

if event.key == pygame.K_RIGHT :

updateBox(0,2)

if __name__ == '__main__':

main()

You can see here that not only have the functions’ names changed but the variables defining the movement and some other parameters also have changed from being separate named variables to being items in a list. This makes it easer to pass them into and out of functions. A function is restricted to returning only one item, but by packing lots of variables into a list you get to return many under the one name. Note that in this code checking for a collision involves only checking the position of the ball against predefined limits. This eliminates the need to do calculations such as adjusting for the ball radius every time you want to do a collision check. Precalculating these limits helps to speed up the overall program and gets the ball moving faster and smoother.

The sound is handled by loading in a bounce sound. Note here though that the initialisation of the sound does not use the default values but uses

pygame.mixer.init(frequency=22050, size=-16, image

channels=2, buffer=512)

This ensures that the sound is produced as soon as possible after the software command is given. Without this there is an unacceptable delay, which is sometimes called latency. You need to create a sound file in the .ogg format with a sample rate of 22.05 KHz; call it bounce.ogg and place it inside a folder called sounds. This sounds folder should be placed in the same folder as the Python code. A short blip sound will do for now. If you like, you can look inside the Python games folder at some of the sound files that are there; you can copy and rename beep.ogg, for example, here. I use the application Audacity for creating sound files. It is free, and there are versions that run on Windows machines or Macs.

A Single-Player Game

The next step produces a usable single-player game, complete with scoring. The point of a single-player game is to see how many times you can return the ball from a perfect computer player. You get three balls, and the number of returns you make from those balls is your score. If it is the highest one of the session, it is transferred to the left number, and that is the one to beat next time. There are a few steps in going from the simple bounce around a box to the single player, but space in the book restricts me from going through all the intermediate stages.

The chief change is that you no longer need to look at just a single value to detect a collision because a bat also has a limited length; you need to see if the ball is sailing over the top or underneath it. The top and bottom collisions are still the same, though. There are a few shortcuts I have taken to simplify the detection of a collision with a ball as opposed to a rectangle. Figure 5.6 shows the dimensions of the bat and ball. For the detection of a collision, you can consider just the bounding box of the circular ball and not worry about the actual geometry of the circle.

Figure 5-6: Measurements for the bat and ball.

image

Now when you consider bat-and-ball collisions you must look at both the X and Y elements of the coordinates of the bat and ball. Figure 5.7 summarises this by showing both sets of conditions that have to be met. This is complicated by the fact that whereas the X coordinate of the bat is fixed, the Y coordinate is going to change with input from the player. Note here that unlike conventional coordinates, Pygame has the Y-axis numbers increase as you move down the screen.

Figure 5-7: The collision geometry of the bat and ball.

image

The overall structure of the game also needs to be defined. Previously there was just a ball bouncing around the screen. Now you have to have more code to define the various stages of the game. Each game consists of three balls, and a rally is the time one ball spends in play. The score advances each time you hit the ball, and a ball is lost when it collides against the far-right bounding box. After each ball is lost, the automatic left-hand player serves a new ball at a randomly chosen speed and direction. This structure needs to be imposed on the simple game mechanics. Let’s see how all this comes together in Listing 5.3.

Listing 5.3 The Single-Player Ping Game

#!/usr/bin/env python

"""

Ping - Tennis game one player

with score

For the Raspberry Pi

"""

import time # for delays

import os, pygame, sys

import random

pygame.init() # initialise graphics interface

pygame.mixer.quit()

pygame.mixer.init(frequency=22050, size=-16, image

channels=2, buffer=512)

bounceSound = pygame.mixer.Sound("sounds/bounce.ogg")

os.environ['SDL_VIDEO_WINDOW_POS'] = 'center'

pygame.display.set_caption("Ping 1 player")

screenWidth = 500

screenHeight =300

screen = pygame.display.set_mode([screenWidth, image

screenHeight],0,32)

background = pygame.Surface((screenWidth,screenHeight))

textSize = 36

scoreSurface = pygame.Surface((textSize,textSize))

font = pygame.font.Font(None, textSize)

pygame.event.set_allowed(None)

pygame.event.set_allowed([pygame.KEYDOWN,pygame.QUIT])

# define the colours to use for the user interface

cBackground =(255,255,255)

cBall = (0,0,0)

background.fill(cBackground) # make background colour

cText = (0,0,0)

box = [screenWidth-10,screenHeight-10]

deltaChoice = [ [15,1], [14,1], [13,1], [12,1], image

[11,1], [10,1], [15,2], [14,2], [13,2], [12,2], [11,2], [10,2] ]

maxDelta = 11

delta = deltaChoice[random.randint(0,maxDelta)]

hw = screenWidth / 2

hh = screenHeight /2

ballPosition = [-hw,hh] # position of the ball off-screen

batMargin = 30 # how far in from the wall is the bat

batHeight = 24

batThick = 6

batInc = 20 # bat / key movement

batX = [batMargin, screenWidth - batMargin]

batY = [hh, hh] # initial bat position

limit = [0, 0, 0, 0, 0, 0] #wall limits & bat limits

ballRad = 8 # size of the ball

rally = True

pause = True

score = 0

best = 0 # high score

balls = 3 # number of balls in a turn

ballsLeft = balls

def main():

global ballPosition, rally, balls, pause, score, best

updateBox(0,0) # set up wall limits

updateScore()

screen.blit(background,[0,0])

while True :

ballsLeft = balls

if score > best:

best = score

score = 0

updateScore()

while ballsLeft > 0:

ballPosition = waitForServe(ballPosition)

while rally :

checkForEvent()

time.sleep(0.05)

drawScreen(ballPosition)

ballPosition = moveBall(ballPosition)

ballsLeft -= 1

print "press space for",balls,"more balls"

pause = True

while pause :

checkForEvent()

def waitForServe(p) :

global batY, rally, delta

computerBatDelta = 2

serveTime = time.time() + 2.0 #automatically serve again

while time.time() < serveTime :

checkForEvent()

drawScreen(p)

batY[0] += computerBatDelta image

# move bat up and down when waiting

if batY[0] > limit[3] or batY[0] < limit[2]:

computerBatDelta = -computerBatDelta

p[0] = batX[0]

p[1] = batY[0]

delta = deltaChoice[random.randint(0,maxDelta)]

rally = True

return p

def moveBall(p):

global delta, batY, rally, score, batThick

p[0] += delta[0]

p[1] += delta[1]

# now test to any interaction

if p[1] <= limit[2] : # test top

bounceSound.play()

delta[1] = - delta[1]

p[1] = limit[2]

elif p[1] >= limit[3] : # test bottom

bounceSound.play()

delta[1] = - delta[1]

p[1] = limit[3]

elif p[0] <= limit[0] : # test missed ball player 1

p[0] = limit[0]

rally = False

print " missed ball"

elif p[0] >= limit[1] : # test missed ball player 2

p[0] = limit[1]

rally = False

print " missed ball"

# now test left bat limit

elif p[0] <= limit[4] and p[1] >= batY[0] - ballRad image

and p[1] <= batY[0] + ballRad + batHeight:

bounceSound.play()

p[0] = limit[4]

delta[0] = random.randint(5,15)

if random.randint(1,4) > 2 : image

# random change in y direction

delta[1] = 16 - delta[0]

else :

delta[1] = -(16 - delta[0])

# Test right bat collision

elif p[0] >= limit[5] and p[1] >= batY[1] - ballRad image

and p[1] <= batY[1] + ballRad + batHeight:

bounceSound.play()

delta[0] = - delta[0]

p[0] = limit[5]

score+= 1

updateScore()

batY[0] = p[1] - ballRad # make auto opponent follow bat

#batY[1] = p[1]- ballRad # temporary test for auto player

return p

def updateScore():

global score, best, scoreRect, scoreSurface

scoreSurface = font.render(str(best)+" : "+str(score),image

True, cText, cBackground)

scoreRect = scoreSurface.get_rect()

scoreRect.centerx = hw

scoreRect.centery = 24

def drawScreen(p) : # draw to the screen

global rally

screen.blit(background,[0,0]) # set background colour

pygame.draw.rect(screen,(255,0,0), (hw - (box[0]/2),image

hh - (box[1]/2),box[0],box[1]), 4)

pygame.draw.line(screen,(0,255,0), (batX[0], batY[0]),image

(batX[0], batY[0]+batHeight),batThick)

pygame.draw.line(screen,(0,255,0), (batX[1], batY[1]),image

(batX[1], batY[1]+batHeight),batThick)

screen.blit(scoreSurface, scoreRect)

if rally :

pygame.draw.circle(screen,cBall, (p[0], p[1]),image

ballRad, 2)

pygame.display.update()

def updateBox(d,amount):

global box, limit

box[d] += amount

limit[0] = hw - (box[0]/2) +ballRad #leftLimit

limit[1] = hw + (box[0]/2) -ballRad #rightLimit

limit[2] = hh - (box[1]/2) + ballRad #topLimit

limit[3] = (hh + (box[1]/2))-ballRad #bottomLimit

limit[4] = batX[0] + ballRad + batThick/2 image

#x Limit ball approaching from the right

limit[5] = batX[1] - ballRad - batThick/2 image

#x Limit ball approaching from the left

def terminate(): # close down the program

print ("Closing down please wait")

pygame.quit() # close pygame

sys.exit()

def checkForEvent(): # see if you need to quit

global batY, rally, pause

event = pygame.event.poll()

if event.type == pygame.QUIT :

terminate()

if event.type == pygame.KEYDOWN :

if event.key == pygame.K_ESCAPE :

terminate()

if event.key == pygame.K_DOWN : image

# expand / contract the box

updateBox(1,-2)

if event.key == pygame.K_UP :

updateBox(1,2)

if event.key == pygame.K_LEFT :

updateBox(0,-2)

if event.key == pygame.K_RIGHT :

updateBox(0,2)

if event.key == pygame.K_s :

rally = True

if event.key == pygame.K_SPACE :

pause = False

if event.key == pygame.K_PAGEDOWN :

if batY[1] < screenHeight - batInc :

batY[1] += batInc

if event.key == pygame.K_PAGEUP :

if batY[1] > batInc :

batY[1] -= batInc

if __name__ == '__main__':

main()

There are a lot more changes this time, but hopefully this code is recognisable in structure from Listing 5.2. The various phases of the program are defined by the Boolean variables rally and pause, which can be changed from the keyboard, in the checkForEvent() function, along with the Y position of the bat. I chose to use the Page Up and Page Down keys for the movement of the bat as they are on the far right of my keyboard.

The updateBox() function has two more limits added to it, that of the ball approaching the bat from the left or right. The drawScreen() function now draws the two bats and only draws the ball if the rally variable is true. It also draws a surface bitmap containing the score. updateScore()is a new function that, as its name implies, changes the surface containing the score and positions the score rectangle. This works just like you saw in Chapter 4, “Here’s the News”, with the teleprompter.

The moveBall() function has grown some. The list of if statements is now replaced by a string of elif clauses based on the original if statement. This means that only one of the sections of code will be executed in each pass. This is because the positional condition for a ball to be off the right side of the screen would also trigger the right ball bat collision, so you must carefully test for collisions in the correct order to avoid a misidentification of what is colliding with what. The function takes the first case that is true from the following list:

• A collision with the top of the box

• A collision with the bottom of the box

• A collision with the left edge of the box (missed ball, computer player)

• A collision with the right edge of the box (missed ball, human player)

• A collision with the left bat

• A collision with the right bat

In the event of any of those conditions being met, the code will take the appropriate action. In the case of the top or bottom walls, the ball’s direction will change appropriately as you have seen before. If the active player hits the ball, the score will be incremented along with an elastic collision. If the computer’s bat hits it, a new value of the two delta variables will be chosen from the list defined at the start of the program. All collisions also cause a bounce sound to be played. Basically, this function controls the action on the screen. The last line keeps the computer’s bat in line with the ball. You can also make the player’s bat follow the ball for testing, but then the game element disappears altogether.

The waitForServe() function is used to restart the rally when the ball has been missed. Here the computer’s bat moves up and down the screen for two seconds before being served with a new random set of delta values. In the two-player game described in the next section of this chapter, this will be expanded.

That leaves us with main() as the only function you have not looked at. As usual, this function orchestrates the whole program. After a bit of initialisation it enters a while True endless loop, which initialises the number of balls and score for a game, before entering a while loop that basically counts down the number of balls in a game. Finally, the third while loop controls the rally and keeps the screen action going until a ball is missed. When it is moveBall() sets the rally variable to False, and that loop terminates. When all the balls have been played the final whileloop in this function just checks for any events, one of which could be the spacebar, which sets the pause variable to False and allows another game to be played.

A Two-Player Game

It doesn’t take much to turn this into a two-player game, but there is a subtle change in what the object of the game is, and that has a few ramifications in the code. In the one-player game the point was to simply return the ball to the perfect opponent. So there was no need to do anything about altering the flight of the ball when the player hit it back. In a two-player game, however, you not only have to return the ball, but you also have the opportunity of changing the flight of the ball to make it more difficult for your opponent to return. In a real game of Ping-Pong, this is done by adding top spin to the ball. In your Ping game you can simulate the same sort of effect by having more delta movement in the Y direction, the further from the center of the bat the collision occurs. This involves a further calculation once the collision has been detected. The other thing that needs changing is the method of serving. The serve goes to the player who has just lost the point, and there needs to be a bit of an element of surprise for the opposing player. Therefore, the serve can be played early by pressing a key, but if the player waits too long, the serve will happen automatically. All these changes can be seen in Listing 5.4, the two-player game.

Listing 5.4 The Two-Player Game of Ping

#!/usr/bin/env python

"""

Ping - Tennis game two player

with score

For the Raspberry Pi

"""

import time # for delays

import os, pygame, sys

import random

pygame.init() # initialise graphics interface

pygame.mixer.quit()

pygame.mixer.init(frequency=22050, size=-16, image

channels=2, buffer=512)

bounceSound = pygame.mixer.Sound("sounds/bounce.ogg")

outSound = pygame.mixer.Sound("sounds/out.ogg")

p0hitSound = pygame.mixer.Sound("sounds/hit0.ogg")

p1hitSound = pygame.mixer.Sound("sounds/hit1.ogg")

os.environ['SDL_VIDEO_WINDOW_POS'] = 'center'

pygame.display.set_caption("Ping 2 players")

screenWidth = 500

screenHeight =300

screen = pygame.display.set_mode([screenWidth,image

screenHeight],0,32)

background = pygame.Surface((screenWidth,screenHeight))

textSize = 36

scoreSurface = pygame.Surface((textSize,textSize))

font = pygame.font.Font(None, textSize)

pygame.event.set_allowed(None)

pygame.event.set_allowed([pygame.KEYDOWN,pygame.QUIT])

# define the colours to use for the user interface

cBackground =(255,255,255)

cBall = (0,0,0)

background.fill(cBackground) # make background colour

cText = (0,0,0)

box = [screenWidth-10,screenHeight-10]

deltaChoice = [ [15,1], [14,1], [13,1], [12,1], [11,1],image

[10,1], [15,2], [14,2], [13,2], [12,2], [11,2], [10,2] ]

maxDelta = 11

delta = deltaChoice[random.randint(0,maxDelta)]

hw = screenWidth / 2

hh = screenHeight /2

ballPosition = [-hw,hh] # position of the ball off-screen

batMargin = 30 # how far in from the wall is the bat

batHeight = 24

batThick = 6

batInc = 20 # bat / key movement

batX = [batMargin, screenWidth - batMargin]

batY = [hh, hh] # initial bat position

limit = [0, 0, 0, 0, 0, 0] #wall limits & bat limits

ballRad = 8 # size of the ball

rally = True

pause = True

server = 0 # player to serve

serve =[False,False]

score = [0,0] # players score

balls = 5 # number of balls in a turn

ballsLeft = balls

batMiddle = (ballRad - (batHeight + ballRad))/2

def main():

global ballPosition, rally, balls, pause, score, server

updateBox(0,0) # set up wall limits

updateScore()

screen.blit(background,[0,0])

while True :

ballsLeft = balls

score = [0,0]

updateScore()

while ballsLeft > 0:

ballPosition = waitForServe(ballPosition,server)

while rally :

checkForEvent()

time.sleep(0.05)

drawScreen(ballPosition)

ballPosition = moveBall(ballPosition)

ballsLeft -= 1

print "press space for",balls,"more balls"

pause = True

while pause :

checkForEvent()

def waitForServe(p,player) :

global batY, rally, delta, serve

computerBatDelta = 2

serve[player] = False

serveTime = time.time() + 4.0 #automatically serve again

while time.time() < serveTime and serve[player] == False:

checkForEvent()

drawScreen(p)

batY[player] += computerBatDelta image

# move bat up and down when waiting

if batY[player] > limit[3] or batY[player] < image

limit[2]:

computerBatDelta = -computerBatDelta

p[0] = batX[player]

p[1] = batY[player]

delta = deltaChoice[random.randint(0,maxDelta)]

if player == 1 :

delta[0] = -delta[0]

p1hitSound.play()

else:

p0hitSound.play()

rally = True

return p

def moveBall(p):

global delta, batY, rally, score, batThick, server

p[0] += delta[0]

p[1] += delta[1]

# now test to any interaction

if p[1] <= limit[2] : # test top

bounceSound.play()

delta[1] = - delta[1]

p[1] = limit[2]

elif p[1] >= limit[3] : # test bottom

bounceSound.play()

delta[1] = - delta[1]

p[1] = limit[3]

elif p[0] <= limit[0] : # test missed ball left player

outSound.play()

rally = False

score[1] += 1

server = 0

p[0] = hw

updateScore()

elif p[0] >= limit[1] : # test missed ball right player

outSound.play()

rally = False

score[0] += 1

server = 1

p[0] = hw

updateScore()

# Test left bat collision

elif p[0] < limit[4] and p[1] >= batY[0] - ballRad image

and p[1] <= batY[0] + ballRad + batHeight:

batBounce(p[1],batY[0],0)

p[0] = limit[4]

# Test right bat collision

elif p[0] >= limit[5] and p[1] >= batY[1] - ballRad image

and p[1] <= batY[1] + ballRad + batHeight:

batBounce(p[1],batY[1],1)

p[0] = limit[5]

return p

def batBounce(ball, bat, player) :

global delta

point = bat - ball

delta[1] = int(-14.0 * ((point * 0.05) + 0.6))

delta[0] = 16 - abs(delta[1])

if player == 1 :

delta[0] = -delta[0]

p1hitSound.play()

else:

p0hitSound.play()

def updateScore():

global scoreRect, scoreSurface

scoreSurface = font.render(str(score[0])+" : "image

+str(score[1]), True, cText, cBackground)

scoreRect = scoreSurface.get_rect()

scoreRect.centerx = hw

scoreRect.centery = 24

drawScreen(ballPosition)

def drawScreen(p) : # draw to the screen

global rally

screen.blit(background,[0,0]) # set background colour

pygame.draw.rect(screen,(255,0,0), (hw - (box[0]/2),image

hh - (box[1]/2),box[0],box[1]), 4)

pygame.draw.line(screen,(0,255,0), (batX[0], batY[0]),image

(batX[0], batY[0]+batHeight),batThick)

pygame.draw.line(screen,(0,255,0), (batX[1], batY[1]),image

(batX[1], batY[1]+batHeight),batThick)

screen.blit(scoreSurface, scoreRect)

if rally :

pygame.draw.circle(screen,cBall, (p[0], p[1]),image

ballRad, 2)

pygame.display.update()

def updateBox(d,amount):

global box, limit

box[d] += amount

limit[0] = hw - (box[0]/2) + ballRad #leftLimit

limit[1] = hw + (box[0]/2) -ballRad #rightLimit

limit[2] = hh - (box[1]/2) + ballRad #topLimit

limit[3] = (hh + (box[1]/2))-ballRad #bottomLimit

limit[4] = batX[0] + ballRad + batThick/2image

#x Limit ball approaching from the right

limit[5] = batX[1] - ballRad - batThick/2image

#x Limit ball approaching from the left

def terminate(): # close down the program

print ("Closing down please wait")

pygame.quit() # close pygame

sys.exit()

def checkForEvent(): # see if you need to quit

global batY, rally, pause, serve

event = pygame.event.poll()

if event.type == pygame.QUIT :

terminate()

if event.type == pygame.KEYDOWN :

if event.key == pygame.K_ESCAPE :

terminate()

if event.key == pygame.K_DOWN : image

# expand / contract the box

updateBox(1,-2)

if event.key == pygame.K_UP :

updateBox(1,2)

if event.key == pygame.K_LEFT :

updateBox(0,-2)

if event.key == pygame.K_RIGHT :

updateBox(0,2)

if event.key == pygame.K_s :

rally = True

if event.key == pygame.K_SPACE :

pause = False

if event.key == pygame.K_q :

serve[0] = True

if event.key == pygame.K_HOME :

serve[1] = True

if event.key == pygame.K_PAGEDOWN :

if batY[1] < screenHeight - batInc :

batY[1] += batInc

if event.key == pygame.K_PAGEUP :

if batY[1] > batInc :

batY[1] -= batInc

if event.key == pygame.K_z :

if batY[0] < screenHeight - batInc :

batY[0] += batInc

if event.key == pygame.K_a :

if batY[0] > batInc :

batY[0] -= batInc

if __name__ == '__main__':

main()

Here more keyboard keys come into play. The Home key has been added for the right player’s serve key, along with the bat movement keys of Page Up and Page Down. The A, Q and Z keys perform the same functions for the left player.

The updateBox() and drawScreen() functions are unchanged, but the updateScore() function has been altered to accommodate the score of both players. The moveBall() function has been enhanced to include top and bottom limits for the left player’s bat, and for both players a new function batBounce() is called when a collision is detected. More on this shortly.

The waitForServe() function now includes code to allow either player to serve the ball, and there is a longer time before an automatic serve, along with a separate serve sound for each player. There are also a few changes to global variables, adding one to indicate who is serving.

So back to the batBounce() function. This performs two functions – the first to determine the ball’s return velocity vector and the second to play a sound dependent on the player striking the ball. As the code is written this is the same as the serve sound, but it could be changed.

The major new feature in this function is in determining how far along the bat the collision occurred. This basically is a floating-point number between -1 and +1, with zero being returned if it is plumb center. After you have gotten this fraction it needs to be multiplied by the number that corresponds to the maximum Y velocity you want if the ball just grazes the top or bottom of the bat. In the code I have used a value of 14. Then to keep the overall speed of the ball constant, the X velocity is the maximum velocity you want it to be if there is a dead center hit minus any Y component of the speed. This is all done in the two lines

delta[1] = int(-14.0 * ((point * 0.05) + 0.6))

delta[0] = 16 - abs(delta[1])

Note how the floating-point calculation is converted to an integer before assigning it to the delta global variable. This keeps the delta values as integers, as operations on integers are much faster to perform for the computer.

The final bit of fun is the sound effects. What I did was scour the Internet looking for tennis sounds and found some interesting examples of famous tennis players’ grunts as they hit the ball. Using Audacity, I clipped out the short hit/grunt noise and saved it as an .ogg format file. I found a line judge’s “out” call, along with some real bounce sounds. These sounds enhance the playing of the game tremendously, especially because there is also some crowd noise. However, for that retro 70s sound, you can’t beat simple tonal bleeps; the choice is yours.

Over to You

Well, that is the end of my bit, but it is not the end of this project. There are many changes, enhancements and improvements you can make. This applies to both the single- and two-player version of the game. The simplest involves changing the colours: You might want green grass, for example, or filled coloured balls or bats. You could even draw a picture with a painting package or photo-editing package to act as the background, or you might want to include a net or white lines on the court. You could have different sounds for a serve and a return, which is easy because they are called up in different places in the code. You can upgrade the sounds in the single-player game to match what you did in the two-player game.

You might want to change things like the number of balls in a game or even the scoring to make it more like a real game of tennis, with a player winning a game only when he or she is two points ahead. You can even use the real tennis terms of advantage and deuce in the score box.

I have deliberately left in the code that alters the bounding box from the cursor keys. How about a variation of the game that allows the serving player to alter the size of the court as the serve takes place? Or how about a court that automatically shrinks as the rally gets longer? Basically, the whole game is one of anticipating where the ball will end up, and by changing the court size only slightly, you can make it more difficult to judge as you don’t have any previous experience to go on.

It is only a small step from this game to making a “knock the bricks out of a wall” game with all the variants that can bring. It’s within your grasp to do this now if you understand the basics of what was done here.