Building Pong
Play it first → Two paddles and a ball. You are on the left, the computer is on the right, and you hit the ball back and forth until one of you misses. Every hit makes the ball a little faster, and where it hits your paddle decides which way it goes. First to 7 wins.
This is how it was built, a step at a time. Every numbered step below is a whole program: copy it into the playground, press Run, and it works. Each step is the one before it with something added, so you can build the game the way it was built. Then break something on purpose and see what changes. That teaches you more than reading does.
There is nothing clever in it. No list comprehensions, no tricks, and every kaypy function it uses is on the API page. There are no pictures at all: the paddles, the ball and the net are rectangles.
If you haven't built a game yet, start with Ghost Chase and then Bomb Dodge. Then build Paddle Ball, which is this game for one player: the ball, the bouncing and the paddle check are all built up there a step at a time, so this page can go faster. Spiral Storm comes between the two, as a break from paddles.
The one big idea: the ball carries its own speed
A game object can carry values of its own. Write ball.dx = 360
and from then on the ball has a dx, just as it has a
pos. Here dx is how fast the ball goes across, and
dy how fast it goes down, both in pixels per second. A negative
dx means it is going left; a negative dy means it is
going up.
Every frame, the ball moves by its speed. Bouncing is just making one of
the two speeds negative. Hit the top and dy turns round; hit a
paddle and dx does. Nothing has an area() or a
body(). The code works out for itself what the ball is
touching.
Step 1 — the paddles
The court, and a paddle you can move. The computer's paddle is there too, but it doesn't move yet.
from kaypy import *
kaypy(width=800, height=600, background=[20, 24, 40])
PADDLE_W = 16 # how wide a paddle is
PADDLE_H = 100 # and how tall
PADDLE_GAP = 30 # how far each paddle is from its edge of the window
PADDLE_SPEED = 480 # how fast your paddle moves, in pixels per second
# The net: a dashed line down the middle. It is only a picture.
for y in range(10, height(), 40):
add([rect(4, 20), pos(width() / 2 - 2, y), color(90, 100, 130)])
# anchor("center") means pos() is the middle of the paddle.
player = add([rect(PADDLE_W, PADDLE_H), anchor("center"),
pos(PADDLE_GAP, height() / 2), color(120, 220, 255)])
cpu = add([rect(PADDLE_W, PADDLE_H), anchor("center"),
pos(width() - PADDLE_GAP, height() / 2), color(255, 140, 120)])
@onKeyDown("up")
def move_up():
player.move(0, -PADDLE_SPEED)
keep_on_screen()
@onKeyDown("down")
def move_down():
player.move(0, PADDLE_SPEED)
keep_on_screen()
@onMouseDown
def move_to_mouse():
if mousePos().y < player.pos.y - 10:
player.move(0, -PADDLE_SPEED)
if mousePos().y > player.pos.y + 10:
player.move(0, PADDLE_SPEED)
keep_on_screen()
def keep_on_screen():
player.pos.y = clamp(player.pos.y, PADDLE_H / 2,
height() - PADDLE_H / 2)
Hold up or down and your paddle moves. Hold the mouse down somewhere and it moves towards it. On a phone, a finger does the same thing.
- The settings come first. Names in capitals, like
PADDLE_SPEED, are numbers you set once and never change while the game runs. Putting them at the top means you can find them, and change the game without hunting through it. - The net is a dashed line made of rectangles 4 pixels
wide and 20 tall.
range(10, height(), 40)counts 10, 50, 90, and so on down the window, one rectangle every 40 pixels. anchor("center")makespos()mean the middle of the paddle, not its top-left corner. Everything below is easier for it: the paddle's top isplayer.pos.y - PADDLE_H / 2and its bottom isplayer.pos.y + PADDLE_H / 2.@onKeyDown("up")runsmove_upevery frame the up arrow is held, about sixty times a second, so the paddle keeps moving until you let go.player.move(0, -PADDLE_SPEED)moves it up atPADDLE_SPEEDpixels a second: up is negative, becauseycounts down from the top of the window.@onMouseDownrunsmove_to_mouseevery frame the mouse button is held, or a finger is on the screen. More than 10 pixels above the paddle's middle and the paddle moves up; more than 10 below and it moves down. Within 10 pixels counts as there.
keep_on_screen() stops the paddle at the
edges. clamp(player.pos.y, PADDLE_H / 2, height() - PADDLE_H /
2) gives back player.pos.y, unless it is less than 50
(then it gives 50) or more than 550 (then it gives 550). The paddle is 100
tall, so 50 is half of it, and the paddle stops with its end touching the
edge.
Something to break. Take
keep_on_screen() out of move_up and hold up. There's
no error message. The paddle goes off the top of the window and keeps
going: three seconds of holding up took it to y = −1161, more
than a thousand pixels above the window. Hold down for as long to bring it
back.
Step 2 — the ball
A ball that moves by itself and bounces off the edges of the window. For now, all four edges are walls.
from kaypy import *
kaypy(width=800, height=600, background=[20, 24, 40])
PADDLE_W = 16 # how wide a paddle is
PADDLE_H = 100 # and how tall
PADDLE_GAP = 30 # how far each paddle is from its edge of the window
PADDLE_SPEED = 480 # how fast your paddle moves, in pixels per second
BALL_SIZE = 16
BALL_SPEED = 360 # how fast the ball goes, in pixels per second
# The net: a dashed line down the middle. It is only a picture.
for y in range(10, height(), 40):
add([rect(4, 20), pos(width() / 2 - 2, y), color(90, 100, 130)])
# anchor("center") means pos() is the middle of the paddle.
player = add([rect(PADDLE_W, PADDLE_H), anchor("center"),
pos(PADDLE_GAP, height() / 2), color(120, 220, 255)])
cpu = add([rect(PADDLE_W, PADDLE_H), anchor("center"),
pos(width() - PADDLE_GAP, height() / 2), color(255, 140, 120)])
# The ball carries its own speed: dx across and dy down, in pixels per
# second. A negative dx means it is going left, a negative dy up.
ball = add([rect(BALL_SIZE, BALL_SIZE), anchor("center"),
pos(width() / 2, height() / 2)])
ball.dx = BALL_SPEED
ball.dy = rand(-BALL_SPEED / 2, BALL_SPEED / 2)
@onKeyDown("up")
def move_up():
player.move(0, -PADDLE_SPEED)
keep_on_screen()
@onKeyDown("down")
def move_down():
player.move(0, PADDLE_SPEED)
keep_on_screen()
@onMouseDown
def move_to_mouse():
if mousePos().y < player.pos.y - 10:
player.move(0, -PADDLE_SPEED)
if mousePos().y > player.pos.y + 10:
player.move(0, PADDLE_SPEED)
keep_on_screen()
def keep_on_screen():
player.pos.y = clamp(player.pos.y, PADDLE_H / 2,
height() - PADDLE_H / 2)
@onUpdate
def move_ball():
ball.pos.x = ball.pos.x + ball.dx * dt()
ball.pos.y = ball.pos.y + ball.dy * dt()
# Off the top or the bottom: put it back on the edge, and turn it round.
if ball.pos.y < BALL_SIZE / 2:
ball.pos.y = BALL_SIZE / 2
ball.dy = -ball.dy
if ball.pos.y > height() - BALL_SIZE / 2:
ball.pos.y = height() - BALL_SIZE / 2
ball.dy = -ball.dy
# The sides are walls too, for now.
if ball.pos.x < BALL_SIZE / 2:
ball.pos.x = BALL_SIZE / 2
ball.dx = -ball.dx
if ball.pos.x > width() - BALL_SIZE / 2:
ball.pos.x = width() - BALL_SIZE / 2
ball.dx = -ball.dx
The ball's own values. ball.dx = BALL_SPEED
gives the ball a dx, 360, so it starts off to the right.
rand(-BALL_SPEED / 2, BALL_SPEED / 2) picks a random
dy between −180 and 180, so it goes off at a slant, up or down,
and differently every time you press Run.
Moving. @onUpdate runs
move_ball once a frame. ball.dx * dt() is how far
the ball goes across in one frame: dt() is how long the frame
took, about 1/60 of a second, so at 360 pixels a second the ball moves about 6
pixels a frame. Multiplying by dt() is what makes the ball go
the same speed on a fast computer and a slow one.
Bouncing. When the ball's middle is less than half a ball
from the top, it has reached the top. It is put back exactly on the edge,
and ball.dy = -ball.dy turns it round: −180 becomes 180, and the
ball heads down. The other three edges work the same way.
Putting it back on the edge matters more than it looks. A frame can carry the ball a few pixels past the edge. If it is turned round but left out there, and the next frame does not bring it all the way back in, it is still past the edge, so it is turned round again, the wrong way, and leaves the window. With the uneven frames a browser can have, that really happens.
Two things to break.
Take out ball.dy = -ball.dy under the top edge, and keep the
line that puts the ball back. There's no error. The first time the ball
reaches the top it stays there, sliding along the top edge from wall to wall
for ever: it is put back on the edge every frame, but never turned round.
Take out the line ball.dx = BALL_SPEED. The game stops at
once, with
AttributeError: GameObj has no attribute 'dx' — did you forget a component that provides it?
A game object only has a dx once you give it one. kaypy
guesses you forgot a component, because that is usually why. Here, though,
dx is a value of your own, and what's missing is the line that
sets it.
Step 3 — hitting it back
The ball bounces off the paddles now, and the computer moves its paddle to meet it. The sides aren't walls any more: a ball that gets past a paddle goes off the side, and comes back from the middle going the other way.
from kaypy import *
kaypy(width=800, height=600, background=[20, 24, 40])
PADDLE_W = 16 # how wide a paddle is
PADDLE_H = 100 # and how tall
PADDLE_GAP = 30 # how far each paddle is from its edge of the window
PADDLE_SPEED = 480 # how fast your paddle moves, in pixels per second
BALL_SIZE = 16
BALL_SPEED = 360 # how fast the ball goes, in pixels per second
BALL_ANGLE = 0.75 # how steeply the ends of a paddle send the ball
CPU_SPEED = 300 # how fast the computer's paddle can move
# The net: a dashed line down the middle. It is only a picture.
for y in range(10, height(), 40):
add([rect(4, 20), pos(width() / 2 - 2, y), color(90, 100, 130)])
# anchor("center") means pos() is the middle of the paddle.
player = add([rect(PADDLE_W, PADDLE_H), anchor("center"),
pos(PADDLE_GAP, height() / 2), color(120, 220, 255)])
cpu = add([rect(PADDLE_W, PADDLE_H), anchor("center"),
pos(width() - PADDLE_GAP, height() / 2), color(255, 140, 120)])
# The ball carries its own speed: dx across and dy down, in pixels per
# second. A negative dx means it is going left, a negative dy up.
ball = add([rect(BALL_SIZE, BALL_SIZE), anchor("center"),
pos(width() / 2, height() / 2)])
ball.dx = BALL_SPEED
ball.dy = rand(-BALL_SPEED / 2, BALL_SPEED / 2)
@onKeyDown("up")
def move_up():
player.move(0, -PADDLE_SPEED)
keep_on_screen()
@onKeyDown("down")
def move_down():
player.move(0, PADDLE_SPEED)
keep_on_screen()
@onMouseDown
def move_to_mouse():
if mousePos().y < player.pos.y - 10:
player.move(0, -PADDLE_SPEED)
if mousePos().y > player.pos.y + 10:
player.move(0, PADDLE_SPEED)
keep_on_screen()
def keep_on_screen():
player.pos.y = clamp(player.pos.y, PADDLE_H / 2,
height() - PADDLE_H / 2)
@onUpdate
def computer():
# The computer follows the ball, but only when the ball is coming its
# way, and never faster than CPU_SPEED.
if ball.dx > 0:
target = ball.pos.y
else:
target = height() / 2 # drift back to the middle
if target < cpu.pos.y - 10:
cpu.move(0, -CPU_SPEED)
if target > cpu.pos.y + 10:
cpu.move(0, CPU_SPEED)
@onUpdate
def move_ball():
# Where the ball is before it moves, to see what it crossed.
old_x = ball.pos.x
ball.pos.x = ball.pos.x + ball.dx * dt()
ball.pos.y = ball.pos.y + ball.dy * dt()
# Off the top or the bottom: put it back on the edge, and turn it round.
if ball.pos.y < BALL_SIZE / 2:
ball.pos.y = BALL_SIZE / 2
ball.dy = -ball.dy
if ball.pos.y > height() - BALL_SIZE / 2:
ball.pos.y = height() - BALL_SIZE / 2
ball.dy = -ball.dy
# Did it just cross the front of your paddle, going left?
edge = player.pos.x + PADDLE_W / 2 + BALL_SIZE / 2
if old_x >= edge and ball.pos.x < edge and touching(player):
ball.pos.x = edge
bounce(player, 1)
# The front of the computer's paddle, going right.
edge = cpu.pos.x - PADDLE_W / 2 - BALL_SIZE / 2
if old_x <= edge and ball.pos.x > edge and touching(cpu):
ball.pos.x = edge
bounce(cpu, -1)
# Gone off the side: back to the middle, going the other way.
if ball.pos.x < -BALL_SIZE or ball.pos.x > width() + BALL_SIZE:
ball.pos = vec2(width() / 2, height() / 2)
ball.dx = -ball.dx
def touching(paddle):
# Is the ball level with the paddle? Half the paddle and half the ball,
# up or down from the paddle's middle.
return abs(ball.pos.y - paddle.pos.y) < PADDLE_H / 2 + BALL_SIZE / 2
def bounce(paddle, direction):
# direction is 1 to send the ball right, -1 to send it left.
ball.dx = BALL_SPEED * direction
# Where on the paddle it hit: -1 is the top end, 0 the middle, 1 the
# bottom end. That decides how steeply it comes off.
where = (ball.pos.y - paddle.pos.y) / (PADDLE_H / 2)
where = clamp(where, -1, 1)
ball.dy = where * BALL_SPEED * BALL_ANGLE
Did the ball reach the paddle? The front of your paddle
is its middle plus half its width. The ball's middle touches it at half a
ball further on: that is edge, 46 pixels from the left of the
window. The ball moves 6 pixels or so a frame, so it is hardly ever exactly
at edge. So the question is whether it crossed
the edge this frame. old_x is where the ball was before it
moved. If it was in front of edge before and is behind it now,
it has just crossed.
Was the paddle there? touching() asks
whether the ball is level with the paddle: whether the distance between their
middles, up and down, is less than half the paddle plus half the ball. It
gives back True or False, which is why it can go
straight into an if.
Where it hits decides where it goes.
bounce() turns the ball round, then works out
where: how far the ball is from the paddle's middle, divided by
half the paddle. In the middle that is 0; at the top end, −1; at the bottom
end, 1. A ball hitting 40 pixels below the middle gets where =
0.8, and goes off downwards at 0.8 * 360 * 0.75, 216 pixels a
second. Hitting the ball with the end of the paddle is how you aim it.
clamp keeps where between −1 and 1, because the
ball can touch the paddle with its edge, a little beyond the paddle's end.
The computer moves its paddle towards the ball, but only
when the ball is coming its way; otherwise it drifts back to the middle. It
can never move faster than CPU_SPEED, 300 pixels a second. For
now that is plenty: the steepest the ball can go is
0.75 * 360, 270 pixels a second up or down. In step 5 the ball
gets faster, and then a steep one can get away from it.
Something to break. In
move_ball, change bounce(player, 1) to
bounce(player, -1), so the ball is sent left instead of right.
There's no error. The ball reaches your paddle and stops dead, stuck to the
front of it. Every frame it moves left, crosses edge, and is put
back on it, still going left. Move the paddle away and the ball is let go:
it carries on off the side.
Step 4 — points, and serving
A ball that goes off the side is a point to the player at the other end. The score goes at the top, and after each point the ball waits in the middle for a second, then is served towards whoever lost it.
from kaypy import *
kaypy(width=800, height=600, background=[20, 24, 40])
PADDLE_W = 16 # how wide a paddle is
PADDLE_H = 100 # and how tall
PADDLE_GAP = 30 # how far each paddle is from its edge of the window
PADDLE_SPEED = 480 # how fast your paddle moves, in pixels per second
BALL_SIZE = 16
BALL_SPEED = 360 # how fast the ball goes, in pixels per second
BALL_ANGLE = 0.75 # how steeply the ends of a paddle send the ball
CPU_SPEED = 300 # how fast the computer's paddle can move
player_score = 0
cpu_score = 0
# The net: a dashed line down the middle. It is only a picture.
for y in range(10, height(), 40):
add([rect(4, 20), pos(width() / 2 - 2, y), color(90, 100, 130)])
# anchor("center") means pos() is the middle of the paddle.
player = add([rect(PADDLE_W, PADDLE_H), anchor("center"),
pos(PADDLE_GAP, height() / 2), color(120, 220, 255)])
cpu = add([rect(PADDLE_W, PADDLE_H), anchor("center"),
pos(width() - PADDLE_GAP, height() / 2), color(255, 140, 120)])
# The ball carries its own speed: dx across and dy down, in pixels per
# second. A negative dx means it is going left, a negative dy up.
ball = add([rect(BALL_SIZE, BALL_SIZE), anchor("center"),
pos(width() / 2, height() / 2)])
ball.dx = 0
ball.dy = 0
player_label = add([text("0", size=56), pos(width() / 2 - 60, 50),
anchor("center")])
cpu_label = add([text("0", size=56), pos(width() / 2 + 60, 50),
anchor("center")])
@onKeyDown("up")
def move_up():
player.move(0, -PADDLE_SPEED)
keep_on_screen()
@onKeyDown("down")
def move_down():
player.move(0, PADDLE_SPEED)
keep_on_screen()
@onMouseDown
def move_to_mouse():
if mousePos().y < player.pos.y - 10:
player.move(0, -PADDLE_SPEED)
if mousePos().y > player.pos.y + 10:
player.move(0, PADDLE_SPEED)
keep_on_screen()
def keep_on_screen():
player.pos.y = clamp(player.pos.y, PADDLE_H / 2,
height() - PADDLE_H / 2)
@onUpdate
def computer():
# The computer follows the ball, but only when the ball is coming its
# way, and never faster than CPU_SPEED.
if ball.dx > 0:
target = ball.pos.y
else:
target = height() / 2 # drift back to the middle
if target < cpu.pos.y - 10:
cpu.move(0, -CPU_SPEED)
if target > cpu.pos.y + 10:
cpu.move(0, CPU_SPEED)
def serve(direction):
# direction is 1 to serve to the right, -1 to the left. The ball starts
# in the middle and goes off at a random slant.
ball.pos = vec2(width() / 2, height() / 2)
ball.dx = BALL_SPEED * direction
ball.dy = rand(-BALL_SPEED / 2, BALL_SPEED / 2)
# wait() runs a function later, and it has to be one that needs nothing
# handed to it. So there is one for each way the ball can be served.
def serve_left():
serve(-1)
def serve_right():
serve(1)
# The first serve goes towards you, a second and a half in.
wait(1.5, serve_left)
@onUpdate
def move_ball():
# Where the ball is before it moves, to see what it crossed.
old_x = ball.pos.x
ball.pos.x = ball.pos.x + ball.dx * dt()
ball.pos.y = ball.pos.y + ball.dy * dt()
# Off the top or the bottom: put it back on the edge, and turn it round.
if ball.pos.y < BALL_SIZE / 2:
ball.pos.y = BALL_SIZE / 2
ball.dy = -ball.dy
if ball.pos.y > height() - BALL_SIZE / 2:
ball.pos.y = height() - BALL_SIZE / 2
ball.dy = -ball.dy
# Did it just cross the front of your paddle, going left?
edge = player.pos.x + PADDLE_W / 2 + BALL_SIZE / 2
if old_x >= edge and ball.pos.x < edge and touching(player):
ball.pos.x = edge
bounce(player, 1)
# The front of the computer's paddle, going right.
edge = cpu.pos.x - PADDLE_W / 2 - BALL_SIZE / 2
if old_x <= edge and ball.pos.x > edge and touching(cpu):
ball.pos.x = edge
bounce(cpu, -1)
# Gone off the side: a point to whoever is at the other end.
if ball.pos.x < -BALL_SIZE:
point_to("cpu")
if ball.pos.x > width() + BALL_SIZE:
point_to("player")
def touching(paddle):
# Is the ball level with the paddle? Half the paddle and half the ball,
# up or down from the paddle's middle.
return abs(ball.pos.y - paddle.pos.y) < PADDLE_H / 2 + BALL_SIZE / 2
def bounce(paddle, direction):
# direction is 1 to send the ball right, -1 to send it left.
ball.dx = BALL_SPEED * direction
# Where on the paddle it hit: -1 is the top end, 0 the middle, 1 the
# bottom end. That decides how steeply it comes off.
where = (ball.pos.y - paddle.pos.y) / (PADDLE_H / 2)
where = clamp(where, -1, 1)
ball.dy = where * BALL_SPEED * BALL_ANGLE
def point_to(who):
global player_score, cpu_score
# Park the ball in the middle, standing still, until the next serve.
ball.pos = vec2(width() / 2, height() / 2)
ball.dx = 0
ball.dy = 0
if who == "player":
player_score = player_score + 1
player_label.text = str(player_score)
wait(1, serve_right)
else:
cpu_score = cpu_score + 1
cpu_label.text = str(cpu_score)
wait(1, serve_left)
Scoring. point_to("cpu") or
point_to("player") parks the ball in the middle with both its
speeds at 0, adds one to the right score, and puts the new number on the
screen. str() turns the number into text, because a
text() object has to be given text.
point_to changes player_score and
cpu_score, so it says global first. They were made
outside the function, and a function that changes a variable from outside
itself has to say so. Without it, Python decides they are new variables
belonging to point_to, with no value yet. Leave it out and
nothing goes wrong until the first point. If you don't move, that's about
two and a half seconds in, and then:
UnboundLocalError: cannot access local variable 'cpu_score' where it is not associated with a value
Serving. serve(direction) puts the ball in
the middle and sends it off: direction is 1 for right, −1 for
left. wait(1, serve_right) runs serve_right a
second from now. wait() wants a function it can run later
with nothing handed to it, and serve needs to be told
which way. So there are two little functions, serve_left and
serve_right, which each call serve the right
way.
Something to break. Put brackets
after serve_left in the first serve:
wait(1.5, serve_left()). There's no error message, which makes
it a hard mistake to spot. The ball goes the moment you press Run, with no
pause at all. Without brackets, wait() is handed the
function, to run in 1.5 seconds. With them, Python runs
serve_left right now, to find out what to hand over, and the
ball is served then and there. wait() is handed what
serve_left gave back, which is nothing, and has nothing left to
do when the time comes.
Step 5 — faster every hit
A long rally gets exciting: every hit makes the ball a little faster.
BALL_SPEED becomes three settings.
from kaypy import *
kaypy(width=800, height=600, background=[20, 24, 40])
PADDLE_W = 16 # how wide a paddle is
PADDLE_H = 100 # and how tall
PADDLE_GAP = 30 # how far each paddle is from its edge of the window
PADDLE_SPEED = 480 # how fast your paddle moves, in pixels per second
BALL_SIZE = 16
BALL_START = 360 # how fast the ball goes at the start of each point
BALL_GAIN = 25 # ...how much faster every hit makes it
BALL_FASTEST = 750 # ...and the fastest it ever goes
BALL_ANGLE = 0.75 # how steeply the ends of a paddle send the ball
CPU_SPEED = 300 # how fast the computer's paddle can move
player_score = 0
cpu_score = 0
# The net: a dashed line down the middle. It is only a picture.
for y in range(10, height(), 40):
add([rect(4, 20), pos(width() / 2 - 2, y), color(90, 100, 130)])
# anchor("center") means pos() is the middle of the paddle.
player = add([rect(PADDLE_W, PADDLE_H), anchor("center"),
pos(PADDLE_GAP, height() / 2), color(120, 220, 255)])
cpu = add([rect(PADDLE_W, PADDLE_H), anchor("center"),
pos(width() - PADDLE_GAP, height() / 2), color(255, 140, 120)])
# The ball carries its own speed: dx across and dy down, in pixels per
# second. A negative dx means it is going left, a negative dy up.
ball = add([rect(BALL_SIZE, BALL_SIZE), anchor("center"),
pos(width() / 2, height() / 2)])
ball.dx = 0
ball.dy = 0
player_label = add([text("0", size=56), pos(width() / 2 - 60, 50),
anchor("center")])
cpu_label = add([text("0", size=56), pos(width() / 2 + 60, 50),
anchor("center")])
@onKeyDown("up")
def move_up():
player.move(0, -PADDLE_SPEED)
keep_on_screen()
@onKeyDown("down")
def move_down():
player.move(0, PADDLE_SPEED)
keep_on_screen()
@onMouseDown
def move_to_mouse():
if mousePos().y < player.pos.y - 10:
player.move(0, -PADDLE_SPEED)
if mousePos().y > player.pos.y + 10:
player.move(0, PADDLE_SPEED)
keep_on_screen()
def keep_on_screen():
player.pos.y = clamp(player.pos.y, PADDLE_H / 2,
height() - PADDLE_H / 2)
@onUpdate
def computer():
# The computer follows the ball, but only when the ball is coming its
# way, and never faster than CPU_SPEED.
if ball.dx > 0:
target = ball.pos.y
else:
target = height() / 2 # drift back to the middle
if target < cpu.pos.y - 10:
cpu.move(0, -CPU_SPEED)
if target > cpu.pos.y + 10:
cpu.move(0, CPU_SPEED)
def serve(direction):
# direction is 1 to serve to the right, -1 to the left. The ball starts
# in the middle and goes off at a random slant.
ball.pos = vec2(width() / 2, height() / 2)
ball.dx = BALL_START * direction
ball.dy = rand(-BALL_START / 2, BALL_START / 2)
# wait() runs a function later, and it has to be one that needs nothing
# handed to it. So there is one for each way the ball can be served.
def serve_left():
serve(-1)
def serve_right():
serve(1)
# The first serve goes towards you, a second and a half in.
wait(1.5, serve_left)
@onUpdate
def move_ball():
# Where the ball is before it moves, to see what it crossed.
old_x = ball.pos.x
ball.pos.x = ball.pos.x + ball.dx * dt()
ball.pos.y = ball.pos.y + ball.dy * dt()
# Off the top or the bottom: put it back on the edge, and turn it round.
if ball.pos.y < BALL_SIZE / 2:
ball.pos.y = BALL_SIZE / 2
ball.dy = -ball.dy
if ball.pos.y > height() - BALL_SIZE / 2:
ball.pos.y = height() - BALL_SIZE / 2
ball.dy = -ball.dy
# Did it just cross the front of your paddle, going left?
edge = player.pos.x + PADDLE_W / 2 + BALL_SIZE / 2
if old_x >= edge and ball.pos.x < edge and touching(player):
ball.pos.x = edge
bounce(player, 1)
# The front of the computer's paddle, going right.
edge = cpu.pos.x - PADDLE_W / 2 - BALL_SIZE / 2
if old_x <= edge and ball.pos.x > edge and touching(cpu):
ball.pos.x = edge
bounce(cpu, -1)
# Gone off the side: a point to whoever is at the other end.
if ball.pos.x < -BALL_SIZE:
point_to("cpu")
if ball.pos.x > width() + BALL_SIZE:
point_to("player")
def touching(paddle):
# Is the ball level with the paddle? Half the paddle and half the ball,
# up or down from the paddle's middle.
return abs(ball.pos.y - paddle.pos.y) < PADDLE_H / 2 + BALL_SIZE / 2
def bounce(paddle, direction):
# direction is 1 to send the ball right, -1 to send it left. A little
# faster each hit, up to BALL_FASTEST.
speed = abs(ball.dx) + BALL_GAIN
if speed > BALL_FASTEST:
speed = BALL_FASTEST
ball.dx = speed * direction
# Where on the paddle it hit: -1 is the top end, 0 the middle, 1 the
# bottom end. That decides how steeply it comes off.
where = (ball.pos.y - paddle.pos.y) / (PADDLE_H / 2)
where = clamp(where, -1, 1)
ball.dy = where * speed * BALL_ANGLE
def point_to(who):
global player_score, cpu_score
# Park the ball in the middle, standing still, until the next serve.
ball.pos = vec2(width() / 2, height() / 2)
ball.dx = 0
ball.dy = 0
if who == "player":
player_score = player_score + 1
player_label.text = str(player_score)
wait(1, serve_right)
else:
cpu_score = cpu_score + 1
cpu_label.text = str(cpu_score)
wait(1, serve_left)
Each point starts at BALL_START, 360 pixels a second. In
bounce, abs(ball.dx) is how fast the ball is going
across, without the minus sign that says it is going left. Every hit adds
BALL_GAIN, 25, so after 10 hits the ball is going 610. The
if stops it at BALL_FASTEST, 750, which it reaches
on the 16th hit. Without that, a long enough rally would get too fast to
see.
The angle uses the new speed too, so a fast ball comes off
the end of a paddle steeply enough to beat the computer, which is still
limited to 300 pixels a second.
Something to break. Leave out the
abs: speed = ball.dx + BALL_GAIN. There's no error.
The ball reaches your paddle and then creeps away from it, at 15 pixels a
second. Going left, dx is −360, so speed is −335,
and the ball is sent off at −335: still going left. Next frame it crosses
edge again and bounces again, −310, and again, and again, 25 at
a time, until dx gets past 0 to 15. abs() is what
keeps the minus sign out of it.
Step 6 — winning, and playing again
First to 7 wins. Then press space or click to play again.
Playing again means building everything fresh: new scores, a new ball,
and no wait() from the last game still waiting to serve. That's
what a scene is for. Everything from the net down moves
inside @scene("game"), one step further in, and
go("game") at the bottom builds it. Calling
go("game") again throws the whole scene away, every object,
handler and wait(), and builds it again.
from kaypy import *
kaypy(width=800, height=600, background=[20, 24, 40])
PADDLE_W = 16 # how wide a paddle is
PADDLE_H = 100 # and how tall
PADDLE_GAP = 30 # how far each paddle is from its edge of the window
PADDLE_SPEED = 480 # how fast your paddle moves, in pixels per second
CPU_SPEED = 300 # how fast the computer's paddle can move
BALL_SIZE = 16
BALL_START = 360 # how fast the ball goes at the start of each point
BALL_GAIN = 25 # ...how much faster every hit makes it
BALL_FASTEST = 750 # ...and the fastest it ever goes
BALL_ANGLE = 0.75 # how steeply the ends of a paddle send the ball
WIN_SCORE = 7 # first to this many points wins
player_score = 0
cpu_score = 0
playing = True
@scene("game")
def game():
global player_score, cpu_score, playing
# A new game, so everything starts again.
player_score = 0
cpu_score = 0
playing = True
# The net: a dashed line down the middle. It is only a picture.
for y in range(10, height(), 40):
add([rect(4, 20), pos(width() / 2 - 2, y), color(90, 100, 130)])
# anchor("center") means pos() is the middle of the paddle, which makes
# "is the ball within the paddle?" easy to work out below.
player = add([rect(PADDLE_W, PADDLE_H), anchor("center"),
pos(PADDLE_GAP, height() / 2), color(120, 220, 255)])
cpu = add([rect(PADDLE_W, PADDLE_H), anchor("center"),
pos(width() - PADDLE_GAP, height() / 2), color(255, 140, 120)])
# The ball carries its own speed: dx across and dy down, in pixels per
# second. A negative dx means it is going left, a negative dy up.
ball = add([rect(BALL_SIZE, BALL_SIZE), anchor("center"),
pos(width() / 2, height() / 2)])
ball.dx = 0
ball.dy = 0
player_label = add([text("0", size=56), pos(width() / 2 - 60, 50),
anchor("center")])
cpu_label = add([text("0", size=56), pos(width() / 2 + 60, 50),
anchor("center")])
# The message for the end of the game. width=560 wraps a long one onto
# more than one line.
message = add([text("", size=26, width=560),
pos(width() / 2, height() - 120), anchor("center"),
z(10)])
# -------------------------------------------------------- your paddle
#
# onKeyDown runs its function every frame the key is held, so the
# paddle keeps moving until you let go.
@onKeyDown("up")
def move_up():
player.move(0, -PADDLE_SPEED)
keep_on_screen()
@onKeyDown("down")
def move_down():
player.move(0, PADDLE_SPEED)
keep_on_screen()
# Holding the mouse down -- or a finger, on a phone -- moves the paddle
# towards it. Within 10 pixels counts as there, and the paddle stops.
@onMouseDown
def move_to_mouse():
if mousePos().y < player.pos.y - 10:
player.move(0, -PADDLE_SPEED)
if mousePos().y > player.pos.y + 10:
player.move(0, PADDLE_SPEED)
keep_on_screen()
def keep_on_screen():
player.pos.y = clamp(player.pos.y, PADDLE_H / 2,
height() - PADDLE_H / 2)
# ------------------------------------------------- the computer's paddle
#
# The computer follows the ball, but only when the ball is coming its
# way, and never faster than CPU_SPEED. That is what makes it possible
# to beat: a fast ball at a steep angle gets past it.
@onUpdate
def computer():
if ball.dx > 0:
target = ball.pos.y
else:
target = height() / 2 # drift back to the middle
if target < cpu.pos.y - 10:
cpu.move(0, -CPU_SPEED)
if target > cpu.pos.y + 10:
cpu.move(0, CPU_SPEED)
# ------------------------------------------------------------- serving
def serve(direction):
# direction is 1 to serve to the right, -1 to the left. The ball
# starts in the middle and goes off at a random slant.
ball.pos = vec2(width() / 2, height() / 2)
ball.dx = BALL_START * direction
ball.dy = rand(-BALL_START / 2, BALL_START / 2)
# wait() runs a function later, and it has to be one that needs nothing
# handed to it. So there is one for each way the ball can be served.
def serve_left():
serve(-1)
def serve_right():
serve(1)
# The first serve goes towards you, a second and a half in.
wait(1.5, serve_left)
# ------------------------------------------------------------ the ball
@onUpdate
def move_ball():
# Where the ball is before it moves, to see what it crossed.
old_x = ball.pos.x
ball.pos.x = ball.pos.x + ball.dx * dt()
ball.pos.y = ball.pos.y + ball.dy * dt()
# Off the top or the bottom: turn it round. Putting it back on the
# edge matters. A slow frame can carry the ball well past it, and if
# the next frame does not bring it back inside, it is turned round
# again -- the wrong way -- and leaves the window.
if ball.pos.y < BALL_SIZE / 2:
ball.pos.y = BALL_SIZE / 2
ball.dy = -ball.dy
if ball.pos.y > height() - BALL_SIZE / 2:
ball.pos.y = height() - BALL_SIZE / 2
ball.dy = -ball.dy
# Did it just cross the front of your paddle, going left? "Just
# crossed" -- it was on one side before it moved and is on the other
# now -- is what matters. The ball moves 6 to 12 pixels a frame, and
# more on a slow one, so it is hardly ever exactly AT the front.
edge = player.pos.x + PADDLE_W / 2 + BALL_SIZE / 2
if old_x >= edge and ball.pos.x < edge and touching(player):
ball.pos.x = edge
bounce(player, 1)
# The front of the computer's paddle, going right.
edge = cpu.pos.x - PADDLE_W / 2 - BALL_SIZE / 2
if old_x <= edge and ball.pos.x > edge and touching(cpu):
ball.pos.x = edge
bounce(cpu, -1)
# Gone off the side: a point to whoever is at the other end.
if ball.pos.x < -BALL_SIZE:
point_to("cpu")
if ball.pos.x > width() + BALL_SIZE:
point_to("player")
def touching(paddle):
# Is the ball level with the paddle? Half the paddle and half the
# ball, up or down from the paddle's middle.
return abs(ball.pos.y - paddle.pos.y) < PADDLE_H / 2 + BALL_SIZE / 2
def bounce(paddle, direction):
# A little faster each hit, up to BALL_FASTEST.
speed = abs(ball.dx) + BALL_GAIN
if speed > BALL_FASTEST:
speed = BALL_FASTEST
ball.dx = speed * direction
# Where on the paddle it hit: -1 is the top end, 0 the middle, 1 the
# bottom end. That decides how steeply it comes off.
where = (ball.pos.y - paddle.pos.y) / (PADDLE_H / 2)
where = clamp(where, -1, 1)
ball.dy = where * speed * BALL_ANGLE
# ------------------------------------------------------------ scoring
def point_to(who):
global player_score, cpu_score, playing
# Park the ball in the middle, standing still, until the next serve.
ball.pos = vec2(width() / 2, height() / 2)
ball.dx = 0
ball.dy = 0
if who == "player":
player_score = player_score + 1
player_label.text = str(player_score)
else:
cpu_score = cpu_score + 1
cpu_label.text = str(cpu_score)
if player_score == WIN_SCORE or cpu_score == WIN_SCORE:
playing = False
ball.destroy()
if player_score == WIN_SCORE:
message.text = "You win! Press SPACE or click to play again"
else:
message.text = ("The computer wins. Press SPACE or click "
"to play again")
return
# The next serve goes towards whoever just lost the point.
if who == "player":
wait(1, serve_right)
else:
wait(1, serve_left)
# Once the game is over, space or a click starts a new one.
def again():
if not playing:
go("game")
onKeyPress("space", again)
onClick(again)
go("game")
The variables live outside the scene.
player_score, cpu_score and playing are
made at the top, and the scene sets them back to their starting values every
time it is built. It changes them, so it says global first, like
point_to.
The end of a game. When either score reaches
WIN_SCORE, point_to sets playing to
False, destroys the ball, and says who won. The
return leaves point_to straight away, before it can
arrange another serve.
again only starts a new game when
playing is False. That matters more here than in
most games, because you play this one with the mouse.
Something to break. Take the
if not playing: out of again, so it always runs
go("game"). There's no error. Then, in the middle of a game,
hold the mouse down to move your paddle. Pressing the button is a click, so
the game starts again from 0–0, every time you go to move.
Step 7 — the whole game
This is the game exactly as it is on the games page. It adds three things to step 6:
- Sounds: a beep when the ball hits a paddle, a smaller
one off the top and bottom, a thump for a point, and a rising beep at the
end. Each is
loadSoundonce at the top andplay()where it happens. - A message at the start saying how to move, which goes away when the first point is scored.
- Comments that explain why each part is there, and the settings gathered into one block near the top.
"""Pong -- the first video game anyone played at home. You against the
computer.
up and down arrows move your paddle
or hold the mouse (or a finger) where you want the paddle to go
Your paddle is on the left. Hit the ball back past the computer's paddle to
score a point. Every hit makes the ball a little faster, and where it hits
your paddle decides where it goes: the middle sends it straight, the ends
send it off at an angle. First to 7 wins.
HOW THIS FILE IS PUT TOGETHER
1. the window and the sounds
2. the settings
3. the game scene: the paddles, the ball, bouncing, scoring
4. start the game
There are no pictures at all. The paddles, the ball and the net are
rectangles, and the ball is a game object that carries its own speed.
"""
from kaypy import *
# ----------------------------------------------------------------- 1. setup
kaypy(width=800, height=600, background=[20, 24, 40])
loadSound("hit", "sounds/beep.wav")
loadSound("wall", "sounds/small_beep.wav")
loadSound("point", "sounds/thump.wav")
loadSound("win", "sounds/rising_beep.wav")
# -------------------------------------------------------------- 2. settings
#
# Everything you would want to change is here. Change one at a time and
# play it.
PADDLE_W = 16 # how wide a paddle is
PADDLE_H = 100 # and how tall
PADDLE_GAP = 30 # how far each paddle is from its edge of the window
PADDLE_SPEED = 480 # how fast your paddle moves, in pixels per second
CPU_SPEED = 300 # how fast the computer's paddle can move
BALL_SIZE = 16
BALL_START = 360 # how fast the ball goes at the start of each point
BALL_GAIN = 25 # ...how much faster every hit makes it
BALL_FASTEST = 750 # ...and the fastest it ever goes
BALL_ANGLE = 0.75 # how steeply the ends of a paddle send the ball
WIN_SCORE = 7 # first to this many points wins
# ------------------------------------------------------- 3. the game scene
#
# These change while you play. A function that CHANGES one has to say
# `global` first, which means "the one up here, not a new one of my own".
player_score = 0
cpu_score = 0
playing = True
@scene("game")
def game():
global player_score, cpu_score, playing
# A new game, so everything starts again.
player_score = 0
cpu_score = 0
playing = True
# The net: a dashed line down the middle. It is only a picture.
for y in range(10, height(), 40):
add([rect(4, 20), pos(width() / 2 - 2, y), color(90, 100, 130)])
# anchor("center") means pos() is the middle of the paddle, which makes
# "is the ball within the paddle?" easy to work out below.
player = add([rect(PADDLE_W, PADDLE_H), anchor("center"),
pos(PADDLE_GAP, height() / 2), color(120, 220, 255)])
cpu = add([rect(PADDLE_W, PADDLE_H), anchor("center"),
pos(width() - PADDLE_GAP, height() / 2), color(255, 140, 120)])
# The ball carries its own speed: dx across and dy down, in pixels per
# second. A negative dx means it is going left, a negative dy up.
ball = add([rect(BALL_SIZE, BALL_SIZE), anchor("center"),
pos(width() / 2, height() / 2)])
ball.dx = 0
ball.dy = 0
player_label = add([text("0", size=56), pos(width() / 2 - 60, 50),
anchor("center")])
cpu_label = add([text("0", size=56), pos(width() / 2 + 60, 50),
anchor("center")])
# width=560 wraps a long message onto more than one line.
message = add([text("Up and down arrows, or hold the mouse where you "
"want your paddle. First to " + str(WIN_SCORE)
+ " wins!", size=26, width=560),
pos(width() / 2, height() - 120), anchor("center"),
z(10)])
# -------------------------------------------------------- your paddle
#
# onKeyDown runs its function every frame the key is held, so the
# paddle keeps moving until you let go.
@onKeyDown("up")
def move_up():
player.move(0, -PADDLE_SPEED)
keep_on_screen()
@onKeyDown("down")
def move_down():
player.move(0, PADDLE_SPEED)
keep_on_screen()
# Holding the mouse down -- or a finger, on a phone -- moves the paddle
# towards it. Within 10 pixels counts as there, and the paddle stops.
@onMouseDown
def move_to_mouse():
if mousePos().y < player.pos.y - 10:
player.move(0, -PADDLE_SPEED)
if mousePos().y > player.pos.y + 10:
player.move(0, PADDLE_SPEED)
keep_on_screen()
def keep_on_screen():
player.pos.y = clamp(player.pos.y, PADDLE_H / 2,
height() - PADDLE_H / 2)
# ------------------------------------------------- the computer's paddle
#
# The computer follows the ball, but only when the ball is coming its
# way, and never faster than CPU_SPEED. That is what makes it possible
# to beat: a fast ball at a steep angle gets past it.
@onUpdate
def computer():
if ball.dx > 0:
target = ball.pos.y
else:
target = height() / 2 # drift back to the middle
if target < cpu.pos.y - 10:
cpu.move(0, -CPU_SPEED)
if target > cpu.pos.y + 10:
cpu.move(0, CPU_SPEED)
# ------------------------------------------------------------- serving
def serve(direction):
# direction is 1 to serve to the right, -1 to the left. The ball
# starts in the middle and goes off at a random slant.
ball.pos = vec2(width() / 2, height() / 2)
ball.dx = BALL_START * direction
ball.dy = rand(-BALL_START / 2, BALL_START / 2)
# wait() runs a function later, and it has to be one that needs nothing
# handed to it. So there is one for each way the ball can be served.
def serve_left():
serve(-1)
def serve_right():
serve(1)
# The first serve goes towards you, a second and a half in.
wait(1.5, serve_left)
# ------------------------------------------------------------ the ball
@onUpdate
def move_ball():
# Where the ball is before it moves, to see what it crossed.
old_x = ball.pos.x
ball.pos.x = ball.pos.x + ball.dx * dt()
ball.pos.y = ball.pos.y + ball.dy * dt()
# Off the top or the bottom: turn it round. Putting it back on the
# edge matters. A slow frame can carry the ball well past it, and if
# the next frame does not bring it back inside, it is turned round
# again -- the wrong way -- and leaves the window.
if ball.pos.y < BALL_SIZE / 2:
ball.pos.y = BALL_SIZE / 2
ball.dy = -ball.dy
play("wall")
if ball.pos.y > height() - BALL_SIZE / 2:
ball.pos.y = height() - BALL_SIZE / 2
ball.dy = -ball.dy
play("wall")
# Did it just cross the front of your paddle, going left? "Just
# crossed" -- it was on one side before it moved and is on the other
# now -- is what matters. The ball moves 6 to 12 pixels a frame, and
# more on a slow one, so it is hardly ever exactly AT the front.
edge = player.pos.x + PADDLE_W / 2 + BALL_SIZE / 2
if old_x >= edge and ball.pos.x < edge and touching(player):
ball.pos.x = edge
bounce(player, 1)
# The front of the computer's paddle, going right.
edge = cpu.pos.x - PADDLE_W / 2 - BALL_SIZE / 2
if old_x <= edge and ball.pos.x > edge and touching(cpu):
ball.pos.x = edge
bounce(cpu, -1)
# Gone off the side: a point to whoever is at the other end.
if ball.pos.x < -BALL_SIZE:
point_to("cpu")
if ball.pos.x > width() + BALL_SIZE:
point_to("player")
def touching(paddle):
# Is the ball level with the paddle? Half the paddle and half the
# ball, up or down from the paddle's middle.
return abs(ball.pos.y - paddle.pos.y) < PADDLE_H / 2 + BALL_SIZE / 2
def bounce(paddle, direction):
# A little faster each hit, up to BALL_FASTEST.
speed = abs(ball.dx) + BALL_GAIN
if speed > BALL_FASTEST:
speed = BALL_FASTEST
ball.dx = speed * direction
# Where on the paddle it hit: -1 is the top end, 0 the middle, 1 the
# bottom end. That decides how steeply it comes off.
where = (ball.pos.y - paddle.pos.y) / (PADDLE_H / 2)
where = clamp(where, -1, 1)
ball.dy = where * speed * BALL_ANGLE
play("hit")
# ------------------------------------------------------------ scoring
def point_to(who):
global player_score, cpu_score, playing
# Park the ball in the middle, standing still, until the next serve.
ball.pos = vec2(width() / 2, height() / 2)
ball.dx = 0
ball.dy = 0
message.text = ""
if who == "player":
player_score = player_score + 1
player_label.text = str(player_score)
else:
cpu_score = cpu_score + 1
cpu_label.text = str(cpu_score)
if player_score == WIN_SCORE or cpu_score == WIN_SCORE:
playing = False
ball.destroy()
play("win")
if player_score == WIN_SCORE:
message.text = "You win! Press SPACE or click to play again"
else:
message.text = ("The computer wins. Press SPACE or click "
"to play again")
return
play("point")
# The next serve goes towards whoever just lost the point.
if who == "player":
wait(1, serve_right)
else:
wait(1, serve_left)
# Once the game is over, space or a click starts a new one.
def again():
if not playing:
go("game")
onKeyPress("space", again)
onClick(again)
# ------------------------------------------------------- 4. start the game
go("game")
Make it yours
Open the whole game in the playground → It goes straight into the editor, ready to change and run.
- Make the computer easier or harder.
CPU_SPEEDis how fast it can move. Make it smaller and you'll beat it more often; make it bigger and you won't. - Change the shape of the game. A bigger
PADDLE_His easier to hit with. A biggerBALL_GAINmakes rallies get fast sooner. A smallerWIN_SCOREmakes a quicker game. - Two players. Take out the computer's
@onUpdate. Change your paddle's keys to"w"and"s", so two people at one keyboard aren't in each other's way, and give the right-hand paddle two handlers of its own with@onKeyDown("up")and@onKeyDown("down"), movingcpuinstead ofplayer. - A different ball. Make it a picture: load one with
loadSprite("apple", "images/apple.png")at the top and usesprite("apple")instead ofrect(BALL_SIZE, BALL_SIZE). Click Sprites in the playground to see what there is. If the picture is much bigger than 16 pixels, addscale()to shrink it, or changeBALL_SIZEto match.