Building Bomb Dodge
Play it first → Bombs fall from the sky and Bean runs left and right along the grass to stay out of the way. Every bomb that lands on the grass is a point. The more points you have, the faster they fall and the more of them there are. One hit and it's over.
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 only two pictures, Bean and the bomb.
It is the smallest game on the site that writes functions of its own. If you haven't got to functions yet, build Ghost Chase first. After this one, and after dictionaries, try Goblin Hit, then Log Hacker, then Paddle Ball, then Spiral Storm, and then Pong.
The one big idea: kaypy calls you
You never write a loop that checks the keyboard sixty times a second.
You write a function, and tell kaypy when to run it:
@onKeyDown("left") says "run this every frame the left arrow is
held", and wait(1, drop_bomb) says "run this in a second". The
game is a handful of small functions like that, each one doing a single
job.
There is no gravity, and nothing is solid. Nothing has an
area() or a body(). Each bomb moves itself a little
way down every frame, and asks two questions: am I close to Bean?
and have I reached the grass? That's the whole game. Everything else
is keeping score.
Step 1 — Bean runs
from kaypy import *
GROUND = 60 # how tall the strip of grass is
BEAN_SPEED = 420 # how fast Bean runs, in pixels per second
kaypy(width=800, height=600, background=[121, 189, 220])
loadSprite("bean", "images/bean.png")
# The grass.
add([rect(width(), GROUND), pos(0, height() - GROUND), color(96, 156, 76)])
# anchor("bot") means pos() is where Bean's feet are, so Bean stands on
# the grass.
bean = add([sprite("bean"), pos(width() / 2, height() - GROUND),
anchor("bot")])
@onKeyDown("left")
def run_left():
bean.move(-BEAN_SPEED, 0)
keep_on_screen()
@onKeyDown("right")
def run_right():
bean.move(BEAN_SPEED, 0)
keep_on_screen()
# Holding the mouse down -- or a finger, on a phone -- runs Bean
# towards it. Within 10 pixels counts as there, and Bean stops.
@onMouseDown
def run_to_mouse():
if mousePos().x < bean.pos.x - 10:
bean.move(-BEAN_SPEED, 0)
if mousePos().x > bean.pos.x + 10:
bean.move(BEAN_SPEED, 0)
keep_on_screen()
def keep_on_screen():
# 32 is half of Bean's width.
bean.pos.x = clamp(bean.pos.x, 32, width() - 32)
Hold left or right and Bean runs. Hold the mouse down somewhere and Bean runs towards it. On a phone, a finger does the same thing.
anchor("bot")makespos()mean the bottom middle of Bean, its feet. Put Bean's feet atheight() - GROUND, the top of the grass, and Bean stands on it. No gravity is needed, because Bean never jumps.@onKeyDown("left")runsrun_leftevery frame the left arrow is held down, about sixty times a second, so Bean keeps running until you let go.@onKeyDown("right")does the same for the right arrow.bean.move(-BEAN_SPEED, 0)moves Bean left atBEAN_SPEEDpixels per second. kaypy works out how far that is for one frame, so Bean runs at the same speed on a fast computer and a slow one.@onMouseDownrunsrun_to_mouseevery frame the mouse button is held, or a finger is on the screen.mousePos().xis where it is, across the window. More than 10 pixels to the left of Bean and Bean runs left; more than 10 to the right and Bean runs right. Within 10 pixels counts as there, and Bean stops.
keep_on_screen() stops Bean at the edges.
All three handlers call it straight after moving.
clamp(bean.pos.x, 32, width() - 32) gives back
bean.pos.x, unless it is less than 32 (then it gives 32) or more
than 768 (then it gives 768). Bean is 64 pixels wide, so 32 is half of Bean,
and Bean stops with its side touching the edge. It is a function of its own
so that the three handlers can share it rather than each having a copy.
Two things to break.
Take keep_on_screen() out of run_left and hold
left. There's no error message. Bean runs off the edge of the window and
keeps going: three seconds of holding left took it to x = −853.
Hold right for as long to bring it back.
Write @onKeyDown without the ("left"). There's
no error message either, and the left arrow does nothing at all. kaypy
needs to be told which key; without it, the function is never
connected to the keyboard.
Step 2 — bombs fall
Every 0.9 seconds a bomb appears just above the window, somewhere along the top, and falls, spinning as it goes.
from kaypy import *
GROUND = 60 # how tall the strip of grass is
BEAN_SPEED = 420 # how fast Bean runs, in pixels per second
BOMB_SIZE = 0.5 # how big a bomb is: 1 is the whole picture
FALL_SPEED = 200 # how fast the bombs fall, in pixels per second
WAIT = 0.9 # seconds between bombs
kaypy(width=800, height=600, background=[121, 189, 220])
loadSprite("bean", "images/bean.png")
loadSprite("bomb", "images/bomb.png")
# The grass.
add([rect(width(), GROUND), pos(0, height() - GROUND), color(96, 156, 76)])
# anchor("bot") means pos() is where Bean's feet are, so Bean stands on
# the grass. z(5) draws Bean in front of the bombs.
bean = add([sprite("bean"), pos(width() / 2, height() - GROUND),
anchor("bot"), z(5)])
@onKeyDown("left")
def run_left():
bean.move(-BEAN_SPEED, 0)
keep_on_screen()
@onKeyDown("right")
def run_right():
bean.move(BEAN_SPEED, 0)
keep_on_screen()
# Holding the mouse down -- or a finger, on a phone -- runs Bean
# towards it. Within 10 pixels counts as there, and Bean stops.
@onMouseDown
def run_to_mouse():
if mousePos().x < bean.pos.x - 10:
bean.move(-BEAN_SPEED, 0)
if mousePos().x > bean.pos.x + 10:
bean.move(BEAN_SPEED, 0)
keep_on_screen()
def keep_on_screen():
# 32 is half of Bean's width.
bean.pos.x = clamp(bean.pos.x, 32, width() - 32)
def drop_bomb():
# Somewhere along the top, just above the window. rotate(0) lets it
# spin.
x = rand(30, width() - 30)
add([sprite("bomb"), pos(x, -40), anchor("center"),
scale(BOMB_SIZE), rotate(0), "bomb"])
# And the next one, a little later.
wait(WAIT, drop_bomb)
# The first bomb, a second after the game starts.
wait(1, drop_bomb)
@onUpdate("bomb")
def fall(bomb):
bomb.move(0, FALL_SPEED)
bomb.angle = bomb.angle + 90 * dt()
# Reached the grass? Throw it away.
if bomb.pos.y > height() - GROUND:
bomb.destroy()
One bomb leads to the next. wait(1,
drop_bomb) at the bottom runs drop_bomb once, a second
after the game starts. drop_bomb makes a bomb, and its last line
is wait(WAIT, drop_bomb): run me again in 0.9 seconds. So every
bomb arranges the next one, and they keep coming without a loop anywhere.
Making a bomb. rand(30, width() - 30) picks
a random x across the top, and pos(x, -40) puts the
bomb just above the window, so it falls into view rather than popping into
it. scale(BOMB_SIZE) draws the picture at half size. The tag
"bomb" is a name for every bomb at once.
Falling. @onUpdate("bomb") runs
fall once a frame for every object tagged
"bomb", handing each one in as bomb. Each one moves
down at FALL_SPEED and turns 90 degrees a second. When it gets
to the grass it is destroyed. Without that, the game would keep every bomb it
ever made, falling for ever out of sight.
Bean now has z(5). Things are drawn in the order they were
added, so bombs, added after Bean, would be drawn over it. A higher
z is drawn in front.
Two things to break.
Put brackets after drop_bomb in the last line:
wait(WAIT, drop_bomb()). A second in, the game stops with
RecursionError: maximum recursion depth exceeded
Without brackets, wait() is handed the function, to
run later. With them, Python runs drop_bomb right now, to find
out what to hand over. That drop_bomb gets to its last line and
runs drop_bomb right now too, and so on, without ever stopping,
until Python gives up. "Recursion" means a function running itself.
Take rotate(0) out of the bomb's list and the first bomb to
appear stops the game with
AttributeError: GameObj has no attribute 'angle' — did you forget a component that provides it?
A bomb only has an angle if it has
rotate(). The message says exactly that, which is worth
remembering: "has no attribute" in kaypy nearly always means a component is
missing from the list.
Bombs fall straight through Bean for now. That's step 4.
Step 3 — points
Every bomb that reaches the grass is a point, and the score goes at the top of the window.
from kaypy import *
GROUND = 60 # how tall the strip of grass is
BEAN_SPEED = 420 # how fast Bean runs, in pixels per second
BOMB_SIZE = 0.5 # how big a bomb is: 1 is the whole picture
FALL_SPEED = 200 # how fast the bombs fall, in pixels per second
WAIT = 0.9 # seconds between bombs
kaypy(width=800, height=600, background=[121, 189, 220])
loadSprite("bean", "images/bean.png")
loadSprite("bomb", "images/bomb.png")
# The grass.
add([rect(width(), GROUND), pos(0, height() - GROUND), color(96, 156, 76)])
# anchor("bot") means pos() is where Bean's feet are, so Bean stands on
# the grass. z(5) draws Bean in front of the bombs.
bean = add([sprite("bean"), pos(width() / 2, height() - GROUND),
anchor("bot"), z(5)])
score_label = add([text("0", size=48), pos(width() / 2, 40),
anchor("center"), z(10)])
score = 0
@onKeyDown("left")
def run_left():
bean.move(-BEAN_SPEED, 0)
keep_on_screen()
@onKeyDown("right")
def run_right():
bean.move(BEAN_SPEED, 0)
keep_on_screen()
# Holding the mouse down -- or a finger, on a phone -- runs Bean
# towards it. Within 10 pixels counts as there, and Bean stops.
@onMouseDown
def run_to_mouse():
if mousePos().x < bean.pos.x - 10:
bean.move(-BEAN_SPEED, 0)
if mousePos().x > bean.pos.x + 10:
bean.move(BEAN_SPEED, 0)
keep_on_screen()
def keep_on_screen():
# 32 is half of Bean's width.
bean.pos.x = clamp(bean.pos.x, 32, width() - 32)
def drop_bomb():
# Somewhere along the top, just above the window. rotate(0) lets it
# spin.
x = rand(30, width() - 30)
add([sprite("bomb"), pos(x, -40), anchor("center"),
scale(BOMB_SIZE), rotate(0), "bomb"])
# And the next one, a little later.
wait(WAIT, drop_bomb)
# The first bomb, a second after the game starts.
wait(1, drop_bomb)
@onUpdate("bomb")
def fall(bomb):
global score
bomb.move(0, FALL_SPEED)
bomb.angle = bomb.angle + 90 * dt()
# Reached the grass? That is a point.
if bomb.pos.y > height() - GROUND:
bomb.destroy()
score = score + 1
score_label.text = str(score)
Three things were added: score = 0, a
text() object to show it, and three lines in fall
where a bomb reaches the grass. score_label.text = str(score)
changes what the label says. str() turns the number into text,
because text has to be text.
fall now changes score, so it says
global score first. score was made outside the
function, and a function that changes a variable from outside itself has to
say global. Without it, Python decides score is a
new variable belonging to fall, which has no value yet when
fall tries to add 1 to it. Leave it out and nothing goes wrong
until the first bomb lands — nearly four seconds in — and then:
UnboundLocalError: cannot access local variable 'score' where it is not associated with a value
Stand still and watch: bombs go straight through Bean, and you get the points anyway. Not much of a game yet.
Step 4 — getting hit, and playing again
A bomb that gets close enough to Bean now blows up, and the game is over. Press space or click to play again.
Playing again means throwing away every bomb and building everything
fresh. That's what a scene is for. Everything from the
grass 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 and builds it from
the start.
from kaypy import *
GROUND = 60 # how tall the strip of grass is
BEAN_SPEED = 420 # how fast Bean runs, in pixels per second
BOMB_SIZE = 0.5 # how big a bomb is: 1 is the whole picture
HIT_DISTANCE = 42 # how close a bomb has to get to Bean to hit it
FALL_SPEED = 200 # how fast the bombs fall, in pixels per second
WAIT = 0.9 # seconds between bombs
kaypy(width=800, height=600, background=[121, 189, 220])
loadSprite("bean", "images/bean.png")
loadSprite("bomb", "images/bomb.png")
loadSound("boom", "sounds/small_boom.wav")
score = 0
alive = True
@scene("game")
def game():
global score, alive
# A new game, so everything starts again.
score = 0
alive = True
# The grass.
add([rect(width(), GROUND), pos(0, height() - GROUND),
color(96, 156, 76)])
# anchor("bot") means pos() is where Bean's feet are, so Bean stands
# on the grass. z(5) draws Bean in front of the bombs.
bean = add([sprite("bean"), pos(width() / 2, height() - GROUND),
anchor("bot"), z(5)])
score_label = add([text("0", size=48), pos(width() / 2, 40),
anchor("center"), z(10)])
# width=560 wraps a long message onto more than one line.
message = add([text("", size=26, width=560),
pos(width() / 2, height() / 2), anchor("center"), z(10)])
@onKeyDown("left")
def run_left():
bean.move(-BEAN_SPEED, 0)
keep_on_screen()
@onKeyDown("right")
def run_right():
bean.move(BEAN_SPEED, 0)
keep_on_screen()
# Holding the mouse down -- or a finger, on a phone -- runs Bean
# towards it. Within 10 pixels counts as there, and Bean stops.
@onMouseDown
def run_to_mouse():
if mousePos().x < bean.pos.x - 10:
bean.move(-BEAN_SPEED, 0)
if mousePos().x > bean.pos.x + 10:
bean.move(BEAN_SPEED, 0)
keep_on_screen()
def keep_on_screen():
# 32 is half of Bean's width.
bean.pos.x = clamp(bean.pos.x, 32, width() - 32)
def drop_bomb():
# Game over: no bomb, and no wait() for the next one either, so
# the bombs stop here.
if not alive:
return
# Somewhere along the top, just above the window. rotate(0) lets it
# spin.
x = rand(30, width() - 30)
add([sprite("bomb"), pos(x, -40), anchor("center"),
scale(BOMB_SIZE), rotate(0), "bomb"])
# And the next one, a little later.
wait(WAIT, drop_bomb)
# The first bomb, a second after the game starts.
wait(1, drop_bomb)
@onUpdate("bomb")
def fall(bomb):
global score
if not alive:
return
bomb.move(0, FALL_SPEED)
bomb.angle = bomb.angle + 90 * dt()
# Close enough to Bean to hit it? 27 pixels up from Bean's feet is
# the middle of Bean.
middle = vec2(bean.pos.x, bean.pos.y - 27)
if bomb.pos.dist(middle) < HIT_DISTANCE:
hit(bomb)
return
# Reached the grass? That is a point.
if bomb.pos.y > height() - GROUND:
bomb.destroy()
score = score + 1
score_label.text = str(score)
def hit(bomb):
global alive
alive = False
play("boom")
shake(12)
addKaboom(bomb.pos)
# Bean goes, and so does every bomb still falling.
bean.destroy()
destroyAll("bomb")
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")
Close enough. bean.pos is Bean's feet, so
27 pixels up, half of Bean's height, is the middle of Bean.
bomb.pos.dist(middle) is how far the middle of the bomb is from
the middle of Bean, in a straight line. Less than HIT_DISTANCE,
42 pixels, is a hit. Make HIT_DISTANCE bigger and the bombs get
you from further away; smaller, and you can get away with near misses.
One hit. hit() does everything that
happens when you lose: a sound, a shake, an explosion where the bomb was,
and then Bean and every bomb are destroyed. destroyAll("bomb")
gets rid of everything tagged "bomb" in one line.
alive is what stops the game. Once it is
False, drop_bomb returns straight away, before
making a bomb and before its wait(), so the chain of
bombs ends there. fall checks it too. With every bomb destroyed
it never has anything to do, but it keeps the rule simple: once
alive is False, nothing falls. The key handlers
don't need to check: Bean has been destroyed, so there is nothing left on
the screen for them to move. And again only starts a new game
when you are not alive, so clicking to move Bean doesn't restart
the game you are in the middle of.
Starting again clears everything. go("game")
throws away every object, every handler and every wait() still
waiting, before it builds the scene again. So a new game never has the old
game's bombs, or a second chain of them, still going.
The variables live outside the scene.
score and alive are made at the top, and the scene
sets them back to their starting values every time it is built. Leave out
score = 0 inside the scene and the next game carries on from the
last one's score. The label says 0 at the start, because it is made new, but
if the last game ended on 9 points, the first bomb to land makes it 10.
Take out destroyAll("bomb")
and there's no error. The bombs that were still falling stop where they are,
because fall does nothing once alive is
False, and hang in the air over the game-over message.
Step 5 — harder as you go
The fall speed and the time between bombs were fixed numbers. Now each one is worked out from the score by a small function.
from kaypy import *
GROUND = 60 # how tall the strip of grass is
BEAN_SPEED = 420 # how fast Bean runs, in pixels per second
BOMB_SIZE = 0.5 # how big a bomb is: 1 is the whole picture
HIT_DISTANCE = 42 # how close a bomb has to get to Bean to hit it
FALL_START = 200 # how fast the bombs fall, to begin with
FALL_FASTEST = 520 # ...the fastest they ever fall
FALL_GAIN = 8 # ...and how much faster each point makes them
WAIT_START = 0.9 # seconds between bombs, to begin with
WAIT_SHORTEST = 0.3 # ...the shortest wait there ever is
WAIT_DROP = 0.015 # ...and how much shorter each point makes it
kaypy(width=800, height=600, background=[121, 189, 220])
loadSprite("bean", "images/bean.png")
loadSprite("bomb", "images/bomb.png")
loadSound("boom", "sounds/small_boom.wav")
score = 0
def fall_speed():
speed = FALL_START + score * FALL_GAIN
if speed > FALL_FASTEST:
speed = FALL_FASTEST
return speed
def bomb_wait():
seconds = WAIT_START - score * WAIT_DROP
if seconds < WAIT_SHORTEST:
seconds = WAIT_SHORTEST
return seconds
alive = True
@scene("game")
def game():
global score, alive
# A new game, so everything starts again.
score = 0
alive = True
# The grass.
add([rect(width(), GROUND), pos(0, height() - GROUND),
color(96, 156, 76)])
# anchor("bot") means pos() is where Bean's feet are, so Bean stands
# on the grass. z(5) draws Bean in front of the bombs.
bean = add([sprite("bean"), pos(width() / 2, height() - GROUND),
anchor("bot"), z(5)])
score_label = add([text("0", size=48), pos(width() / 2, 40),
anchor("center"), z(10)])
# width=560 wraps a long message onto more than one line.
message = add([text("", size=26, width=560),
pos(width() / 2, height() / 2), anchor("center"), z(10)])
@onKeyDown("left")
def run_left():
bean.move(-BEAN_SPEED, 0)
keep_on_screen()
@onKeyDown("right")
def run_right():
bean.move(BEAN_SPEED, 0)
keep_on_screen()
# Holding the mouse down -- or a finger, on a phone -- runs Bean
# towards it. Within 10 pixels counts as there, and Bean stops.
@onMouseDown
def run_to_mouse():
if mousePos().x < bean.pos.x - 10:
bean.move(-BEAN_SPEED, 0)
if mousePos().x > bean.pos.x + 10:
bean.move(BEAN_SPEED, 0)
keep_on_screen()
def keep_on_screen():
# 32 is half of Bean's width.
bean.pos.x = clamp(bean.pos.x, 32, width() - 32)
def drop_bomb():
# Game over: no bomb, and no wait() for the next one either, so
# the bombs stop here.
if not alive:
return
# Somewhere along the top, just above the window. rotate(0) lets it
# spin.
x = rand(30, width() - 30)
add([sprite("bomb"), pos(x, -40), anchor("center"),
scale(BOMB_SIZE), rotate(0), "bomb"])
# And the next one, a little later.
wait(bomb_wait(), drop_bomb)
# The first bomb, a second after the game starts.
wait(1, drop_bomb)
@onUpdate("bomb")
def fall(bomb):
global score
if not alive:
return
bomb.move(0, fall_speed())
bomb.angle = bomb.angle + 90 * dt()
# Close enough to Bean to hit it? 27 pixels up from Bean's feet is
# the middle of Bean.
middle = vec2(bean.pos.x, bean.pos.y - 27)
if bomb.pos.dist(middle) < HIT_DISTANCE:
hit(bomb)
return
# Reached the grass? That is a point.
if bomb.pos.y > height() - GROUND:
bomb.destroy()
score = score + 1
score_label.text = str(score)
def hit(bomb):
global alive
alive = False
play("boom")
shake(12)
addKaboom(bomb.pos)
# Bean goes, and so does every bomb still falling.
bean.destroy()
destroyAll("bomb")
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")
Take fall_speed(). It starts at FALL_START, 200
pixels a second, and adds 8 for every point. At 10 points that's
200 + 10 * 8, which is 280. At 50 points it would be 600, but
the if stops it at FALL_FASTEST, 520.
bomb_wait() works the same way: 0.9 seconds between bombs to
begin with, 0.015 less for every point, and never less than 0.3. It is
called in drop_bomb's wait(), so each bomb works
out how long until the next one from the score at the moment it falls.
Those stops matter. Without them the bombs keep getting faster and closer together until nobody could dodge them. With them, the game gets hard by about 40 points and then stays there, which is a game people keep playing.
The two functions are outside the scene, near the top, with the other
settings. They only read score, so they don't need
global. Only a function that changes a variable does.
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 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"bomb_dodge_best"rather than just"best", because every game on a website shares the same storage, and another game saving its own"best"would overwrite yours. - A message at the start saying how to move, which goes away when you score your first point.
- "1 point", not "1 points". An
ifpicks the right word.
It also has all the numbers worth changing in one settings block near the top, and comments that explain why each part is there.
"""Bomb Dodge -- bombs fall from the sky. Don't get hit.
left and right arrows move
or hold the mouse (or a finger) where you want Bean to go
Every bomb that lands on the grass instead of on Bean is a point. The more
points you have, the faster the bombs fall and the more of them there are.
One hit and it is game over. Your best score is remembered.
HOW THIS FILE IS PUT TOGETHER
1. the window, the pictures and the sound
2. the settings
3. how hard the game is
4. the game scene: Bean, the bombs, points, getting hit
5. start the game
This is the smallest game on the site, and a good one to start from. Swap
the bomb for an apple, make Bean faster, make the bombs bigger -- every
number is in the settings.
"""
from kaypy import *
# ----------------------------------------------------------------- 1. setup
kaypy(width=800, height=600, background=[121, 189, 220])
loadSprite("bean", "images/bean.png")
loadSprite("bomb", "images/bomb.png")
loadSound("boom", "sounds/small_boom.wav")
# -------------------------------------------------------------- 2. settings
#
# Everything you would want to change is here. Change one at a time and
# play it.
GROUND = 60 # how tall the strip of grass is
BEAN_SPEED = 420 # how fast Bean runs, in pixels per second
BOMB_SIZE = 0.5 # how big a bomb is: 1 is the whole picture
HIT_DISTANCE = 42 # how close a bomb has to get to Bean to hit it
FALL_START = 200 # how fast the bombs fall, to begin with
FALL_FASTEST = 520 # ...the fastest they ever fall
FALL_GAIN = 8 # ...and how much faster each point makes them
WAIT_START = 0.9 # seconds between bombs, to begin with
WAIT_SHORTEST = 0.3 # ...the shortest wait there ever is
WAIT_DROP = 0.015 # ...and how much shorter each point makes it
# 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 = "bomb_dodge_best"
# -------------------------------------------------- 3. how hard the game is
#
# Each of these works out a number from the score. They get harder as the
# score goes up, and they stop somewhere, so the game ends up hard but never
# impossible.
score = 0
def fall_speed():
speed = FALL_START + score * FALL_GAIN
if speed > FALL_FASTEST:
speed = FALL_FASTEST
return speed
def bomb_wait():
seconds = WAIT_START - score * WAIT_DROP
if seconds < WAIT_SHORTEST:
seconds = WAIT_SHORTEST
return seconds
# ------------------------------------------------------- 4. 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".
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)
# The grass. It is only a picture: nothing here uses area() or body(),
# because the code below works out for itself what is touching what.
add([rect(width(), GROUND), pos(0, height() - GROUND),
color(96, 156, 76)])
# anchor("bot") means pos() is where Bean's feet are, so Bean stands
# on the grass. z(5) draws Bean in front of the bombs.
bean = add([sprite("bean"), pos(width() / 2, height() - GROUND),
anchor("bot"), z(5)])
score_label = add([text("0", size=48), pos(width() / 2, 40),
anchor("center"), z(10)])
best_label = add([text("best " + str(best), size=18), pos(12, 12),
opacity(0.75), z(10)])
# width=560 wraps a long message onto more than one line.
message = add([text("Arrow keys to move, or hold the mouse where you "
"want to go. Don't get hit!", size=26, width=560),
pos(width() / 2, height() / 2), anchor("center"), z(10)])
# ------------------------------------------------------------ running
#
# onKeyDown runs its function every frame the key is held, so Bean
# keeps running until you let go.
@onKeyDown("left")
def run_left():
bean.move(-BEAN_SPEED, 0)
keep_on_screen()
@onKeyDown("right")
def run_right():
bean.move(BEAN_SPEED, 0)
keep_on_screen()
# Holding the mouse down -- or a finger, on a phone -- runs Bean
# towards it. Within 10 pixels counts as there, and Bean stops.
@onMouseDown
def run_to_mouse():
if mousePos().x < bean.pos.x - 10:
bean.move(-BEAN_SPEED, 0)
if mousePos().x > bean.pos.x + 10:
bean.move(BEAN_SPEED, 0)
keep_on_screen()
def keep_on_screen():
# 32 is half of Bean's width.
bean.pos.x = clamp(bean.pos.x, 32, width() - 32)
# --------------------------------------------------------------- bombs
def drop_bomb():
# Game over: no bomb, and no wait() for the next one either, so
# the bombs stop here.
if not alive:
return
# Somewhere along the top, just above the window so it falls in
# rather than popping into view. rotate(0) lets it spin.
x = rand(30, width() - 30)
add([sprite("bomb"), pos(x, -40), anchor("center"),
scale(BOMB_SIZE), rotate(0), "bomb"])
# And the next one, a little later. No brackets after drop_bomb:
# wait() is handed the function, to run when the time is up.
wait(bomb_wait(), drop_bomb)
# The first bomb, a second after the game starts.
wait(1, drop_bomb)
# Every bomb runs this, every frame.
@onUpdate("bomb")
def fall(bomb):
global score
if not alive:
return
bomb.move(0, fall_speed())
bomb.angle = bomb.angle + 90 * dt()
# Close enough to Bean to hit it? Bean's pos() is its feet, so
# 27 pixels up -- half its height -- is the middle of Bean.
middle = vec2(bean.pos.x, bean.pos.y - 27)
if bomb.pos.dist(middle) < HIT_DISTANCE:
hit(bomb)
return
# Reached the grass without hitting Bean? That is a point.
if bomb.pos.y > height() - GROUND:
bomb.destroy()
score = score + 1
score_label.text = str(score)
if score == 1:
message.text = ""
# -------------------------------------------------------- getting hit
def hit(bomb):
global alive
alive = False
play("boom")
shake(12)
addKaboom(bomb.pos)
# Bean goes, and so does every bomb still falling. Left where they
# are, they hang in the air and cover up the message below.
bean.destroy()
destroyAll("bomb")
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)
# ------------------------------------------------------- 5. 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.
- Change the numbers. Make
BEAN_SPEED250 and Bean feels heavy; make it 700 and Bean darts about. MakeBOMB_SIZE1 and the bombs are twice the size, and thenHIT_DISTANCEwants to be bigger to match. - Something else falls. Change
"images/bomb.png"to"images/apple.png"or"images/pizza.png". Click Sprites in the playground to see what there is. - A sound for each point.
loadSound("point", "sounds/small_beep.wav")near the top, andplay("point")where a bomb reaches the grass. - Some bombs fall faster than others. Give each bomb its
own speed when it is made, with
{"speed": rand(150, 300)}in its list, and usebomb.speedinfallinstead offall_speed().