8. Case Study: Catch¶
8.1. Getting started¶
In our first case study we will build a small video game using the facilities
in the graphics
package. The game will shoot a ball across a window from left to
right and you will manipulate a mitt at the right side of the window to catch
it.
8.2. Using while
to move a ball¶
while
statements can be used with graphics
to add motion to a program. The
following program moves a black ball across an 800 x 600 pixel graphics window.
Add this to a file named pitch.py
:
from graphics import *
from time import sleep
def main():
window = GraphWin("Catch", 800, 600)
window.setBackground("yellow")
ball_x = 10
ball_y = 300
center = Point(ball_x, ball_y)
ball = Circle(center, 10)
ball.setFill("black")
ball.draw(window)
dx = 4
dy = 1
while ball_x < 810:
ball.move(dx, dy)
ball_x += dx
ball_y += dy
sleep(1/120)
window.getMouse()
window.close()
main()
As the ball moves across the screen, you will see a graphics window that looks like this:
Trace the first few iterations of this program to be sure you see what is
happening to the variables x
and y
.
Some new things to learn about graphics
from this example:
GraphWin
takes arguments for the title, width and height of the graphics window and it returns a window that you can draw on.window = GraphWin
stores the graphics window in the variable namedwindow
window.setBackground
calls a method of thewindow
object calledsetBackground
with a string representing the name of a color as an argument. The result is that the background of the window is painted that color.center = Point
stores a point in a variable namedcenter
Point
takes two arguments: thex
andy
values of the position of the point on the graphics window.ball = Circle
stores the circle in a variable namedball
so that it can be referenced later.Circle
takes two arguments: the first argument is aPoint
and the second argument is the radius of the circle.ball.setFill
calls a method of theball
object that fills theball
with whatever color is passed as an argument.ball.draw
calls a method of theball
object that draws the ball into the graphics window provided as an argument.The
ball.move
method moves theball
a distancedx
in thex
direction anddy
in they
direction.The
sleep
function is used to delay the action in a graphics program. The argument tosleep
is the number of seconds to wait. Here, we wait 1/120 of a second: a small amount, but enough to make the ball move smoothly across the screen. Try the program without thesleep
function and see how fast the ball moves across the screen.window.getMouse()
waits for the user to click the mouse until moving until the next line. Without this line, the graphics window will close immediately after finishing the loop making it hard to see what just happened.window.close()
closes the graphics window
8.3. Varying the pitches¶
To make our game more interesting, we want to be able to vary the speed and
direction of the ball. We can use the randint
function in the random
library to choose a random number from a range between low
and high
. To see how this
works, run the following program:
from random import randint
def main():
i = 0
while i < 10:
print(randint(-5, 5))
i += 1
main()
Each time the function is called a more or less random integer is chosen between -5 and 5. When we ran this program we got:
-2
-1
-4
1
-2
3
-5
-3
4
-5
You will probably get a different sequence of numbers.
Let’s use randint
to vary the direction of the ball. Replace the
line in pitch.py
that assigns 1
to y
:
dy = 1
with an assignment to a random number between -4 and 4. Be sure to add the line from random import randint
to the top of your program.
dy = randint(-4, 4)
8.4. Making the ball bounce¶
Running this new version of the program, you will notice that ball frequently
goes off either the top or bottom edges of the screen before it completes its
journey. To prevent this, let’s make the ball bounce off the edges by changing
the sign of dy
and sending the ball back in the opposite verticle
direction.
Add the following as the first line of the body of the while loop in
pitch.py
:
if ball_y >= 590 or ball_y <= 10:
dy *= -1
Run the program several times to see how it behaves.
8.5. Responding to the keyboard¶
The following program creates a circle (which will become our mitt in the game of catch) which responds to keyboard input. Pressing the j
or k
keys moves the mitt up and down, respectively. Add this to a file named mitt.py
:
from graphics import *
def main():
window = GraphWin("Catch", 800, 600)
window.setBackground("yellow")
mitt_x = 780
mitt_y = 300
mitt = Circle(Point(mitt_x, mitt_y), 20)
mitt.draw(window)
pressed_escape = False # a flag variable to indicate that Escape was pressed
while not pressed_escape:
key_pressed = window.checkKey()
if key_pressed == 'k' and mitt_y <= 580:
dy += 5
elif key_pressed == 'j' and mitt_y >= 20:
dy -= 5
elif key_pressed == 'Escape':
pressed_escape = True
else:
dy = 0
mitt.move(0, dy)
mitt_y += dy
window.close()
main()
Run mitt.py
, pressing j
and k
to move up and down the screen.
8.6. Checking for collisions¶
The following program moves two balls toward each other from opposite sides of the screen. When they collide, both balls disappear and the program ends and waits for the user to click the mouse in the window:
from graphics import *
from time import *
def distance(x1, y1, x2, y2):
return ((x2 - x1)**2 + (y2 - y1)**2)**0.5
def main():
window = GraphWin("Catch", 800, 600)
window.setBackground("yellow")
ball1_x = 10
ball1_y = 300
ball1 = Circle(Point(ball1_x, ball1_y), 10)
ball1.setFill("black")
ball1_dx = 4
ball1.draw(window)
ball2_x = 790
ball2_y = 300
ball2 = Circle(Point(ball2_x, ball2_y), 10)
ball2.setFill("black")
ball2_dx = -4
ball2.draw(window)
collision = False # have the balls collided?
while ball1_x < 810 and not collision:
ball1.move(ball1_dx, 0)
ball2.move(ball2_dx, 0)
ball1_x += ball1_dx
ball2_x += ball2_dx
if distance(ball1_x, ball1_y, ball2_x, ball2_y) <= 20:
ball1.undraw()
ball2.undraw()
collision = True
else:
sleep(1/120)
window.getMouse()
window.close()
main()
Put this program in a file named collide.py
and run it.
8.7. Putting the pieces together¶
In order to combine the moving ball, moving mitt, and collision detection, we
need a single while
loop that does each of these things in turn:
from graphics import *
from random import randint
from time import *
def distance(x1, y1, x2, y2):
return ((x2 - x1)**2 + (y2 - y1)**2)**0.5
def main():
window = GraphWin("Catch", 800, 600)
window.setBackground("yellow")
ball_x = 10
ball_y = 300
ball = Circle(Point(ball_x, ball_y), 10)
ball.setFill("black")
dx = 4
dy = randint(-4, 4)
ball.draw(window)
mitt_x = 790
mitt_y = 300
mitt = Circle(Point(mitt_x, mitt_y), 20)
mitt_dy = 0
mitt.draw(window)
# the game is over if the user presses Escape, misses the ball,
# or catches the ball.
game_over = False
while not game_over:
#move the ball
if ball_y >= 590 or ball_y < 10:
dy *= -1
ball_x += dx
ball_y += dy
ball.move(dx, dy)
if ball_x > 810:
game_over = True # the ball went past the paddle
#check the mitt
key_pressed = window.checkKey()
if key_pressed == 'k' and mitt_y <= 580:
mitt_dy += 5
elif key_pressed == 'j' and mitt_y >= 20:
mitt_dy -= 5
elif key_pressed == 'Escape':
game_over = True # the user pressed Escape
else:
mitt_dy = 0
if not game_over:
mitt_y += mitt_dy
mitt.move(0, mitt_dy)
#check if we caught the ball
if distance(ball_x, ball_y, mitt_x, mitt_y) <= 30:
ball.undraw()
game_over = True
else:
sleep(1/120)
window.getMouse()
window.close()
main()
Put this program in a file named catch.py
and run it several times. Be
sure to catch the ball on some runs and miss it on others.
8.8. Displaying text¶
This program displays scores for both a player and the computer on the graphics screen. It generates a random number of 0 or 1 (like flipping a coin) and adds a point to the player if the value is 1 and to the computer if it is not. It then updates the display on the screen.
from graphics import *
from random import randint
from time import sleep
def main():
window = GraphWin("Catch", 800, 600)
window.setBackground("yellow")
player_score = 0
comp_score = 0
player = Text(Point(110, 570), "Player: %d Points" % player_score)
player.setSize(24)
player.draw(window)
computer = Text(Point(640, 570), "Computer: %d Points" % comp_score)
computer.setSize(24)
computer.draw(window)
while player_score < 5 and comp_score < 5:
sleep(1)
winner = randint(0, 1)
if winner == 1:
player_score += 1
player.setText("Player: %d Points" % (player_score))
else:
comp_score += 1
computer.setText("Computer: %d Points" % (comp_score))
if player_score == 5:
result_text = "Player Wins!"
else:
result_text = "Computer Wins!"
result = Text(Point(340, 290), result_text)
result.setSize(32)
result.draw(window)
window.getMouse()
window.close()
main()
Put this program in a file named scores.py
and run it.
We can now modify catch.py
to diplay the winner. Immediately after the if
ball_x > 810:
conditional, add the following:
text = Text(Point(340, 290), "Computer Wins!")
text.setSize(32)
text.draw(window)
It is left as an excercise to display when the player wins.
8.9. Abstraction¶
Our program is getting a bit complex. To make matters worse, we are about to increase its complexity. The next stage of development requires a nested loop. The outer loop will handle repeating rounds of play until either the player or the computer reaches a winning score. The inner loop will be the one we already have, which plays a single round, moving the ball and mitt, and determining if a catch or a miss has occured.
Research suggests there are clear limits to our ability to process cognitive tasks (see George A. Miller’s The Magical Number Seven, Plus or Minus Two: Some Limits on our Capacity for Processing Information). The more complex a program becomes, the more difficult it is for even an experienced programmer to develop and maintain.
To handle increasing complexity, we can wrap groups of related statements in functions, using abstraction to hide program details. This allows us to mentally treat a group of programming statements as a single concept, freeing up mental bandwidth for further tasks. The ability to use abstraction is one of the most powerful ideas in computer programming.
Here is a completed version of catch.py
:
from graphics import *
from random import randint
from time import *
def distance(x1, y1, x2, y2):
return ((x2 - x1)**2 + (y2 - y1)**2)**0.5
def play_round(window):
"""
Play one round of catch. Returns "PLAYER" if the player wins the round,
"COMPUTER" if the computer wins the round, and "QUIT" if the player
pressed Escape and wants to quit.
"""
ball_x = 10
ball_y = randint(20, 280)
ball = Circle(Point(ball_x, ball_y), 10)
ball.setFill("black")
dx = 4
dy = randint(-5, 5)
ball.draw(window)
mitt_x = 790
mitt_y = randint(20, 280)
mitt = Circle(Point(mitt_x, mitt_y), 20)
mitt_dy = 0
mitt.draw(window)
game_over = False
result = ""
while not game_over:
#move the ball
if ball_y >= 590 or ball_y < 10:
dy *= -1
ball_x += dx
ball_y += dy
ball.move(dx, dy)
if ball_x > 810:
ball.undraw()
mitt.undraw()
result = "COMPUTER"
game_over = True
#check the mitt
key_pressed = window.checkKey()
if key_pressed == 'k' and mitt_y <= 580:
mitt_dy += 5
elif key_pressed == 'j' and mitt_y >= 20:
mitt_dy -= 5
elif key_pressed == 'Escape':
result = "QUIT"
game_over = True
else:
mitt_dy = 0
if not game_over:
mitt_y += mitt_dy
mitt.move(0, mitt_dy)
#check if we caught the ball
if distance(ball_x, ball_y, mitt_x, mitt_y) <= 30:
ball.undraw()
mitt.undraw()
game_over = True
result = "PLAYER"
else:
sleep(1/120)
return result
def play_game(window):
"""
Play multiple rounds of the game until someone wins 5 games. Returns "PLAYER"
if the player wins the game, "COMPUTER" if the computer wins the game, and
"QUIT" if the player pressed Escape and wants to quit.
"""
player_score = 0
comp_score = 0
game_result = "" # keep track of the final 5 game winner
while game_result != "":
pmsg = Text(Point(110, 570), "Player: %d Points" % player_score)
pmsg.setSize(24)
pmsg.draw(window)
cmsg = Text(Point(640, 570), "Computer: %d Points" % comp_score)
cmsg.setSize(24)
cmsg.draw(window)
sleep(3)
pmsg.undraw()
cmsg.undraw()
round_result = play_round(window)
if round_result == "PLAYER":
player_score += 1
if player_score == 5:
game_result = "PLAYER"
elif round_result == "COMPUTER":
comp_score += 1
if comp_score == 5:
game_result = "COMPUTER"
else:
game_result = "QUIT"
return game_result
def main():
window = GraphWin("Catch", 800, 600)
window.setBackground("yellow")
result = play_game(window)
if result == "PLAYER":
result_text = "Player Wins!"
else:
result_text = "Computer Wins!"
result = Text(Point(340, 290), result_text)
result.setSize(32)
result.draw(window)
window.getMouse()
window.close()
main()
Some new things to learn from this example:
Following good organizational practices makes programs easier to read. Use the following organization in your programs:
imports
function definitions
main body of the program
We took the version of the program developed in section 8.8 and wrapped it in a function named
play_round()
.A new function,
play_game()
, creates variables forplayer_score
andcomp_score
. Using awhile
loop, it repeatedly callsplay_round
, checking the result of each call and updating the score appropriately. Finally, when either the player or computer reach 5 points,play_game
returns the winner to the main body of the program, which then displays the winner and then quits.There are two variables named
result
—one in theplay_game
function and one in the main body of the program. While they have the same name, they are in different namespaces, and bear no relation to each other. Each function creates its own namespace, and names defined within the body of the function are not visible to code outside the function body. Namespaces will be discussed in greater detail in the next chapter.Throughout this chapter we have used hardcoded values for convenience and clarity. In general you should avoid hardcoding in your programs. For graphics programs this generally means defining values in terms of the width or height of the graphics window. For instance, we might define a variable
ball_radius = width/80
and then use this variable when we create the ball. Then we could replace a value like590
with the more general expressionwindow_height - ball_radius
. If all the relevant quantities are defined in terms of the window’s width and height, we can change just these two values and see everything scale nicely.
8.10. The break
statement¶
The break statement is used to immediately leave the body of a loop. The following program implements a simple guessing game:
from random import randint def main(): number = randint(1, 1000) guesses = 1 guess = int(input("Guess the number between 1 and 1000: ")) while guess != number: if guess > number: print("Too high!") else: print("Too low!") guess = int(input("Guess the number between 1 and 1000: ")) guesses += 1 print("\n\nCongratulations, you got it in %d guesses!\n\n" % (guesses)) main()We can rewrite this program to eliminate the duplication of the
input
statement by using a “sentinel” or “flag” variable to indicate when the user has guessed the number.from random import randint def main(): number = randint(1, 1000) guesses = 0 guessed = False # the flag variable is False until we guess correctly while not guessed: guess = int(input("Guess the number between 1 and 1000: ")) guesses += 1 if guess > number: print("Too high!") elif guess < number: print("Too low!") else: print("\n\nCongratulations, you got it in %d guesses!\n\n" % (guesses)) guessed = True # set the flag variable to True main()This program makes use of the mathematical law of trichotomy (given real numbers a and b, a > b, a < b, or a == b). While both versions of the program are 15 lines long, it could be argued that the logic in the second version is clearer.
Put this program in a file named
guess.py
.
8.11. Glossary¶
- abstraction
Generalization by reducing the information content of a concept. Functions in Python can be used to group a number of program statements with a single name, abstracting out the details and making the program easier to understand.
- constant
A numerical value that does not change during the execution of a program. It is conventional to use names with all uppercase letters to repesent constants, though Python programs rely on the discipline of the programmers to enforce this, since there is no language mechanism to support true constants in Python.
- nested loop
A loop inside the body of another loop.
- random
Having no specific pattern. Unpredictable. Computers are designed to be predicatable, and it is not possible to get a truly random value from a computer. Certain functions produce sequences of values that appear as if they were random, and it is these psuedorandom values that we get from Python.
- trichotomy
Given any real numbers a and b, exactly one of the following relations holds: a < b, a > b, or a = b. Thus when you can establish that two of the relations are false, you can assume the remaining one is true.
8.12. Exercises¶
What happens when you press the key while running
mitt.py
? List the two lines from the program that produce this behavior and explain how they work.What is the name of the counter variable in
guess.py
? With a proper strategy, the maximum number of guesses required to arrive at the correct number should be 11. What is this strategy?What happens when the mitt in
mitt.py
gets to the top or bottom of the graphics window? List the lines from the program that control this behavior and explain in detail how they work.Change the value of
ball1_dx
incollide.py
to 2. How does the program behave differently? Now changeball1_dx
back to 4 and setball2_dx
to -2. Explain in detail how these changes effect the behavior of the program.Comment out (put a
#
in front of the statement) thebreak
statement incollide.py
. Do you notice any change in the behavior of the program? Now also comment out theball1.undraw()
statement. What happens now? Experiment with commenting and uncommenting the twoundraw
statements and thebreak
statement until you can describe specifically how these statements work together to produce the desired behavior in the program.Where can you add the lines
text = Text(Point(340, 290), "Player Wins!") text.setSize(32) text.draw(window)
to the version of
catch.py
in section 8.8 so that the program displays this message when the ball is caught?Trace the flow of execution in the final version of
catch.py
when you press theEscape
key during the execution ofplay_round
. What happens when you press this key? Why?List the main body of the final version of
catch.py
. Describe in detail what each line of code does. Which statement calls the function that starts the game?Identify the function responsible for displaying the ball and the mitt. What other operations are provided by this function?
Which function keeps track of the score? Is this also the function that displays the score? Justify your answer by discussing specific parts of the code which implement these operations.
8.13. Project: pong.py
¶
Pong was one of the first commercial video games. With a capital P it is a registered trademark, but pong is used to refer any of the table tennis like paddle and ball video games.
catch.py
already contains all the programming tools we need to develop our
own version of pong. Incrementally changing catch.py
into pong.py
is
the goal of this project, which you will accomplish by completing the following
series of exercises:
Copy
catch.py
topong1.py
and change the ball into a paddle by usingRectangle
instead of theCircle
. You can look at thegraphics
library reference for more information onRectangle
. Make the adjustments needed to keep the paddle on the screen.Copy
pong1.py
topong2.py
. Replace thedistance
function with a boolean functionhit(ball_x, ball_y, radius, paddle_x, paddle_y, height)
that returnsTrue
when the vertical coordinate of the ball (ball_y
) is between the bottom and top of the paddle, and the horizontal location of the ball (ball_x
) is less than or equal to the radius (radius
) away from the front of the paddle. Usehit
to determine when the ball hits the paddle, and make the ball bounce back in the opposite horizontal direction whenhit
returnsTrue
. Your completed function should pass these doctests:def hit(ball_x, ball_y, radius, paddle_x, paddle_y, height): """ >>> hit(760, 100, 10, 780, 100, 100) False >>> hit(770, 100, 10, 780, 100, 100) True >>> hit(770, 200, 10, 780, 100, 100) True >>> hit(770, 210, 10, 780, 100, 100) False """
Finally, change the scoring logic to give the player a point when the ball goes off the screen on the left.
Copy
pong2.py
topong3.py
. Add a new paddle on the left side of the screen which moves up when'a'
is pressed and down when's'
is pressed. Change the starting point for the ball to the center of the screen, (400, 300), and make it randomly move to the left or right at the start of each round.