Building Coin Dash
Play it first → Bean runs and jumps around one room, grabbing a coin that moves somewhere new each time it is collected. Red blocks drop in through a gap in the ceiling and run about, and the higher your score, the faster they come. Touch one, or fall through the gap in the floor, and it's game 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. Every picture and sound comes with the playground, so there is nothing to find or upload.
If you have done Coin Rush, a lot of this will look familiar. The big difference is that Coin Rush looks down on Bean from above, and Coin Dash looks at Bean from the side. Things fall, and Bean has to jump.
The one big idea: a room is a picture
The room has over a hundred wall tiles in it. Typing a position for every one of them would take ages, and moving a platform would mean working out new numbers.
So the room is a picture made of letters. One letter is one tile:
LEVEL = [
"##########=====##########",
"# #",
"# #",
"# # # #",
"# #",
"# ###### ###### #",
...
]
# is a wall and = is one of the red gaps, at the
top and the bottom. To move a platform, move its #s. Nothing
else in the game has to be told about it.
Step 1 — a room
addLevel() reads the picture. Every time it meets a letter it
looks it up in TILES and builds whatever it finds there. A letter
that isn't listed is ignored, which is why a space does nothing.
from kaypy import *
TILE = 40 # one tile is 40 pixels square
kaypy(width=TILE * 25, height=TILE * 17, background=[52, 152, 219])
LEVEL = [
"##########=====##########",
"# #",
"# #",
"# # # #",
"# #",
"# ###### ###### #",
"# #",
"# #",
"# #",
"##### ######### #####",
"# # # #",
"# # # #",
"# ## ## #",
"# ### ### #",
"# #",
"# #",
"##########=====##########",
]
TILES = {
"#": lambda: [rect(TILE, TILE), color(52, 73, 94), area(),
body(isStatic=True), "wall"],
"=": lambda: [rect(TILE, TILE), color(231, 76, 60)],
}
addLevel(LEVEL, {
"tileWidth": TILE,
"tileHeight": TILE,
"tiles": TILES,
})
Run it and you get an empty room with dark walls and a red gap at the top and the bottom.
- The window is
TILE * 25wide andTILE * 17tall, because the picture is 25 letters across and 17 rows down. Change the size of a tile and the window changes with it. body(isStatic=True)is what makes a wall solid. It never moves, and anything with abody()stands on it and bumps into it.- A red gap has no
area()at all, so nothing can touch it. It is only there to be looked at. Later, things fall straight through it. - The
lambda:in front of each list means "build a new one of these every time you find this letter," so every#is a wall of its own. Leave it off and the game stops withTypeError: 'list' object is not callable:addLevelwants something it can call to make each tile, and a plain list is not that.
Every row of the picture must be the same length, 25 letters here. If one row is shorter, there's no error message. The walls on the right just don't line up.
Step 2 — Bean runs and jumps
This is a platform game, so the first new line is
setGravity(1000). Anything with a body() now falls
until it lands on something.
from kaypy import *
TILE = 40
RUN_SPEED = 400 # how fast Bean runs, in pixels per second
JUMP = 640 # how hard Bean jumps
BEAN_SIZE = 0.6 # Bean's picture is bigger than a tile, so shrink it
kaypy(width=TILE * 25, height=TILE * 17, background=[52, 152, 219])
setGravity(1000)
loadSprite("bean", "images/bean.png")
loadSound("jump", "sounds/swoosh.wav")
LEVEL = [
"##########=====##########",
"# #",
"# #",
"# # # #",
"# #",
"# ###### ###### #",
"# #",
"# #",
"# #",
"##### ######### #####",
"# # # #",
"# # # #",
"# ## ## #",
"# ### ### #",
"# #",
"# #",
"##########=====##########",
]
TILES = {
"#": lambda: [rect(TILE, TILE), color(52, 73, 94), area(),
body(isStatic=True), "wall"],
"=": lambda: [rect(TILE, TILE), color(231, 76, 60)],
}
addLevel(LEVEL, {
"tileWidth": TILE,
"tileHeight": TILE,
"tiles": TILES,
})
bean = add([sprite("bean"), scale(BEAN_SIZE), pos(500, 340),
anchor("center"), area(), body(), "player"])
@onUpdate
def each_frame():
if isKeyDown("left"):
bean.move(-RUN_SPEED, 0)
bean.flipX = True
if isKeyDown("right"):
bean.move(RUN_SPEED, 0)
bean.flipX = False
if isKeyDown("up") or isKeyDown("space"):
if bean.isGrounded():
bean.jump(JUMP)
play("jump")
Arrow keys to run, up or space to jump.
scale(BEAN_SIZE)shrinks Bean. The bean picture is 64 pixels wide and a tile is only 40, and a Bean bigger than a tile looks squashed into the room.anchor("center")meanspos(500, 340)is the middle of Bean, not its top-left corner. The coin positions in the next step work the same way.bean.move(-RUN_SPEED, 0)moves Bean left atRUN_SPEEDpixels per second. It is per second, not per frame, so Bean runs at the same speed on a fast computer and a slow one.bean.flipX = Trueturns the picture round, so Bean faces the way it is running.
bean.isGrounded() asks whether Bean is standing on
something. Take that if away and Bean can jump again in mid-air,
which means you can fly up the screen by tapping the key. Because the game
checks the key every frame, holding up down makes Bean jump
again the moment it lands.
Try running into the gap in the floor. Bean falls out of the bottom of the window and keeps falling for ever. Nothing in the game is watching for it yet. Step 5 fixes that.
Step 3 — a coin that moves
There is only ever one coin. When Bean touches it, it moves to another
spot. The spots are in a list, COIN_SPOTS.
from kaypy import *
TILE = 40
RUN_SPEED = 400 # how fast Bean runs, in pixels per second
JUMP = 640 # how hard Bean jumps
BEAN_SIZE = 0.6 # Bean's picture is bigger than a tile, so shrink it
COIN_SIZE = 0.7
POINTS = 5 # points for each coin
# Where the coin can be.
COIN_SPOTS = [
vec2(120, 300),
vec2(840, 300),
vec2(800, 160),
vec2(200, 160),
vec2(140, 600),
vec2(900, 600),
vec2(230, 490),
vec2(760, 490),
]
kaypy(width=TILE * 25, height=TILE * 17, background=[52, 152, 219])
setGravity(1000)
loadSprite("bean", "images/bean.png")
loadSprite("coin", "images/coin.png")
loadSound("jump", "sounds/swoosh.wav")
loadSound("coin", "sounds/small_beep.wav")
LEVEL = [
"##########=====##########",
"# #",
"# #",
"# # # #",
"# #",
"# ###### ###### #",
"# #",
"# #",
"# #",
"##### ######### #####",
"# # # #",
"# # # #",
"# ## ## #",
"# ### ### #",
"# #",
"# #",
"##########=====##########",
]
TILES = {
"#": lambda: [rect(TILE, TILE), color(52, 73, 94), area(),
body(isStatic=True), "wall"],
"=": lambda: [rect(TILE, TILE), color(231, 76, 60)],
}
addLevel(LEVEL, {
"tileWidth": TILE,
"tileHeight": TILE,
"tiles": TILES,
})
bean = add([sprite("bean"), scale(BEAN_SIZE), pos(500, 340),
anchor("center"), area(), body(), "player"])
coin = add([sprite("coin"), scale(COIN_SIZE), pos(COIN_SPOTS[0]),
anchor("center"), area(), opacity(1), "coin"])
score = 0
label = add([text("Score: 0", size=36), pos(60, 50), z(100)])
@onUpdate
def each_frame():
if isKeyDown("left"):
bean.move(-RUN_SPEED, 0)
bean.flipX = True
if isKeyDown("right"):
bean.move(RUN_SPEED, 0)
bean.flipX = False
if isKeyDown("up") or isKeyDown("space"):
if bean.isGrounded():
bean.jump(JUMP)
play("jump")
@bean.onCollide("coin")
def got_coin(c):
global score
score = score + POINTS
label.text = "Score: " + str(score)
play("coin")
move_coin()
def move_coin():
# Every spot except the one the coin is at now.
spots = []
for spot in COIN_SPOTS:
if spot.x != coin.pos.x or spot.y != coin.pos.y:
spots.append(spot)
coin.pos = choose(spots)
# Fade in at the new spot, so you can see where it went.
tween(0, 1, 0.3, fade_coin)
def fade_coin(amount):
coin.opacity = amount
The coin has an area() but no body(). An
area() on its own is something you can touch but walk straight
through, which is what a coin should be. It also has no gravity, so it hangs
in the air.
@bean.onCollide("coin") runs got_coin whenever
Bean touches something tagged "coin". The tag is the
"coin" at the end of the coin's list. If the two are spelled even
slightly differently, there is no error message. Bean just walks through
the coin and nothing happens.
move_coin has to pick a new spot that isn't the one the coin
is already at, or sometimes the coin would "move" without moving. So it makes
a new, empty list, goes through COIN_SPOTS one at a time, and
adds every spot that isn't where the coin is now. Then choose()
picks one of those.
tween(0, 1, 0.3, fade_coin) counts smoothly from 0 to 1 over
0.3 seconds, and calls fade_coin with each number along the way.
fade_coin sets the coin's opacity, so the coin fades in at its new
spot and your eye catches where it went.
Don't forget global.
Take out global score and the first coin you grab crashes the
game with UnboundLocalError: cannot access local variable 'score' where
it is not associated with a value. Without global,
Python thinks score = score + POINTS is about a new variable of
got_coin's own, and that one has no value yet to add to.
Step 4 — red blocks
Now something to dodge. Every few seconds a red block drops in through the gap in the ceiling and runs about the room.
from kaypy import *
TILE = 40
RUN_SPEED = 400 # how fast Bean runs, in pixels per second
JUMP = 640 # how hard Bean jumps
BEAN_SIZE = 0.6 # Bean's picture is bigger than a tile, so shrink it
COIN_SIZE = 0.7
POINTS = 5 # points for each coin
ENEMY_SPEED = 200 # how fast a red block runs once it gets going
ENEMY_WAIT = 3 # seconds between red blocks
# Where the coin can be.
COIN_SPOTS = [
vec2(120, 300),
vec2(840, 300),
vec2(800, 160),
vec2(200, 160),
vec2(140, 600),
vec2(900, 600),
vec2(230, 490),
vec2(760, 490),
]
kaypy(width=TILE * 25, height=TILE * 17, background=[52, 152, 219])
setGravity(1000)
loadSprite("bean", "images/bean.png")
loadSprite("coin", "images/coin.png")
loadSound("jump", "sounds/swoosh.wav")
loadSound("coin", "sounds/small_beep.wav")
LEVEL = [
"##########=====##########",
"# #",
"# #",
"# # # #",
"# #",
"# ###### ###### #",
"# #",
"# #",
"# #",
"##### ######### #####",
"# # # #",
"# # # #",
"# ## ## #",
"# ### ### #",
"# #",
"# #",
"##########=====##########",
]
TILES = {
"#": lambda: [rect(TILE, TILE), color(52, 73, 94), area(),
body(isStatic=True), "wall"],
"=": lambda: [rect(TILE, TILE), color(231, 76, 60)],
}
addLevel(LEVEL, {
"tileWidth": TILE,
"tileHeight": TILE,
"tiles": TILES,
})
bean = add([sprite("bean"), scale(BEAN_SIZE), pos(500, 340),
anchor("center"), area(), body(), "player"])
coin = add([sprite("coin"), scale(COIN_SIZE), pos(COIN_SPOTS[0]),
anchor("center"), area(), opacity(1), "coin"])
score = 0
next_enemy = 0 # seconds until the next red block drops in
label = add([text("Score: 0", size=36), pos(60, 50), z(100)])
@onUpdate
def each_frame():
global next_enemy
if isKeyDown("left"):
bean.move(-RUN_SPEED, 0)
bean.flipX = True
if isKeyDown("right"):
bean.move(RUN_SPEED, 0)
bean.flipX = False
if isKeyDown("up") or isKeyDown("space"):
if bean.isGrounded():
bean.jump(JUMP)
play("jump")
# Time for another red block? dt() is how long this frame took, so
# taking it away every frame counts down in real seconds.
next_enemy = next_enemy - dt()
if next_enemy <= 0:
add_enemy()
next_enemy = ENEMY_WAIT
@bean.onCollide("coin")
def got_coin(c):
global score
score = score + POINTS
label.text = "Score: " + str(score)
play("coin")
move_coin()
def move_coin():
# Every spot except the one the coin is at now.
spots = []
for spot in COIN_SPOTS:
if spot.x != coin.pos.x or spot.y != coin.pos.y:
spots.append(spot)
coin.pos = choose(spots)
# Fade in at the new spot, so you can see where it went.
tween(0, 1, 0.3, fade_coin)
def fade_coin(amount):
coin.opacity = amount
def add_enemy():
# In through the gap in the ceiling. lifespan(15) clears it away
# after 15 seconds, so they never pile up.
enemy = add([rect(40, 40), color(231, 76, 60), pos(500, -20),
anchor("center"), area(), body(), lifespan(15),
"enemy",
{"speed": choose([-200, 200, 60, -60]), "last_x": 0}])
# After two seconds it stops dawdling and picks a direction.
@wait(2)
def get_going():
enemy.speed = choose([-ENEMY_SPEED, ENEMY_SPEED])
@onUpdate("enemy")
def run_about(enemy):
# If it got nowhere since last frame, a wall is in the way -- so
# turn round.
if abs(enemy.pos.x - enemy.last_x) < 0.1:
enemy.speed = -enemy.speed
enemy.last_x = enemy.pos.x
enemy.move(enemy.speed, 0)
The red blocks don't hurt yet. They just shove Bean about. That's the next step.
The countdown. next_enemy is how many
seconds until the next block. dt() is how long the last frame
took, so taking it away every frame counts down in real seconds. When it gets
to zero, a block is added and the countdown starts again from
ENEMY_WAIT. It starts at 0, so the first block arrives straight
away.
Each block's own values. The {"speed": ...,
"last_x": 0} at the end of the list gives each block two variables of
its own, read as enemy.speed and enemy.last_x. Each
block runs at its own speed, so they need one each.
Getting going. A new block wanders slowly or quickly
for two seconds, then @wait(2) gives it a proper speed in one
direction or the other. Because get_going is written
inside add_enemy, the enemy it changes is
the block that was just made, not some other one.
Turning round at walls. This is the one bit of thinking
in the game. A block runs into a wall and the wall stops it, but nothing
tells the block it hit anything. So every frame it asks: did I get
anywhere since last frame? If its x is the same as
last_x (to within a tenth of a pixel), something must be in the
way, so it turns round. abs() is the size of a number without its
minus sign, so abs(enemy.pos.x - enemy.last_x) is how far it moved
either way.
last_x starts at 0, far away from where a block appears.
Start it at 500 instead and every new block would think it was stuck on its
very first frame, and turn round before it had moved.
lifespan(15) removes a block after fifteen seconds, so they
don't pile up for ever. Plenty of them fall out through the gap in the floor
long before that.
Step 5 — game over, and again
Touching a red block, or falling out through a gap, now ends the game. A second later it starts again.
Starting again means throwing away every wall, coin and block and building
them all fresh. That is what a scene is for. Everything from
addLevel down moves inside @scene("game"), one step
further in, and go("game") builds it. Calling
go("game") again throws the whole scene away and builds it from
the start.
from kaypy import *
TILE = 40
RUN_SPEED = 400 # how fast Bean runs, in pixels per second
JUMP = 640 # how hard Bean jumps
BEAN_SIZE = 0.6 # Bean's picture is bigger than a tile, so shrink it
COIN_SIZE = 0.7
POINTS = 5 # points for each coin
ENEMY_SPEED = 200 # how fast a red block runs once it gets going
ENEMY_WAIT = 3 # seconds between red blocks
# Where the coin can be.
COIN_SPOTS = [
vec2(120, 300),
vec2(840, 300),
vec2(800, 160),
vec2(200, 160),
vec2(140, 600),
vec2(900, 600),
vec2(230, 490),
vec2(760, 490),
]
kaypy(width=TILE * 25, height=TILE * 17, background=[52, 152, 219])
setGravity(1000)
loadSprite("bean", "images/bean.png")
loadSprite("coin", "images/coin.png")
loadSound("jump", "sounds/swoosh.wav")
loadSound("coin", "sounds/small_beep.wav")
loadSound("dead", "sounds/small_boom.wav")
LEVEL = [
"##########=====##########",
"# #",
"# #",
"# # # #",
"# #",
"# ###### ###### #",
"# #",
"# #",
"# #",
"##### ######### #####",
"# # # #",
"# # # #",
"# ## ## #",
"# ### ### #",
"# #",
"# #",
"##########=====##########",
]
TILES = {
"#": lambda: [rect(TILE, TILE), color(52, 73, 94), area(),
body(isStatic=True), "wall"],
"=": lambda: [rect(TILE, TILE), color(231, 76, 60)],
}
score = 0
next_enemy = 0
dead = False
@scene("game")
def game():
global score, next_enemy, dead
# Back to the start, every time the scene is built.
score = 0
next_enemy = 0
dead = False
addLevel(LEVEL, {
"tileWidth": TILE,
"tileHeight": TILE,
"tiles": TILES,
})
bean = add([sprite("bean"), scale(BEAN_SIZE), pos(500, 340),
anchor("center"), area(), body(), z(10), "player"])
coin = add([sprite("coin"), scale(COIN_SIZE), pos(COIN_SPOTS[0]),
anchor("center"), area(), opacity(1), "coin"])
label = add([text("Score: 0", size=36), pos(60, 50), z(100)])
@onUpdate
def each_frame():
global next_enemy
if dead:
return
if isKeyDown("left"):
bean.move(-RUN_SPEED, 0)
bean.flipX = True
if isKeyDown("right"):
bean.move(RUN_SPEED, 0)
bean.flipX = False
if isKeyDown("up") or isKeyDown("space"):
if bean.isGrounded():
bean.jump(JUMP)
play("jump")
# Out through the gap in the floor, or the one in the ceiling.
if bean.pos.y > height() or bean.pos.y < 0:
die()
next_enemy = next_enemy - dt()
if next_enemy <= 0:
add_enemy()
next_enemy = ENEMY_WAIT
@bean.onCollide("coin")
def got_coin(c):
global score
if dead:
return
score = score + POINTS
label.text = "Score: " + str(score)
play("coin")
move_coin()
def move_coin():
spots = []
for spot in COIN_SPOTS:
if spot.x != coin.pos.x or spot.y != coin.pos.y:
spots.append(spot)
coin.pos = choose(spots)
tween(0, 1, 0.3, fade_coin)
def fade_coin(amount):
coin.opacity = amount
def add_enemy():
enemy = add([rect(40, 40), color(231, 76, 60), pos(500, -20),
anchor("center"), area(), body(), lifespan(15),
"enemy",
{"speed": choose([-200, 200, 60, -60]), "last_x": 0}])
@wait(2)
def get_going():
enemy.speed = choose([-ENEMY_SPEED, ENEMY_SPEED])
@onUpdate("enemy")
def run_about(enemy):
if abs(enemy.pos.x - enemy.last_x) < 0.1:
enemy.speed = -enemy.speed
enemy.last_x = enemy.pos.x
enemy.move(enemy.speed, 0)
@bean.onCollide("enemy")
def hit(enemy):
die()
def die():
global dead
# Touching two red blocks at once would otherwise die twice.
if dead:
return
dead = True
play("dead")
shake(12)
# A red flash over the whole window, gone in a third of a second.
add([rect(width(), height()), color(255, 50, 35), opacity(0.6),
lifespan(0.3, fade=0.3), z(200)])
# Bean bursts into little white squares.
for i in range(40):
direction = vec2(rand(-1, 1), rand(-1, 1))
add([rect(8, 8), pos(bean.pos), color(255, 255, 255),
opacity(1), move(direction, rand(50, 300)),
lifespan(1, fade=0.5), z(150)])
bean.destroy()
# Start again, a second later.
@wait(1)
def start_again():
go("game")
go("game")
The variables that change during a game are set back to their starting
values at the top of the scene, because the scene is built again every game.
If score = 0 wasn't there, your next game would start with the
last game's score.
Why dead? Without it, die()
would run more than once. Touch two red blocks at once and that's two calls.
And Bean is still below the window after falling out, so
each_frame would call die() again on every frame
until the game restarted, playing the sound and starting a new restart timer
each time. So die() sets dead = True the first time
and does nothing after that, and each_frame and
got_coin stop working once it is set.
The burst. A for loop makes forty small
white squares where Bean was. Each gets its own random direction and speed
from move(), and lifespan(1, fade=0.5) fades it out
and removes it after a second. The fade needs opacity(1) in the
list. Leave it out and the squares vanish all at once instead of fading, and
kaypy prints note: lifespan(fade=...) needs an opacity() component to
fade for every one of them.
Don't call the scene function
play. kaypy already has a play() for
sounds. Write def play(): and you have replaced it, so every
play("coin") afterwards tries to start your scene instead of
making a noise. That's why the scene is called "game" and its
function is game().
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 menu. A second scene,
"menu", with the title, "Press Up to Start" and your score. Dying now doesgo("menu", score), and the score is handed to the menu aslast_score. The game starts on the menu withgo("menu", 0), since there is no last game yet. - A best score that is remembered.
getData(BEST_KEY, 0)reads the best score saved last time, or 0 if there isn't one.setDatasaves a new one. The key is"coin_dash_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. - Red blocks that speed up.
enemy_wait()works out the wait from your score. At 0 points it is 5 seconds; at 25 points, halfway toHARDEST_AT, it is 2.75; at 50 points and above it is half a second.
The title drops in with tween again, this time moving the
title's y from above the window down to 160, with
easings.easeOutBounce so it bounces as it lands. "Press Up to
Start" rocks from side to side because wave(-2, 2, time() * 3)
swings between −2 and 2 degrees for ever.
It also has all the numbers worth changing in one settings block near the top, and comments that explain why each part is there.
"""Coin Dash -- grab the coin, dodge the red blocks, don't fall in the hole.
left / right run
up or space jump
One coin at a time. Grab it and it pops up somewhere else, worth 5 points.
Red blocks drop in through the gap in the ceiling and run about, and the
higher your score, the faster they come. Touch one, or fall through the gap
in the floor, and it is game over. Your best score is remembered.
HOW THIS FILE IS PUT TOGETHER
1. the window, the pictures and the sounds
2. the settings
3. the level
4. the menu scene
5. the game scene: Bean, the coin, the red blocks, dying
6. start the game
The level is a picture made of letters. Change a '#' and you have moved a
wall -- you never have to touch the code to change the level.
"""
from kaypy import *
# ----------------------------------------------------------------- 1. setup
TILE = 40 # one tile is 40 pixels square
ACROSS = 25 # tiles across
DOWN = 17 # tiles down
kaypy(width=TILE * ACROSS, height=TILE * DOWN, background=[52, 152, 219])
# A platform game, so things fall. Without this line nothing would.
setGravity(1000)
# Every picture and sound here comes with the playground, so you can paste
# this whole file in and press Run. The red blocks are not pictures at all
# -- they are plain rectangles, made with rect().
loadSprite("bean", "images/bean.png")
loadSprite("coin", "images/coin.png")
# The name before the comma is the name the game uses. The file after it is
# the one that gets played -- swap it for any sound in the playground.
loadSound("jump", "sounds/swoosh.wav")
loadSound("coin", "sounds/small_beep.wav")
loadSound("dead", "sounds/small_boom.wav")
# -------------------------------------------------------------- 2. settings
#
# Every number worth changing is here.
BEAN_SIZE = 0.6 # Bean's picture is bigger than a tile, so shrink it
COIN_SIZE = 0.7 # ...and the coin a little less
RUN_SPEED = 400 # how fast you run, in pixels per second
JUMP = 640 # how hard you jump
ENEMY_SPEED = 200 # how fast a red block runs once it gets going
POINTS = 5 # points for each coin
# A new red block every 5 seconds to begin with. The more points you have,
# the shorter the wait, until at 50 points there is one every half second.
SLOWEST = 5
FASTEST = 0.5
HARDEST_AT = 50
# Where the coin can be. Each time you grab it, it moves to one of these --
# never the one it was just at.
COIN_SPOTS = [
vec2(120, 300),
vec2(840, 300),
vec2(800, 160),
vec2(200, 160),
vec2(140, 600),
vec2(900, 600),
vec2(230, 490),
vec2(760, 490),
]
# 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 -- a plain "best"
# would be overwritten by the next game that saved one.
BEST_KEY = "coin_dash_best"
# ----------------------------------------------------------------- 3. level
#
# One letter is one tile.
#
# # wall
# = the red gaps, top and bottom. Red blocks drop in through the top
# one, and you can fall out through the bottom one.
#
# A space is empty. Every row has to be 25 letters long.
LEVEL = [
"##########=====##########",
"# #",
"# #",
"# # # #",
"# #",
"# ###### ###### #",
"# #",
"# #",
"# #",
"##### ######### #####",
"# # # #",
"# # # #",
"# ## ## #",
"# ### ### #",
"# #",
"# #",
"##########=====##########",
]
# What each letter turns into. A wall has body(isStatic=True), so things
# stand on it and bump into it. A gap has no area(), so nothing touches it
# -- it is only there to be looked at.
TILES = {
"#": lambda: [rect(TILE, TILE), color(52, 73, 94), area(),
body(isStatic=True), "wall"],
"=": lambda: [rect(TILE, TILE), color(231, 76, 60)],
}
# ------------------------------------------------------------ 4. the menu
#
# last_score is what you got last time. The very first time, there is no
# last time, so it starts at 0.
@scene("menu")
def menu(last_score):
best = getData(BEST_KEY, 0)
if last_score > best:
best = last_score
setData(BEST_KEY, best)
# The title starts above the top of the window and drops in, bouncing.
# tween() changes a number smoothly from -100 to 160 over one second,
# and calls drop_title with each new number along the way.
title = add([text("Coin Dash", size=120), pos(width() / 2, -100),
anchor("center")])
def drop_title(y):
title.pos.y = y
tween(-100, 160, 1, drop_title, easings.easeOutBounce)
start = add([text("Press Up to Start", size=60), pos(center()),
anchor("center"), rotate(0)])
add([text("Score: " + str(last_score) + "\nBest Score: " + str(best),
size=44),
pos(width() / 2, 520), anchor("center")])
# wave() swings between two numbers for ever, so the words rock from
# side to side.
@onUpdate
def rock():
start.angle = wave(-2, 2, time() * 3)
@onKeyPress("up")
def start_game():
go("game")
@onKeyPress("space")
def start_game_too():
go("game")
@onClick
def clicked():
go("game")
# ------------------------------------------------------- 5. the game scene
#
# The scene is CALLED "game". The function is NOT called play(), because
# kaypy already has a play() for sounds -- name your function play() and
# every play("coin") would start a game instead of making a noise.
#
# 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
next_enemy = 0 # seconds until the next red block drops in
dead = False
@scene("game")
def game():
global score, next_enemy, dead
score = 0
next_enemy = 0
dead = False
addLevel(LEVEL, {
"tileWidth": TILE,
"tileHeight": TILE,
"tiles": TILES,
})
bean = add([sprite("bean"), scale(BEAN_SIZE), pos(500, 340),
anchor("center"), area(), body(), z(10), "player"])
coin = add([sprite("coin"), scale(COIN_SIZE), pos(COIN_SPOTS[0]),
anchor("center"), area(), opacity(1), "coin"])
label = add([text("Score: 0", size=36), pos(60, 50), z(100)])
# ------------------------------------------------------- every frame
@onUpdate
def each_frame():
global next_enemy
if dead:
return
# Running. flipX turns the picture round, so Bean faces the way
# it is going.
if isKeyDown("left"):
bean.move(-RUN_SPEED, 0)
bean.flipX = True
if isKeyDown("right"):
bean.move(RUN_SPEED, 0)
bean.flipX = False
# Jumping. Only from the ground, or you could fly by tapping.
if isKeyDown("up") or isKeyDown("space"):
if bean.isGrounded():
bean.jump(JUMP)
play("jump")
# Out through the gap in the floor, or the one in the ceiling.
if bean.pos.y > height() or bean.pos.y < 0:
die()
# Time for another red block? dt() is how long this frame took, so
# taking it away every frame counts down in real seconds.
next_enemy = next_enemy - dt()
if next_enemy <= 0:
add_enemy()
next_enemy = enemy_wait()
# -------------------------------------------------------------- coins
@bean.onCollide("coin")
def got_coin(c):
global score
if dead:
return
score = score + POINTS
label.text = "Score: " + str(score)
play("coin")
move_coin()
def move_coin():
# Every spot except the one the coin is at now.
spots = []
for spot in COIN_SPOTS:
if spot.x != coin.pos.x or spot.y != coin.pos.y:
spots.append(spot)
coin.pos = choose(spots)
# Fade in at the new spot, so you can see where it went.
tween(0, 1, 0.3, fade_coin)
def fade_coin(amount):
coin.opacity = amount
# -------------------------------------------------------- red blocks
# The longer you survive, the less time between red blocks.
def enemy_wait():
progress = score / HARDEST_AT
if progress > 1:
progress = 1
return SLOWEST - (SLOWEST - FASTEST) * progress
def add_enemy():
# In through the gap in the ceiling. lifespan(15) clears it away
# after 15 seconds, so they never pile up.
enemy = add([rect(40, 40), color(231, 76, 60), pos(500, -20),
anchor("center"), area(), body(), lifespan(15),
"enemy",
{"speed": choose([-200, 200, 60, -60]), "last_x": 0}])
# After two seconds it stops dawdling and picks a direction.
@wait(2)
def get_going():
enemy.speed = choose([-ENEMY_SPEED, ENEMY_SPEED])
@onUpdate("enemy")
def run_about(enemy):
# If it got nowhere since last frame, a wall is in the way -- so
# turn round. abs() is the size of a number without its sign, so
# this asks "did it move less than a tenth of a pixel either way?"
# (last_x starts at 0, far from where a block starts, so a brand
# new block does not turn round before it has had a chance to move.)
if abs(enemy.pos.x - enemy.last_x) < 0.1:
enemy.speed = -enemy.speed
enemy.last_x = enemy.pos.x
enemy.move(enemy.speed, 0)
@bean.onCollide("enemy")
def hit(enemy):
die()
# ------------------------------------------------------------ dying
def die():
global dead
# Touching two red blocks at once would otherwise die twice.
if dead:
return
dead = True
play("dead")
shake(12)
# A red flash over the whole window, gone in a third of a second.
add([rect(width(), height()), color(255, 50, 35), opacity(0.6),
lifespan(0.3, fade=0.3), z(200)])
# Bean bursts into little white squares, each flying off in
# its own direction and fading away.
for i in range(40):
direction = vec2(rand(-1, 1), rand(-1, 1))
add([rect(8, 8), pos(bean.pos), color(255, 255, 255),
opacity(1), move(direction, rand(50, 300)),
lifespan(1, fade=0.5),
z(150)])
bean.destroy()
@wait(1)
def back_to_menu():
go("menu", score)
# ------------------------------------------------------- 6. start the game
go("menu", 0)
Make it yours
Open the whole game in the playground → It goes straight into the editor, ready to change and run.
- Change the numbers.
RUN_SPEED,JUMP,ENEMY_SPEED,HARDEST_AT. TrysetGravity(500)with the sameJUMPand Bean floats like it's on the moon. - Redraw the room. Move the
#s around. Check theCOIN_SPOTSafterwards: a spot that is now inside a wall puts the coin where Bean can never reach it. Each tile is 40 pixels, so the tile in columncand rowrhas its middle atvec2(c * 40 + 20, r * 40 + 20), counting from 0. - Add a second kind of block. Copy
add_enemyintoadd_fast_enemy, make the block purple and give it twice the speed. Keep the"enemy"tag and it already turns at walls and kills Bean, because both of those work from the tag. - Use WASD as well. Change
isKeyDown("left")toisKeyDown("left") or isKeyDown("a"), and the same for the others.