Building Dark Blue
Play it first → Five levels of running, jumping and collecting coins. Get every coin on a level to move on. Touch anything red, or fall off the bottom, and the level starts again. The game counts how many times you died.
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. It is based on Dark Blue by Thomas Palef.
The levels are big, so steps 1 to 5 only use the first one or two of them. Step 6 is the whole game, with all five.
The one big idea: the level is wider than the window
Each level is a picture made of letters, like the ones in Coin Rush and Coin Dash. The difference is size. The window is 30 tiles across, and a level is about 100. So the game needs a camera: as Bean runs to the right, the camera moves right with it, and the level slides past.
The level is exactly 20 tiles tall, the same as the window, so the camera only ever has to move sideways.
Step 1 — a level
from kaypy import *
TILE = 30 # one tile is 30 pixels square
kaypy(width=TILE * 30, height=TILE * 20, background=[67, 117, 210])
setGravity(1350)
loadSprite("bean", "images/bean.png")
loadSprite("steel", "images/steel.png")
LEVEL_1 = [
" #",
" #",
"#### #",
" # #",
" # #",
" # #",
" # $ #",
" # #",
" # #### #",
" # # # $ $$ #",
" # # # #",
" # # # #### ##### #### #",
" #### $$ # # # # # # ####",
" # # # $$ # # # # # ",
" # @ # # # # # # $ # ",
" ########## ############ ################# ########## ###################### ",
" #===========# ",
" ############# ",
]
TILES = {
"#": lambda: [sprite("steel"), scale(TILE / 64), area(),
body(isStatic=True), "ground"],
"@": lambda: [sprite("bean"), scale(0.4), area(), body(), "player"],
}
addLevel(LEVEL_1, {
"tileWidth": TILE,
"tileHeight": TILE,
"tiles": TILES,
})
Run it and Bean drops onto the ground at the start of level 1. You can only see the first 30 columns; the rest is off to the right, waiting for the camera.
TILE * 30wide andTILE * 20tall makes the window exactly 30 tiles by 20.- The steel picture is 64 pixels square and a tile is 30, so
scale(TILE / 64)shrinks it to fit. ChangeTILEand the walls still fit. - Bean is a tile too: the
@in the picture. Moving the@moves where Bean starts.scale(0.4)makes Bean a little smaller than a tile, so it fits through a gap one tile wide.
The picture also has $ and = in it, for coins and
lava, but nothing appears there yet. A letter that isn't in
TILES is simply ignored. They get added in later steps.
Step 2 — Bean runs, and the camera follows
from kaypy import *
TILE = 30 # one tile is 30 pixels square
RUN_SPEED = 375 # how fast Bean runs, in pixels per second
kaypy(width=TILE * 30, height=TILE * 20, background=[67, 117, 210])
setGravity(1350)
loadSprite("bean", "images/bean.png")
loadSprite("steel", "images/steel.png")
LEVEL_1 = [
" #",
" #",
"#### #",
" # #",
" # #",
" # #",
" # $ #",
" # #",
" # #### #",
" # # # $ $$ #",
" # # # #",
" # # # #### ##### #### #",
" #### $$ # # # # # # ####",
" # # # $$ # # # # # ",
" # @ # # # # # # $ # ",
" ########## ############ ################# ########## ###################### ",
" #===========# ",
" ############# ",
]
TILES = {
"#": lambda: [sprite("steel"), scale(TILE / 64), area(),
body(isStatic=True), "ground"],
"@": lambda: [sprite("bean"), scale(0.4), area(), body(), "player"],
}
addLevel(LEVEL_1, {
"tileWidth": TILE,
"tileHeight": TILE,
"tiles": TILES,
})
bean = get("player")[0]
level_width = len(LEVEL_1[0]) * TILE
@onUpdate
def each_frame():
if isKeyDown("left"):
bean.move(-RUN_SPEED, 0)
if isKeyDown("right"):
bean.move(RUN_SPEED, 0)
# The camera follows Bean from side to side, but stops at the ends of
# the level.
x = clamp(bean.pos.x, width() / 2, level_width - width() / 2)
setCamPos(vec2(x, height() / 2))
Bean starts in a dip and can't get out of it without jumping, which is step 3. Until then the camera has nowhere to go: Bean never gets more than half a window from the start, and the camera doesn't move until it does.
The camera. setCamPos() says which point of
the level should be in the middle of the window. Put it where Bean is, and
Bean stays in the middle as the level slides past.
But not right at the ends. If Bean walks to the very left edge and the
camera follows, half the window shows nothing past the end of the level. So
clamp() keeps the camera's x between two limits:
never less than half a window from the start, never less than half a window
from the end. level_width is how wide the level is, in pixels:
the length of a row, times the size of a tile.
The y is always height() / 2, the middle of the
window, because the level is exactly one window tall.
setCamPos takes one
vec2, not two numbers. setCamPos(x, 300)
stops the game with TypeError: setCamPos() takes 1 positional argument
but 2 were given. Wrap the two numbers up as one point:
setCamPos(vec2(x, 300)).
Step 3 — jumping, and jumping higher
from kaypy import *
TILE = 30 # one tile is 30 pixels square
RUN_SPEED = 375 # how fast Bean runs on the ground, pixels per second
AIR_SPEED = 300 # ...and in the air, a little slower
JUMP = 712 # how hard Bean jumps
JUMP_HOLD = 0.2 # how long holding up keeps pushing Bean higher
kaypy(width=TILE * 30, height=TILE * 20, background=[67, 117, 210])
setGravity(1350)
loadSprite("bean", "images/bean.png")
loadSprite("steel", "images/steel.png")
LEVEL_1 = [
" #",
" #",
"#### #",
" # #",
" # #",
" # #",
" # $ #",
" # #",
" # #### #",
" # # # $ $$ #",
" # # # #",
" # # # #### ##### #### #",
" #### $$ # # # # # # ####",
" # # # $$ # # # # # ",
" # @ # # # # # # $ # ",
" ########## ############ ################# ########## ###################### ",
" #===========# ",
" ############# ",
]
TILES = {
"#": lambda: [sprite("steel"), scale(TILE / 64), area(),
body(isStatic=True), "ground"],
"@": lambda: [sprite("bean"), scale(0.4), area(), body(), "player"],
}
addLevel(LEVEL_1, {
"tileWidth": TILE,
"tileHeight": TILE,
"tiles": TILES,
})
bean = get("player")[0]
level_width = len(LEVEL_1[0]) * TILE
hold = 0 # seconds of jump-holding left
@onUpdate
def each_frame():
global hold
speed = AIR_SPEED
if bean.isGrounded():
speed = RUN_SPEED
if isKeyDown("left"):
bean.move(-speed, 0)
if isKeyDown("right"):
bean.move(speed, 0)
# A tap is a small jump. Keep holding up and Bean keeps being pushed
# upward for JUMP_HOLD seconds, so it goes higher.
if isKeyDown("up"):
if bean.isGrounded():
bean.jump(JUMP)
hold = JUMP_HOLD
elif hold > 0:
hold = hold - dt()
bean.jump(JUMP)
else:
hold = 0
# The camera follows Bean from side to side, but stops at the ends of
# the level.
x = clamp(bean.pos.x, width() / 2, level_width - width() / 2)
setCamPos(vec2(x, height() / 2))
Tap up for a small jump, about 6 tiles high. Hold it for a big one, about 11.
How holding works. When Bean jumps,
hold is set to JUMP_HOLD, a fifth of a second.
While up is still held and there is hold left, every frame takes a little
off hold and calls bean.jump(JUMP) again, which
keeps throwing Bean upward at full speed. Let go, or run out of hold, and
gravity takes over. Letting go also sets hold to 0, so you
can't let go and press again in mid-air for another push.
Bean also runs a little slower in the air than on the ground. It makes a jump easier to aim.
Don't forget global.
Take out global hold and the game crashes the first time you
hold up in mid-air, with UnboundLocalError: cannot access local
variable 'hold' where it is not associated with a value. Without
global, Python thinks the hold in
each_frame is a new variable of its own, and that one has no
value yet when hold > 0 asks about it.
Step 4 — coins, and a second level
Collect every coin on a level and the next level starts. To build a level
again from nothing, the game moves into a scene:
everything from addLevel down goes inside
@scene("level"), one step further in. number says
which level to build, and go("level", 1) throws the current one
away and builds level 2.
from kaypy import *
TILE = 30 # one tile is 30 pixels square
RUN_SPEED = 375 # how fast Bean runs on the ground, pixels per second
AIR_SPEED = 300 # ...and in the air, a little slower
JUMP = 712 # how hard Bean jumps
JUMP_HOLD = 0.2 # how long holding up keeps pushing Bean higher
kaypy(width=TILE * 30, height=TILE * 20, background=[67, 117, 210])
setGravity(1350)
loadSprite("bean", "images/bean.png")
loadSprite("coin", "images/coin.png")
loadSprite("steel", "images/steel.png")
loadSound("coin", "sounds/small_beep.wav")
loadSound("done", "sounds/rising_beep.wav")
LEVEL_1 = [
" #",
" #",
"#### #",
" # #",
" # #",
" # #",
" # $ #",
" # #",
" # #### #",
" # # # $ $$ #",
" # # # #",
" # # # #### ##### #### #",
" #### $$ # # # # # # ####",
" # # # $$ # # # # # ",
" # @ # # # # # # $ # ",
" ########## ############ ################# ########## ###################### ",
" #===========# ",
" ############# ",
]
LEVEL_2 = [
" # # # ",
" # # # ",
" # # # ",
" # # # ####",
" # # > # # # # ",
" # # # # ",
" # #======# # ",
" # #======# #### # ",
" # #======# # # $ # ",
" # ######## ### # # ",
" # @ # # ### # ",
" # ### # $ # ",
" # v v # # # ",
" # $ $ $ ### # # $ $ # # ",
" # # # # > # # ",
" ############################################# # ######################### ",
" #===============# ",
" ################# ",
" ",
" ",
]
LEVELS = [LEVEL_1, LEVEL_2]
TILES = {
"#": lambda: [sprite("steel"), scale(TILE / 64), area(),
body(isStatic=True), "ground"],
"$": lambda: [sprite("coin"), scale(0.5), area(), "coin",
{"home_y": 0}],
"@": lambda: [sprite("bean"), scale(0.4), area(), body(), rotate(0),
z(10), "player"],
}
coins = 0
hold = 0
level_over = False
@scene("level")
def build_level(number):
global coins, hold, level_over
coins = 0
hold = 0
level_over = False
level = LEVELS[number]
level_width = len(level[0]) * TILE
addLevel(level, {
"tileWidth": TILE,
"tileHeight": TILE,
"tiles": TILES,
})
bean = get("player")[0]
total = len(get("coin"))
for coin in get("coin"):
coin.home_y = coin.pos.y
label = add([text("", size=24), pos(16, 12), fixed(), z(100)])
def show_score():
label.text = ("Level " + str(number + 1)
+ " Coins " + str(coins) + " / " + str(total))
show_score()
@onUpdate
def each_frame():
global hold
if level_over:
return
speed = AIR_SPEED
if bean.isGrounded():
speed = RUN_SPEED
if isKeyDown("left"):
bean.move(-speed, 0)
if isKeyDown("right"):
bean.move(speed, 0)
if isKeyDown("up"):
if bean.isGrounded():
bean.jump(JUMP)
hold = JUMP_HOLD
elif hold > 0:
hold = hold - dt()
bean.jump(JUMP)
else:
hold = 0
x = clamp(bean.pos.x, width() / 2, level_width - width() / 2)
setCamPos(vec2(x, height() / 2))
@onUpdate("coin")
def bob(coin):
coin.pos.y = coin.home_y + wave(-3, 3, time() * 5)
@bean.onCollide("coin")
def got_coin(coin):
global coins
if level_over:
return
coin.destroy()
coins = coins + 1
play("coin")
show_score()
if coins == total:
finish()
def finish():
global level_over
level_over = True
play("done")
def spin(angle):
bean.angle = angle
tween(0, 360, 0.6, spin)
@wait(0.7)
def next_level():
if number + 1 < len(LEVELS):
go("level", number + 1)
else:
go("level", 0)
go("level", 0)
How many coins. total = len(get("coin"))
counts the coins straight after the level is built, so a level can have as
many as you like. Add a $ to the picture and the count is right
without changing anything else.
Bobbing. Each coin swings up and down around where the
level put it, so each one needs to remember that height. The
{"home_y": 0} in its list gives every coin a value of its own,
and the loop after addLevel sets it to where the coin really
is. wave(-3, 3, time() * 5) then swings between 3 pixels above
and 3 below.
The spin. When the last coin is taken,
tween(0, 360, 0.6, spin) turns Bean all the way round in 0.6
seconds, then @wait(0.7) starts the next level. Bean needs
rotate(0) in its list to be able to turn at all. After the last
level, it goes back to level 1 for now.
level_over stops Bean moving and stops more coins counting
while it spins.
Level 2 has lava pits, but = isn't in TILES yet,
so they are just holes. Fall in one and Bean falls for ever. That's the next
step.
Step 5 — the red things
Red means bad. Lava (=) sits still, red blocks
(> and v) move, and touching either one, or
falling off the bottom, starts the level again.
from kaypy import *
TILE = 30 # one tile is 30 pixels square
RUN_SPEED = 375 # how fast Bean runs on the ground, pixels per second
AIR_SPEED = 300 # ...and in the air, a little slower
JUMP = 712 # how hard Bean jumps
JUMP_HOLD = 0.2 # how long holding up keeps pushing Bean higher
ENEMY_SPEED = 150 # how fast the red blocks move
kaypy(width=TILE * 30, height=TILE * 20, background=[67, 117, 210])
setGravity(1350)
loadSprite("bean", "images/bean.png")
loadSprite("coin", "images/coin.png")
loadSprite("steel", "images/steel.png")
loadSound("coin", "sounds/small_beep.wav")
loadSound("ouch", "sounds/thump.wav")
loadSound("done", "sounds/rising_beep.wav")
LEVEL_1 = [
" #",
" #",
"#### #",
" # #",
" # #",
" # #",
" # $ #",
" # #",
" # #### #",
" # # # $ $$ #",
" # # # #",
" # # # #### ##### #### #",
" #### $$ # # # # # # ####",
" # # # $$ # # # # # ",
" # @ # # # # # # $ # ",
" ########## ############ ################# ########## ###################### ",
" #===========# ",
" ############# ",
]
LEVEL_2 = [
" # # # ",
" # # # ",
" # # # ",
" # # # ####",
" # # > # # # # ",
" # # # # ",
" # #======# # ",
" # #======# #### # ",
" # #======# # # $ # ",
" # ######## ### # # ",
" # @ # # ### # ",
" # ### # $ # ",
" # v v # # # ",
" # $ $ $ ### # # $ $ # # ",
" # # # # > # # ",
" ############################################# # ######################### ",
" #===============# ",
" ################# ",
" ",
" ",
]
LEVELS = [LEVEL_1, LEVEL_2]
TILES = {
"#": lambda: [sprite("steel"), scale(TILE / 64), area(),
body(isStatic=True), "ground"],
"=": lambda: [rect(TILE, TILE), color(220, 50, 50), area(), "danger"],
"$": lambda: [sprite("coin"), scale(0.5), area(), "coin",
{"home_y": 0}],
">": lambda: [rect(TILE, TILE), color(255, 100, 100), area(),
"danger", "enemy", {"across": 1, "way": 1}],
"v": lambda: [rect(TILE, TILE), color(255, 100, 100), area(),
"danger", "enemy", {"across": 0, "way": 1}],
"@": lambda: [sprite("bean"), scale(0.4), area(), body(), rotate(0),
z(10), "player"],
}
deaths = 0
coins = 0
hold = 0
level_over = False
@scene("level")
def build_level(number, so_far):
global deaths, coins, hold, level_over
deaths = so_far
coins = 0
hold = 0
level_over = False
level = LEVELS[number]
level_width = len(level[0]) * TILE
addLevel(level, {
"tileWidth": TILE,
"tileHeight": TILE,
"tiles": TILES,
})
bean = get("player")[0]
total = len(get("coin"))
for coin in get("coin"):
coin.home_y = coin.pos.y
label = add([text("", size=24), pos(16, 12), fixed(), z(100)])
def show_score():
label.text = ("Level " + str(number + 1)
+ " Coins " + str(coins) + " / " + str(total)
+ " Deaths " + str(deaths))
show_score()
@onUpdate
def each_frame():
global hold
if level_over:
return
speed = AIR_SPEED
if bean.isGrounded():
speed = RUN_SPEED
if isKeyDown("left"):
bean.move(-speed, 0)
if isKeyDown("right"):
bean.move(speed, 0)
if isKeyDown("up"):
if bean.isGrounded():
bean.jump(JUMP)
hold = JUMP_HOLD
elif hold > 0:
hold = hold - dt()
bean.jump(JUMP)
else:
hold = 0
# Fell off the bottom.
if bean.pos.y > height() + 100:
die()
x = clamp(bean.pos.x, width() / 2, level_width - width() / 2)
setCamPos(vec2(x, height() / 2))
@onUpdate("coin")
def bob(coin):
coin.pos.y = coin.home_y + wave(-3, 3, time() * 5)
@bean.onCollide("coin")
def got_coin(coin):
global coins
if level_over:
return
coin.destroy()
coins = coins + 1
play("coin")
show_score()
if coins == total:
finish()
@onUpdate("enemy")
def patrol(enemy):
if enemy.across == 1:
enemy.move(enemy.way * ENEMY_SPEED, 0)
else:
enemy.move(0, enemy.way * ENEMY_SPEED)
# A red block that touches the ground turns round.
@onCollide("enemy", "ground")
def turn_round(enemy, tile):
enemy.way = -enemy.way
@bean.onCollide("danger")
def ouch(thing):
die()
def die():
global level_over
if level_over:
return
level_over = True
play("ouch")
shake(8)
# The same level again, one more death on the count.
@wait(0.4)
def again():
go("level", number, deaths + 1)
def finish():
global level_over
level_over = True
play("done")
def spin(angle):
bean.angle = angle
tween(0, 360, 0.6, spin)
@wait(0.7)
def next_level():
if number + 1 < len(LEVELS):
go("level", number + 1, deaths)
else:
go("level", 0, 0)
go("level", 0, 0)
One tag for everything that hurts. Lava and red blocks
are both tagged "danger", so one
@bean.onCollide("danger") handles them all. Neither has a
body(), so they float, and Bean goes into them rather than
bouncing off.
Red blocks. Each one carries two values of its own:
across is 1 for a block that moves side to side and 0 for one
that moves up and down, and way is 1 or −1 for which way it's
going. patrol moves each block along, and when a block touches
the ground, turn_round flips its way.
@onCollide("enemy", "ground") is the version that takes two tags,
so it works for every red block and every wall tile at once.
Starting again. die() waits a moment, then
calls go("level", number, deaths + 1): the same level, built
again from nothing. That puts every coin back, because they are made fresh
from the picture. The death count goes in as so_far, because
everything at the top of the scene starts again and a number kept there would
go back to 0.
Why level_over in die()? Bean
can touch lava and a red block in the same moment, and each one calls
die(). Without the check, one mistake would play the sound
twice, shake twice and build the level twice.
Step 6 — the whole game
This is the game exactly as it is on the games page. It adds:
- All five levels.
- Signs. The words written into the levels, like "use the
arrow keys to move", are in
SIGNS: one list for each level, of[column, row, words]. Columns and rows are counted in tiles, the same as the letters, sosign[0] * TILEturns a column into pixels. - A menu. Up starts at level 1.
Down asks which level, with
ask(): it listsLEVEL_NAMES, you click one or press its number, andpicked()is handed the words you chose, such as"Level 3". The loop finds where that is in the list, and that is the level to start. - An ending. After level 5 comes the
"end"scene, with how many times you died. Fewer is better here, so a new best is one that is smaller:total_deaths < best. The first time there is no best at all, which is why it is read withgetData(BEST_KEY, None)and checked withis None. - Escape goes back to the menu.
It also has every number worth changing in one settings block near the top, and comments that explain why each part is there.
"""Dark Blue -- jump, collect the coins, and avoid the red stuff.
left / right run
up jump (hold it to jump higher)
escape back to the menu
Collect every coin on a level to go on to the next. Touch anything red --
lava or a moving red block -- or fall off the bottom, and the level starts
again. There are five levels. The game counts how many times you died, and
remembers your fewest.
Based on "Dark Blue" by Thomas Palef (lessmilk.com).
HOW THIS FILE IS PUT TOGETHER
1. the window, the pictures and the sounds
2. the settings
3. the levels
4. what each letter turns into
5. the menu
6. the level scene: Bean, the camera, the coins, the red stuff
7. the end
8. start the game
The levels are pictures made of letters. Move a '$' and you have moved a
coin -- you never have to touch the code to change a level.
"""
from kaypy import *
# ----------------------------------------------------------------- 1. setup
TILE = 30 # one tile is 30 pixels square
# 30 tiles across and 20 down fit in the window. A level is wider than that,
# so the camera follows Bean from side to side.
kaypy(width=TILE * 30, height=TILE * 20, background=[67, 117, 210])
setGravity(1350)
loadSprite("bean", "images/bean.png")
loadSprite("coin", "images/coin.png")
loadSprite("steel", "images/steel.png")
loadSound("coin", "sounds/small_beep.wav")
loadSound("ouch", "sounds/thump.wav")
loadSound("done", "sounds/rising_beep.wav")
# -------------------------------------------------------------- 2. settings
#
# Every number worth changing is here.
RUN_SPEED = 375 # how fast Bean runs on the ground, pixels per second
AIR_SPEED = 300 # ...and in the air, a little slower
JUMP = 712 # how hard Bean jumps
JUMP_HOLD = 0.2 # how long holding up keeps pushing Bean higher
ENEMY_SPEED = 150 # how fast the red blocks move
# The name the fewest deaths is saved under. Every game on a website shares
# the same storage, so the game's own name goes in it.
BEST_KEY = "dark_blue_fewest_deaths"
# ---------------------------------------------------------------- 3. levels
#
# One letter is one tile.
#
# # ground $ coin @ Bean starts here
# = lava > red block that moves side to side
# v red block that moves up and down
#
# A space is empty.
LEVEL_1 = [
" #",
" #",
"#### #",
" # #",
" # #",
" # #",
" # $ #",
" # #",
" # #### #",
" # # # $ $$ #",
" # # # #",
" # # # #### ##### #### #",
" #### $$ # # # # # # ####",
" # # # $$ # # # # # ",
" # @ # # # # # # $ # ",
" ########## ############ ################# ########## ###################### ",
" #===========# ",
" ############# ",
]
LEVEL_2 = [
" # # # ",
" # # # ",
" # # # ",
" # # # ####",
" # # > # # # # ",
" # # # # ",
" # #======# # ",
" # #======# #### # ",
" # #======# # # $ # ",
" # ######## ### # # ",
" # @ # # ### # ",
" # ### # $ # ",
" # v v # # # ",
" # $ $ $ ### # # $ $ # # ",
" # # # # > # # ",
" ############################################# # ######################### ",
" #===============# ",
" ################# ",
" ",
" ",
]
LEVEL_3 = [
" # # # # ",
" # # # # ",
" # # # # ",
" ###### # # # # ",
" # # # # # # ",
" # ## # $ $ $ # # # $$ # ",
" # # # # ## # # ",
" # #### #### ####### # # # # ",
" # # # # # # # # ",
" # # # # > # # ## $$ # ",
" # @ #### # # # > # $ v $ # # # ",
" # # # # # # # # # # ",
" # # # # # # # $ $ ## # # ",
" # #### # # # # # #====# ",
" # # # # # # # # ### # ### #====# ",
" # # #==# #==# #======= # # #===================# # #====# ",
" ################ #### #### #######= ############# ##################### ################# ",
" = ",
" = ",
" = ",
]
LEVEL_4 = [
" # # # ",
"######### ########################### ####### ",
" # = = # ",
" # # ",
" # # ",
" ###### = = # ",
" # ########################### ### # ",
" # ##### # #> # # # ",
" # # # ######################## # > # # # ",
" # # # v # # > # # $ # ",
" # @ # # $ $ $ # # ># # # ",
" # $ $ ### ### v # ########################### ##### ",
" # # # $ $ $ # ",
" ###### ##### # # v # ",
" #=======# # # # $ $ $ # ",
" #=======# # # # v # ",
" ######### ########## ############################### ",
" ",
" ",
" ",
]
LEVEL_5 = [
" # = # # # # # ",
" # = # # # # # ",
" # = # ####################### # ># ",
" # = # $ $ $ # > # ",
" # = # #> # ",
" # = # @ ########## # ",
" # = # ## # # # ## # ",
" # = # #=======================# # # # # # ",
" # = # #=======================# #========#========#========# # ",
" # = ######################################################################################### # ",
" # = # ",
" # = $ $ $ $ $ $ ### ### # ",
" # = v v v # # # # # ",
" # = $ v ### $ $ $ $ $ # ",
" # = # # # > # # ",
" #=== $ ### v ### v ### v ### ### # # ################### vvv vvv # ",
" #===== # # # ",
" #=================#=======#=======#================# # # ",
" ##################################################=# ########################################### ",
" = ",
]
LEVELS = [LEVEL_1, LEVEL_2, LEVEL_3, LEVEL_4, LEVEL_5]
LEVEL_NAMES = ["Level 1", "Level 2", "Level 3", "Level 4", "Level 5"]
# The words written into each level, as [column, row, words]. Columns and
# rows are counted in tiles, from 0, the same as the letters above.
SIGNS = [
[[12, 3, "use the arrow keys to move"],
[40, 17, "coins are awesome, take them"],
[66, 3, "red things = bad things"],
[87, 17, "collect all coins to finish the level"]],
[[15, 1, "right, left, right... this looks boring"],
[59, 1, "use the up arrow key to jump"],
[59, 3, "but you probably already know that"]],
[],
[[79, 14, "there are so many red things"],
[79, 16, "stop reading this!"]],
[[15, 1, "this is the last level"],
[15, 3, "you can't beat it"]],
]
# ----------------------------------------------- 4. what each letter becomes
#
# addLevel() reads the picture. Each time it finds a letter it looks it up
# here and builds an object from the list. The `lambda:` means "build a new
# one every time you find this letter", so every coin is its own coin.
#
# The red blocks have no body(), so gravity leaves them alone and they
# float. Each one carries its own direction: "across" is 1 for side to side
# and 0 for up and down, and "way" is 1 or -1 for which way it is going.
TILES = {
"#": lambda: [sprite("steel"), scale(TILE / 64), area(),
body(isStatic=True), "ground"],
"=": lambda: [rect(TILE, TILE), color(220, 50, 50), area(), "danger"],
"$": lambda: [sprite("coin"), scale(0.5), area(), "coin",
{"home_y": 0}],
">": lambda: [rect(TILE, TILE), color(255, 100, 100), area(),
"danger", "enemy", {"across": 1, "way": 1}],
"v": lambda: [rect(TILE, TILE), color(255, 100, 100), area(),
"danger", "enemy", {"across": 0, "way": 1}],
# Bean is a tile too, so moving the '@' moves where you start.
# scale(0.4) makes Bean a little smaller than a tile, so it fits
# through a gap one tile wide.
"@": lambda: [sprite("bean"), scale(0.4), area(), body(), rotate(0),
z(10), "player"],
}
# ---------------------------------------------- what the game has to remember
#
# 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".
deaths = 0 # how many times you have died, this game
coins = 0 # coins collected on this level
hold = 0 # seconds of jump-holding left
level_over = False
# -------------------------------------------------------------- 5. the menu
@scene("menu")
def menu():
add([text("Dark Blue", size=80), pos(width() / 2, 170),
anchor("center")])
add([text("Jump, collect coins, avoid the red stuff", size=26),
pos(width() / 2, 250), anchor("center")])
start = add([text("press UP to start, or DOWN to choose a level", size=28),
pos(width() / 2, height() - 120), anchor("center"),
rotate(0)])
best = getData(BEST_KEY, None)
if best is not None:
add([text("fewest deaths: " + str(best), size=22),
pos(width() / 2, 330), anchor("center")])
add([text('based on "Dark Blue" by Thomas Palef (lessmilk.com)', size=16),
pos(width() / 2, height() - 30), anchor("center"),
color(200, 200, 220)])
# wave() swings between two numbers for ever, so the words rock from
# side to side.
@onUpdate
def rock():
start.angle = wave(-1.5, 1.5, time() * 3)
@onKeyPress("up")
def play_from_the_start():
go("level", 0, 0)
@onKeyPress("down")
def choose():
# ask() lists the levels. Click one, or press its number, and
# picked() is handed the words you chose -- "Level 3", say. The loop
# finds which one that was, and starts that level.
@ask("Which level?", LEVEL_NAMES)
def picked(choice):
for i in range(len(LEVEL_NAMES)):
if LEVEL_NAMES[i] == choice:
go("level", i, 0)
# ------------------------------------------------------ 6. the level scene
#
# number is which level (0 is the first), and so_far is how many times you
# have died already. Dying starts the same level again with so_far + 1,
# which puts every coin back where it was.
#
# The scene is CALLED "level". 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 level instead of making a noise.
@scene("level")
def build_level(number, so_far):
global deaths, coins, hold, level_over
deaths = so_far
coins = 0
hold = 0
level_over = False
level = LEVELS[number]
level_width = len(level[0]) * TILE
addLevel(level, {
"tileWidth": TILE,
"tileHeight": TILE,
"tiles": TILES,
})
bean = get("player")[0]
total = len(get("coin"))
# Each coin bobs up and down around where the level put it, so it has
# to remember where that was.
for coin in get("coin"):
coin.home_y = coin.pos.y
for sign in SIGNS[number]:
add([text(sign[2], size=22), pos(sign[0] * TILE, sign[1] * TILE)])
label = add([text("", size=24), pos(16, 12), fixed(), z(100)])
def show_score():
label.text = ("Level " + str(number + 1)
+ " Coins " + str(coins) + " / " + str(total)
+ " Deaths " + str(deaths))
show_score()
@onKeyPress("escape")
def back_to_menu():
go("menu")
# ------------------------------------------------------- every frame
@onUpdate
def each_frame():
global hold
if level_over:
return
# Running. A little slower in the air, so a jump is easier to aim.
speed = AIR_SPEED
if bean.isGrounded():
speed = RUN_SPEED
if isKeyDown("left"):
bean.move(-speed, 0)
if isKeyDown("right"):
bean.move(speed, 0)
# Jumping. A tap is a small jump. Keep holding up and Bean keeps
# being pushed upward for JUMP_HOLD seconds, so it goes higher.
if isKeyDown("up"):
if bean.isGrounded():
bean.jump(JUMP)
hold = JUMP_HOLD
elif hold > 0:
hold = hold - dt()
bean.jump(JUMP)
else:
# Let go, and the push is over until Bean lands again.
hold = 0
# Fell off the bottom.
if bean.pos.y > height() + 100:
die()
# The camera follows Bean from side to side, but stops at the ends
# of the level so you never see past them. It never moves up or
# down: the whole level is exactly one window tall.
x = clamp(bean.pos.x, width() / 2, level_width - width() / 2)
setCamPos(vec2(x, height() / 2))
# ------------------------------------------------------------- coins
@onUpdate("coin")
def bob(coin):
coin.pos.y = coin.home_y + wave(-3, 3, time() * 5)
@bean.onCollide("coin")
def got_coin(coin):
global coins
if level_over:
return
coin.destroy()
coins = coins + 1
play("coin")
show_score()
if coins == total:
finish()
# ------------------------------------------------------ the red stuff
@onUpdate("enemy")
def patrol(enemy):
if enemy.across == 1:
enemy.move(enemy.way * ENEMY_SPEED, 0)
else:
enemy.move(0, enemy.way * ENEMY_SPEED)
# A red block that touches the ground turns round.
@onCollide("enemy", "ground")
def turn_round(enemy, tile):
enemy.way = -enemy.way
@bean.onCollide("danger")
def ouch(thing):
die()
def die():
global level_over
# Bean can touch lava and a red block at the same moment, which
# would otherwise be two deaths.
if level_over:
return
level_over = True
play("ouch")
shake(8)
# The same level again, one more death on the count. go() builds
# it from scratch, so every coin is back.
@wait(0.4)
def again():
go("level", number, deaths + 1)
# ------------------------------------------------------- level done
def finish():
global level_over
level_over = True
play("done")
# Spin Bean round once, then go on.
def spin(angle):
bean.angle = angle
tween(0, 360, 0.6, spin)
@wait(0.7)
def next_level():
if number + 1 < len(LEVELS):
go("level", number + 1, deaths)
else:
go("end", deaths)
# ---------------------------------------------------------------- 7. the end
@scene("end")
def the_end(total_deaths):
best = getData(BEST_KEY, None)
if best is None or total_deaths < best:
setData(BEST_KEY, total_deaths)
best_line = "That's your best yet!"
else:
best_line = "Your best is " + str(best) + "."
if total_deaths == 1:
died = "You died 1 time."
else:
died = "You died " + str(total_deaths) + " times."
add([text("You beat Dark Blue!", size=60), pos(width() / 2, 180),
anchor("center")])
add([text(died + "\n" + best_line, size=30), pos(width() / 2, 300),
anchor("center")])
again = add([text("press UP to play again", size=28),
pos(width() / 2, height() - 120), anchor("center"),
rotate(0)])
@onUpdate
def rock():
again.angle = wave(-1.5, 1.5, time() * 3)
@onKeyPress("up")
def play_again():
go("menu")
# --------------------------------------------------------- 8. start the game
go("menu")
Make it yours
Open the whole game in the playground → It goes straight into the editor, ready to change and run.
- Change how Bean moves. Make
JUMP_HOLDbigger and held jumps go higher. LowersetGravityand Bean floats. - Draw level 6. Copy a level, change the letters, and add
it to
LEVELS. Add"Level 6"toLEVEL_NAMESas well, and a list of signs toSIGNS, even an empty one,[]. Forget the signs and level 6 crashes withIndexError: list index out of range, becauseSIGNS[5]doesn't exist. - A faster red block. Pick a spare letter, like
f, and add it toTILESas a copy of the>line. With the"danger"and"enemy"tags it already moves, turns round and hurts. To make it faster, give every red block a"speed"of its own next to"way", and useenemy.speedinpatrolinstead ofENEMY_SPEED. - Use WASD as well. Change
isKeyDown("left")toisKeyDown("left") or isKeyDown("a"), and the same for the others.