Building Paddle Ball
Play it first → A ball bounces around the top of the window, and you keep it up with a paddle along the bottom. Every hit is a point and makes the ball a little faster. Miss it and it's over. It is Pong with one player.
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 paddle and the ball are rectangles.
Build it after Bomb Dodge,
Goblin Hit and
Log Hacker, and before
Spiral Storm and Pong. It is put together the way Bomb Dodge is — one
score, one life, global, a scene to play again in — and it adds
the one idea Pong is built on.
The one big idea: the ball carries its own speed
In Bomb Dodge, a bomb falls because every frame it calls
bomb.move(0, fall_speed()), and it only ever goes one way:
down. A ball that bounces has to go every way, and change its mind.
So the ball keeps its speed on itself, in two numbers.
ball.dx is how fast it is going across: plus is right, minus is
left. ball.dy is how fast it is going down: plus is down, minus
is up. Every frame, the ball moves by those two numbers.
Then bouncing is easy. When the ball hits the left or right wall,
ball.dx = -ball.dx turns it round. When it hits the top, or the
paddle, ball.dy = -ball.dy does. A number made negative is a
bounce. That's the whole of the physics.
Step 1 — the paddle
from kaypy import *
PADDLE_W = 120 # how wide the paddle is
PADDLE_H = 16 # and how tall
PADDLE_SPEED = 520 # how fast the paddle moves, in pixels per second
kaypy(width=800, height=600, background=[20, 24, 40])
# anchor("center") means pos() is the middle of the rectangle.
paddle = add([rect(PADDLE_W, PADDLE_H), anchor("center"),
pos(width() / 2, height() - 40), color(120, 220, 255)])
@onKeyDown("left")
def move_left():
paddle.move(-PADDLE_SPEED, 0)
keep_on_screen()
@onKeyDown("right")
def move_right():
paddle.move(PADDLE_SPEED, 0)
keep_on_screen()
# Holding the mouse down -- or a finger, on a phone -- moves the paddle
# towards it. Within 10 pixels counts as there.
@onMouseDown
def move_to_mouse():
if mousePos().x < paddle.pos.x - 10:
paddle.move(-PADDLE_SPEED, 0)
if mousePos().x > paddle.pos.x + 10:
paddle.move(PADDLE_SPEED, 0)
keep_on_screen()
def keep_on_screen():
paddle.pos.x = clamp(paddle.pos.x, PADDLE_W / 2, width() - PADDLE_W / 2)
Hold left or right and the paddle slides. Hold the mouse down somewhere and the paddle slides towards it. On a phone, a finger does the same thing. It stops at the edges.
rect(PADDLE_W, PADDLE_H)draws a plain rectangle, 120 pixels wide and 16 tall, where asprite()would draw a picture.color(120, 220, 255)makes it light blue.anchor("center")meanspos()is the middle of the rectangle, sopos(width() / 2, height() - 40)puts the paddle in the middle, 40 pixels up from the bottom.- This is how Bean ran in Bomb Dodge, with
paddlein place ofbean:@onKeyDownfor the arrows,@onMouseDownfor the mouse, andkeep_on_screen()after every move. clamp(paddle.pos.x, PADDLE_W / 2, width() - PADDLE_W / 2)keeps the middle of the paddle at least half a paddle (60 pixels) from each edge, so the paddle stops with its end touching the edge.
Break it. Take
keep_on_screen() out of move_left and hold left.
There's no error message. The paddle slides off the edge of the window and
keeps going: three seconds of holding left took it to x =
−1170. Hold right and it stops at the right-hand edge as it should, because
move_right still has its keep_on_screen().
Step 2 — a ball that moves itself
A ball appears near the top and sets off down and to the right. It bounces off all four sides of the window for ever. You can't do anything to it yet: the paddle is still there, but the ball goes straight through it.
from kaypy import *
PADDLE_W = 120 # how wide the paddle is
PADDLE_H = 16 # and how tall
PADDLE_SPEED = 520 # how fast the paddle moves, in pixels per second
BALL_SIZE = 16
kaypy(width=800, height=600, background=[20, 24, 40])
loadSound("wall", "sounds/small_beep.wav")
# anchor("center") means pos() is the middle of the rectangle.
paddle = add([rect(PADDLE_W, PADDLE_H), anchor("center"),
pos(width() / 2, height() - 40), color(120, 220, 255)])
# The ball carries its own speed: dx is how fast it goes across (minus is
# left) and dy is how fast it goes down (minus is up), in pixels per second.
ball = add([rect(BALL_SIZE, BALL_SIZE), anchor("center"),
pos(width() / 2, 100)])
ball.dx = 200
ball.dy = 300
@onKeyDown("left")
def move_left():
paddle.move(-PADDLE_SPEED, 0)
keep_on_screen()
@onKeyDown("right")
def move_right():
paddle.move(PADDLE_SPEED, 0)
keep_on_screen()
# Holding the mouse down -- or a finger, on a phone -- moves the paddle
# towards it. Within 10 pixels counts as there.
@onMouseDown
def move_to_mouse():
if mousePos().x < paddle.pos.x - 10:
paddle.move(-PADDLE_SPEED, 0)
if mousePos().x > paddle.pos.x + 10:
paddle.move(PADDLE_SPEED, 0)
keep_on_screen()
def keep_on_screen():
paddle.pos.x = clamp(paddle.pos.x, PADDLE_W / 2, width() - PADDLE_W / 2)
# Every frame the ball moves by its own speed, times dt() -- the time since
# the last frame -- so it goes as fast on a slow computer.
@onUpdate
def move_ball():
ball.pos.x = ball.pos.x + ball.dx * dt()
ball.pos.y = ball.pos.y + ball.dy * dt()
# The side walls: put it back inside, and turn it round.
if ball.pos.x < BALL_SIZE / 2:
ball.pos.x = BALL_SIZE / 2
ball.dx = -ball.dx
play("wall")
if ball.pos.x > width() - BALL_SIZE / 2:
ball.pos.x = width() - BALL_SIZE / 2
ball.dx = -ball.dx
play("wall")
# The top.
if ball.pos.y < BALL_SIZE / 2:
ball.pos.y = BALL_SIZE / 2
ball.dy = -ball.dy
play("wall")
# The bottom, for now. Step 3 takes it away.
if ball.pos.y > height() - BALL_SIZE / 2:
ball.pos.y = height() - BALL_SIZE / 2
ball.dy = -ball.dy
play("wall")
ball.dx = 200andball.dy = 300stick two numbers of our own onto the ball. A game object will carry any number you give it like this, under any name you like. These two are the ball's speed: 200 pixels a second to the right, and 300 a second down.@onUpdaterunsmove_ballevery frame.dt()is the time since the last frame, in seconds — about 0.017 — soball.dx * dt()is how far the ball goes across in this frame. That is the sum.move()was doing for you in Bomb Dodge.BALL_SIZE / 2is 8, half the ball. The ball'sposis its middle, so it touches the left wall when itsxis 8, and the right wall when itsxiswidth() - 8.
Each wall does two things. First it puts the ball back
inside: ball.pos.x = BALL_SIZE / 2. A ball moving 200 pixels a
second goes three or four pixels a frame, so by the time the
if notices, it is a little way into the wall. Then it
makes the speed negative, and the ball goes back the way it came.
The bottom is a wall too, but only for now, so you can watch the ball bounce. Step 3 takes it away and the paddle takes over.
Two things to break.
Take * dt() off both lines that move the ball. There's no
error message, and the ball goes mad: it moves the whole of
dx and dy every frame — 200 pixels across and
300 down — so it hits a wall nearly every frame and flickers from one side
of the window to the other. That is sixty times too fast.
dt() is what turns "pixels a second" into "pixels this
frame".
Move the two lines ball.dx = 200 and
ball.dy = 300 above ball = add(...). Nothing runs,
and the error is NameError: name 'ball' is not defined.
Python may add a guess after it, like Did you mean: 'all'? —
it's only a guess, and here it's wrong. The real problem is the order: you
can't put a number on the ball before there is a ball.
Step 3 — hitting it back
The bottom isn't a wall any more. Get the paddle under the ball and it bounces back up, with a beep. Miss it and the ball falls out of the window and is gone — nothing else happens yet.
from kaypy import *
PADDLE_W = 120 # how wide the paddle is
PADDLE_H = 16 # and how tall
PADDLE_SPEED = 520 # how fast the paddle moves, in pixels per second
BALL_SIZE = 16
kaypy(width=800, height=600, background=[20, 24, 40])
loadSound("hit", "sounds/beep.wav")
loadSound("wall", "sounds/small_beep.wav")
# anchor("center") means pos() is the middle of the rectangle.
paddle = add([rect(PADDLE_W, PADDLE_H), anchor("center"),
pos(width() / 2, height() - 40), color(120, 220, 255)])
# The ball carries its own speed: dx is how fast it goes across (minus is
# left) and dy is how fast it goes down (minus is up), in pixels per second.
ball = add([rect(BALL_SIZE, BALL_SIZE), anchor("center"),
pos(width() / 2, 100)])
ball.dx = 200
ball.dy = 300
@onKeyDown("left")
def move_left():
paddle.move(-PADDLE_SPEED, 0)
keep_on_screen()
@onKeyDown("right")
def move_right():
paddle.move(PADDLE_SPEED, 0)
keep_on_screen()
# Holding the mouse down -- or a finger, on a phone -- moves the paddle
# towards it. Within 10 pixels counts as there.
@onMouseDown
def move_to_mouse():
if mousePos().x < paddle.pos.x - 10:
paddle.move(-PADDLE_SPEED, 0)
if mousePos().x > paddle.pos.x + 10:
paddle.move(PADDLE_SPEED, 0)
keep_on_screen()
def keep_on_screen():
paddle.pos.x = clamp(paddle.pos.x, PADDLE_W / 2, width() - PADDLE_W / 2)
# Every frame the ball moves by its own speed, times dt() -- the time since
# the last frame -- so it goes as fast on a slow computer.
@onUpdate
def move_ball():
old_y = ball.pos.y
ball.pos.x = ball.pos.x + ball.dx * dt()
ball.pos.y = ball.pos.y + ball.dy * dt()
# The side walls: put it back inside, and turn it round.
if ball.pos.x < BALL_SIZE / 2:
ball.pos.x = BALL_SIZE / 2
ball.dx = -ball.dx
play("wall")
if ball.pos.x > width() - BALL_SIZE / 2:
ball.pos.x = width() - BALL_SIZE / 2
ball.dx = -ball.dx
play("wall")
# The top.
if ball.pos.y < BALL_SIZE / 2:
ball.pos.y = BALL_SIZE / 2
ball.dy = -ball.dy
play("wall")
# The paddle. `top` is where the middle of the ball is when it sits on
# the paddle, and `reach` is how far from the middle of the paddle it
# can be and still touch it. Did the ball go past `top` THIS frame,
# coming down, within reach? Checking where it was a frame ago as well
# as where it is now means a fast ball can't jump straight through.
top = paddle.pos.y - PADDLE_H / 2 - BALL_SIZE / 2
across = abs(ball.pos.x - paddle.pos.x)
reach = PADDLE_W / 2 + BALL_SIZE / 2
if old_y <= top and ball.pos.y > top and across < reach:
ball.pos.y = top
hit()
def hit():
ball.dy = -ball.dy
play("hit")
The paddle check is the hard part of this game, so here it is a piece at a time.
topis where the middle of the ball is when the ball is sitting on the paddle: the middle of the paddle, up half a paddle, up half a ball. The paddle's middle is 40 pixels up from the bottom, at 560, sotopis 560 − 8 − 8 = 544.acrossis how far the ball is, sideways, from the middle of the paddle.abs()makes a number positive, so it doesn't matter which side the ball is on: 30 to the left and 30 to the right are both 30.reachis how far across the ball can be and still touch the paddle: half a paddle and half a ball, 68.old_yis where the ball was at the start of this frame, before it moved.old_y <= top and ball.pos.y > topmeans "it was above the line, and now it is below it": the ball crossed the top of the paddle this frame. Withacross < reachas well, it crossed it where the paddle is.
Then ball.pos.y = top puts the ball back on top of the
paddle, the same way the walls put it back inside, and hit()
sends it back up.
Why not just ask "is the ball touching the paddle?" Because on a slow frame, a fast ball can go from just above the paddle to just below it, and be touching it on neither frame. Asking whether it crossed the line can't be fooled that way, however fast the ball gets.
Break it. Take
old_y <= top and out of the if. There's no
error message, and the game seems fine — until you miss. Let the ball go
past the end of the paddle, then slide the paddle under it while it is
still falling: the ball jumps back up onto the paddle and bounces, as if you
hadn't missed at all. Without old_y, the check is "is the ball
anywhere below the line?", and a ball you've already missed is.
Step 4 — points, missing, and playing again
Now there is a score at the top. Every hit is a point. Miss, and the screen shakes, you're told your score, and space or a click starts a new game.
from kaypy import *
PADDLE_W = 120 # how wide the paddle is
PADDLE_H = 16 # and how tall
PADDLE_SPEED = 520 # how fast the paddle moves, in pixels per second
BALL_SIZE = 16
kaypy(width=800, height=600, background=[20, 24, 40])
loadSound("hit", "sounds/beep.wav")
loadSound("wall", "sounds/small_beep.wav")
loadSound("miss", "sounds/thump.wav")
# 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".
score = 0
alive = True
@scene("game")
def game():
global score, alive
# A new game, so everything starts again.
score = 0
alive = True
# anchor("center") means pos() is the middle of the rectangle.
paddle = add([rect(PADDLE_W, PADDLE_H), anchor("center"),
pos(width() / 2, height() - 40), color(120, 220, 255)])
# The ball carries its own speed: dx is how fast it goes across (minus
# is left) and dy is how fast it goes down (minus is up).
ball = add([rect(BALL_SIZE, BALL_SIZE), anchor("center"),
pos(width() / 2, 100)])
ball.dx = 200
ball.dy = 300
score_label = add([text("0", size=56), pos(width() / 2, 60),
anchor("center")])
message = add([text("", size=26, width=560),
pos(width() / 2, height() / 2), anchor("center"), z(10)])
@onKeyDown("left")
def move_left():
paddle.move(-PADDLE_SPEED, 0)
keep_on_screen()
@onKeyDown("right")
def move_right():
paddle.move(PADDLE_SPEED, 0)
keep_on_screen()
# Holding the mouse down -- or a finger, on a phone -- moves the
# paddle towards it. Within 10 pixels counts as there.
@onMouseDown
def move_to_mouse():
if mousePos().x < paddle.pos.x - 10:
paddle.move(-PADDLE_SPEED, 0)
if mousePos().x > paddle.pos.x + 10:
paddle.move(PADDLE_SPEED, 0)
keep_on_screen()
def keep_on_screen():
paddle.pos.x = clamp(paddle.pos.x, PADDLE_W / 2,
width() - PADDLE_W / 2)
@onUpdate
def move_ball():
if not alive:
return
old_y = ball.pos.y
ball.pos.x = ball.pos.x + ball.dx * dt()
ball.pos.y = ball.pos.y + ball.dy * dt()
# The side walls: put it back inside, and turn it round.
if ball.pos.x < BALL_SIZE / 2:
ball.pos.x = BALL_SIZE / 2
ball.dx = -ball.dx
play("wall")
if ball.pos.x > width() - BALL_SIZE / 2:
ball.pos.x = width() - BALL_SIZE / 2
ball.dx = -ball.dx
play("wall")
# The top.
if ball.pos.y < BALL_SIZE / 2:
ball.pos.y = BALL_SIZE / 2
ball.dy = -ball.dy
play("wall")
# The paddle.
top = paddle.pos.y - PADDLE_H / 2 - BALL_SIZE / 2
across = abs(ball.pos.x - paddle.pos.x)
reach = PADDLE_W / 2 + BALL_SIZE / 2
if old_y <= top and ball.pos.y > top and across < reach:
ball.pos.y = top
hit()
# Gone past the bottom?
if ball.pos.y > height() + BALL_SIZE:
missed()
def hit():
global score
ball.dy = -ball.dy
play("hit")
score = score + 1
score_label.text = str(score)
def missed():
global alive
alive = False
play("miss")
shake(8)
ball.destroy()
message.text = (str(score)
+ " points -- press SPACE or click to play again")
# Once the game is over, space or a click starts a new one.
def again():
if not alive:
go("game")
onKeyPress("space", again)
onClick(again)
go("game")
This step is Bomb Dodge's step 4 all over again, so it is put together the same way.
- Everything is inside
@scene("game").go("game")at the bottom runs it, and running it again throws away everything — paddle, ball, words, handlers — and starts from the top. That's how playing again works. scoreandalivelive outside the scene, and every function that changes one saysglobalfirst.hitchangesscore;missedchangesalive.if ball.pos.y > height() + BALL_SIZE:waits until the ball is right out of the window, so you see it go.missed()setsalivetoFalse, destroys the ball, and puts up the message.again()only starts a new game whenaliveisFalse, so a click to move the paddle doesn't throw your game away.if not alive: returnat the top ofmove_ballstops the ball code once the game is over. See the second break below for why.
Two things to break.
Take global score out of hit. The game starts
as normal, and the first time the ball touches the paddle it stops with
UnboundLocalError: cannot access local variable 'score' where it is
not associated with a value. Without global, a function
that changes score gets a new score of its own,
which has no value yet.
Take out if not alive: and the return under
it. There's no error, but when you miss, the miss sound plays over and over
— 33 times in the first second — and the screen won't stop shaking. A
destroyed ball still has a pos, so move_ball keeps
moving it, finds it below the window every frame, and calls
missed() every frame.
Step 5 — faster every hit
Every hit sends the ball back up a little faster than it came down, so the game gets harder the longer you last. It stops getting faster at 800 pixels a second.
from kaypy import *
PADDLE_W = 120 # how wide the paddle is
PADDLE_H = 16 # and how tall
PADDLE_SPEED = 520 # how fast the paddle moves, in pixels per second
BALL_SIZE = 16
BALL_GAIN = 25 # how much faster every hit makes the ball
BALL_FASTEST = 800 # ...and the fastest it ever goes
kaypy(width=800, height=600, background=[20, 24, 40])
loadSound("hit", "sounds/beep.wav")
loadSound("wall", "sounds/small_beep.wav")
loadSound("miss", "sounds/thump.wav")
# 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".
score = 0
alive = True
@scene("game")
def game():
global score, alive
# A new game, so everything starts again.
score = 0
alive = True
# anchor("center") means pos() is the middle of the rectangle.
paddle = add([rect(PADDLE_W, PADDLE_H), anchor("center"),
pos(width() / 2, height() - 40), color(120, 220, 255)])
# The ball carries its own speed: dx is how fast it goes across (minus
# is left) and dy is how fast it goes down (minus is up).
ball = add([rect(BALL_SIZE, BALL_SIZE), anchor("center"),
pos(width() / 2, 100)])
ball.dx = 200
ball.dy = 300
score_label = add([text("0", size=56), pos(width() / 2, 60),
anchor("center")])
message = add([text("", size=26, width=560),
pos(width() / 2, height() / 2), anchor("center"), z(10)])
@onKeyDown("left")
def move_left():
paddle.move(-PADDLE_SPEED, 0)
keep_on_screen()
@onKeyDown("right")
def move_right():
paddle.move(PADDLE_SPEED, 0)
keep_on_screen()
# Holding the mouse down -- or a finger, on a phone -- moves the
# paddle towards it. Within 10 pixels counts as there.
@onMouseDown
def move_to_mouse():
if mousePos().x < paddle.pos.x - 10:
paddle.move(-PADDLE_SPEED, 0)
if mousePos().x > paddle.pos.x + 10:
paddle.move(PADDLE_SPEED, 0)
keep_on_screen()
def keep_on_screen():
paddle.pos.x = clamp(paddle.pos.x, PADDLE_W / 2,
width() - PADDLE_W / 2)
@onUpdate
def move_ball():
if not alive:
return
old_y = ball.pos.y
ball.pos.x = ball.pos.x + ball.dx * dt()
ball.pos.y = ball.pos.y + ball.dy * dt()
# The side walls: put it back inside, and turn it round.
if ball.pos.x < BALL_SIZE / 2:
ball.pos.x = BALL_SIZE / 2
ball.dx = -ball.dx
play("wall")
if ball.pos.x > width() - BALL_SIZE / 2:
ball.pos.x = width() - BALL_SIZE / 2
ball.dx = -ball.dx
play("wall")
# The top.
if ball.pos.y < BALL_SIZE / 2:
ball.pos.y = BALL_SIZE / 2
ball.dy = -ball.dy
play("wall")
# The paddle.
top = paddle.pos.y - PADDLE_H / 2 - BALL_SIZE / 2
across = abs(ball.pos.x - paddle.pos.x)
reach = PADDLE_W / 2 + BALL_SIZE / 2
if old_y <= top and ball.pos.y > top and across < reach:
ball.pos.y = top
hit()
# Gone past the bottom?
if ball.pos.y > height() + BALL_SIZE:
missed()
def hit():
global score
# A little faster than it came down, but never faster than
# BALL_FASTEST. Then make it negative, which sends it back up.
speed = ball.dy + BALL_GAIN
if speed > BALL_FASTEST:
speed = BALL_FASTEST
ball.dy = -speed
play("hit")
score = score + 1
score_label.text = str(score)
def missed():
global alive
alive = False
play("miss")
shake(8)
ball.destroy()
message.text = (str(score)
+ " points -- press SPACE or click to play again")
# Once the game is over, space or a click starts a new one.
def again():
if not alive:
go("game")
onKeyPress("space", again)
onClick(again)
go("game")
All the change is in hit.
- The ball is coming down when it hits the paddle, so
ball.dyis positive: 300 to begin with.speed = ball.dy + BALL_GAINmakes that 325. if speed > BALL_FASTEST:stops it there once it reaches 800. Twenty hits takes it from 300 to 800.ball.dy = -speedmakes it negative, which sends the ball back up — 25 faster than it came down. The top wall flips it again, and it comes back down at the new speed.- Only
dy, the up-and-down speed, gets faster. Ifdxgrew too, the ball would soon be going sideways faster than the paddle can follow. A game you can't win isn't harder, it's broken.
Break it. Leave out the minus:
ball.dy = speed. There's no error message, and the ball doesn't
bounce. It lands on the paddle and slides along it, still going down, so
every frame it crosses top again and scores again: four points
in a tenth of a second, until it slides off the end and falls.
Step 6 — the whole game
This is the game exactly as it is on the games page. It adds three things to step 5:
- A serve. The ball starts still, and a second and a
half later
serve()sets it going: always down, and sideways a random amount, left or right.rand(150, 250)picks the sideways speed, andif chance(0.5):— true half the time — makes it go left instead. Every game starts differently. - A best score that is remembered.
getData(BEST_KEY, 0)reads the best score saved last time, or 0 if there isn't one, andsetDatasaves a new one when you beat it.BEST_KEYis"paddle_ball_best"rather than just"best", because every game on a website shares the same storage. - A message at the start saying how to move, which goes away at your first hit, and "1 point" rather than "1 points".
It also has all the numbers worth changing in one settings block near the top, and comments that explain why each part is there.
"""Paddle Ball -- keep the ball up. Pong, for one.
left and right arrows move your paddle
or hold the mouse (or a finger) where you want the paddle to go
The ball bounces off the top and the sides. Hit it back up with your paddle
and that is a point. Every hit makes the ball a little faster. Miss it and
it is game over. Your best score is remembered.
HOW THIS FILE IS PUT TOGETHER
1. the window and the sounds
2. the settings
3. the game scene: the paddle, the ball, bouncing, points, missing
4. start the game
There are no pictures at all. The paddle and the ball are rectangles, and
the ball is a game object that carries its own speed: ball.dx is how fast
it is going across, and ball.dy is how fast it is going down. Bouncing is
just making one of them negative. After this, try Spiral Storm, and then
Pong.
"""
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("miss", "sounds/thump.wav")
# -------------------------------------------------------------- 2. settings
#
# Everything you would want to change is here. Change one at a time and
# play it.
PADDLE_W = 120 # how wide the paddle is
PADDLE_H = 16 # and how tall
PADDLE_SPEED = 520 # how fast the paddle moves, in pixels per second
BALL_SIZE = 16
BALL_START = 300 # how fast the ball comes down at the start
BALL_GAIN = 25 # ...how much faster every hit makes it
BALL_FASTEST = 800 # ...and the fastest it ever goes
# The name the best score is saved under. Every game on a website shares
# the same storage, so the game's own name goes in it.
BEST_KEY = "paddle_ball_best"
# ------------------------------------------------------- 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".
score = 0
alive = True
@scene("game")
def game():
global score, alive
# A new game, so everything starts again.
score = 0
alive = True
best = getData(BEST_KEY, 0)
# anchor("center") means pos() is the middle of the rectangle.
paddle = add([rect(PADDLE_W, PADDLE_H), anchor("center"),
pos(width() / 2, height() - 40), color(120, 220, 255)])
# The ball starts still. serve() below sets it going.
ball = add([rect(BALL_SIZE, BALL_SIZE), anchor("center"),
pos(width() / 2, 100)])
ball.dx = 0
ball.dy = 0
score_label = add([text("0", size=56), pos(width() / 2, 60),
anchor("center")])
best_label = add([text("best " + str(best), size=18), pos(12, 12),
opacity(0.75)])
# width=560 wraps a long message onto more than one line.
message = add([text("Left and right arrows, or hold the mouse where "
"you want your paddle. Keep the ball up!",
size=26, width=560),
pos(width() / 2, height() / 2), anchor("center"), z(10)])
# ------------------------------------------------------- the paddle
@onKeyDown("left")
def move_left():
paddle.move(-PADDLE_SPEED, 0)
keep_on_screen()
@onKeyDown("right")
def move_right():
paddle.move(PADDLE_SPEED, 0)
keep_on_screen()
# Holding the mouse down -- or a finger, on a phone -- moves the
# paddle towards it. Within 10 pixels counts as there.
@onMouseDown
def move_to_mouse():
if mousePos().x < paddle.pos.x - 10:
paddle.move(-PADDLE_SPEED, 0)
if mousePos().x > paddle.pos.x + 10:
paddle.move(PADDLE_SPEED, 0)
keep_on_screen()
def keep_on_screen():
paddle.pos.x = clamp(paddle.pos.x, PADDLE_W / 2,
width() - PADDLE_W / 2)
# --------------------------------------------------------- serving
#
# Sideways a random amount, left or right, and always down.
def serve():
ball.dx = rand(150, 250)
if chance(0.5):
ball.dx = -ball.dx
ball.dy = BALL_START
wait(1.5, serve)
# -------------------------------------------------------- the ball
#
# Every frame the ball moves by its own speed, times dt() -- the time
# since the last frame -- so it goes as fast on a slow computer.
@onUpdate
def move_ball():
if not alive:
return
old_y = ball.pos.y
ball.pos.x = ball.pos.x + ball.dx * dt()
ball.pos.y = ball.pos.y + ball.dy * dt()
# The side walls: put it back inside, and turn it round.
if ball.pos.x < BALL_SIZE / 2:
ball.pos.x = BALL_SIZE / 2
ball.dx = -ball.dx
play("wall")
if ball.pos.x > width() - BALL_SIZE / 2:
ball.pos.x = width() - BALL_SIZE / 2
ball.dx = -ball.dx
play("wall")
# The top.
if ball.pos.y < BALL_SIZE / 2:
ball.pos.y = BALL_SIZE / 2
ball.dy = -ball.dy
play("wall")
# The paddle. `top` is where the middle of the ball is when it
# sits on the paddle, and `reach` is how far from the middle of the
# paddle it can be and still touch it. Did the ball go past `top`
# THIS frame, coming down, within reach? Checking where it was a
# frame ago as well as where it is now means a fast ball can't
# jump straight through.
top = paddle.pos.y - PADDLE_H / 2 - BALL_SIZE / 2
across = abs(ball.pos.x - paddle.pos.x)
reach = PADDLE_W / 2 + BALL_SIZE / 2
if old_y <= top and ball.pos.y > top and across < reach:
ball.pos.y = top
hit()
# Gone past the bottom?
if ball.pos.y > height() + BALL_SIZE:
missed()
# ------------------------------------------------------- hitting it
def hit():
global score
# A little faster than it came down, but never faster than
# BALL_FASTEST. Then make it negative, which sends it back up.
# Only the up-and-down speed grows: if the sideways speed grew too,
# the ball would soon cross the window faster than the paddle can.
speed = ball.dy + BALL_GAIN
if speed > BALL_FASTEST:
speed = BALL_FASTEST
ball.dy = -speed
play("hit")
score = score + 1
score_label.text = str(score)
message.text = ""
# -------------------------------------------------------- missing it
def missed():
global alive
alive = False
play("miss")
shake(8)
ball.destroy()
if score > best:
setData(BEST_KEY, score)
best_label.text = "best " + str(score)
if score == 1:
message.text = "1 point -- press SPACE or click to play again"
else:
message.text = (str(score)
+ " points -- press SPACE or click to play again")
# Once the game is over, space or a click starts a new one.
def again():
if not alive:
go("game")
onKeyPress("space", again)
onClick(again)
# ------------------------------------------------------- 4. start the game
go("game")
Break it. Write
wait(1.5, serve()), with brackets after serve.
There's no error message, and the ball sets off the moment the game starts,
with no pause at all. The brackets run serve straight
away, and hand wait what it gave back, which is nothing.
wait wants the function itself, to run later — so no
brackets.
Make it yours
Open the whole game in the playground → It goes straight into the editor, ready to change and run.
- Change the numbers. Make
PADDLE_W60 and it's a lot harder; makeBALL_GAIN60 and it gets fast in a hurry. - A ball that goes red. Give the ball
color(255, 255, 255)in its list — white, which is how it looks already. Then inhit, after the speed is worked out,if speed == BALL_FASTEST:withball.color = REDunder it, and the ball turns red when it's going its fastest. Without thecolor()in its list, the ball has no colour to change, and staying white is all it does. - A picture for a ball.
loadSprite("apple", "images/apple.png")near the top, andsprite("apple"), scale(0.4)in place ofrect(BALL_SIZE, BALL_SIZE). That makes the apple about 24 pixels across, so makeBALL_SIZE24 to match, or the apple goes a little way into each wall before it bounces. - Now play Spiral Storm. Spiral Storm is a break from paddles: a bullet hell, where a hundred bullets fly by themselves and you dodge them all. After that, Pong is this game turned on its side, with a second paddle that the computer moves, and a ball that goes off at an angle when it hits the end of a paddle.