Tutorials
Whole games, short enough to read in one sitting.
The guide teaches one idea at a time and builds up. This page is the other half of that: finished programs you can copy into the playground, press Run, and then take apart. Nothing here is a fragment — every listing is a complete file that runs as it stands.
How to use one. Copy it, run it, then break something on purpose. Change a number and see what moves. That tells you more about what a line does than reading it does.
Build one from scratch
Building Ghost Chase → — the easiest game on
the site, and the one to build first: you can do it before you have learned
functions. Bean grabs coins and two ghosts chase. Six steps, each one a
whole program, using only variables, if, a for
loop and a list.
Building Bomb Dodge → — the smallest game on the site that writes functions of its own, and the one to build next. Bombs fall, Bean runs out of the way, and every bomb that misses is a point. Six steps, each one a whole program, in plain Python with no list comprehensions and nothing that isn't on the API page.
Building Goblin Hit → — the one for after dictionaries. Monsters pop up out of nine holes and you click them before they duck. Each monster's name is the key that looks up its points and how long it stays up, and a dictionary counts what you hit. Six steps, each one a whole program, in the same plain Python.
Building Log Hacker → — the one for after
files.
Guess a server's password from a list of words, in four tries, with a log
that says how close each guess was, and no clock. The game writes its own
files with with open(...), then reads them back. Six steps, each
one a whole program, in the same plain Python, and every "break it" is a real
mistake with files.
Building Paddle Ball → — Pong for one player, and the step between Bomb Dodge and Pong. A ball that carries its own speed bounces off three walls and your paddle, a little faster every hit. Six steps, each one a whole program, in the same plain Python, with no pictures at all.
Building Spiral Storm → — a bullet hell,
and a break from paddles between Paddle Ball and Pong. The camera zooms out,
bullets fly by themselves, a ring is a for loop over angles and a
spiral is one angle that keeps turning. Six steps, each one a whole program,
in the same plain Python.
Building Pong → — you against the computer, with two paddles and a ball that carries its own speed. Bouncing is just making a number negative. Seven steps, each one a whole program, in the same plain Python, with no pictures at all.
Building Flappy Bean → — another short one, with gravity this time. Bean flaps, pipes slide past, and the game gets harder as you score. Six steps, each one a whole program, in plain Python with no list comprehensions and nothing that isn't on the API page.
Building Coin Rush → — Bean, a clock, and every coin on the level before it runs out. A good second one: seven steps, each one a whole program, in the same plain Python.
Building Coin Dash → — Bean runs and jumps round one room, chasing a coin and dodging red blocks that drop in from the ceiling. The next step after Coin Rush: the same plain Python, but things fall. Six steps, each one a whole program.
Building Block Slide → — a puzzle you play with your finger. No game objects at all: the board is a list of dictionaries, and dragging, falling and clearing rows are loops over it. Six steps, each one a whole program.
Building Knight's Hall → — a game for two thumbs. An on-screen d-pad and buttons that pretend to be keys, walking in eight directions, a sword that points the way you face, and monsters that come in waves. Six steps, each one a whole program.
Building Pop Drop → — a bubble shooter in the style of Snood. Aim with the arrow keys, bounce off the walls, and pop three of a kind. The board is a honeycomb made of a list of lists, and a flood fill finds what pops and what falls. Seven steps, each one a whole program.
Building Garden Guard → — a game in the style of Plants vs. Zombies, played by clicking or tapping. Suns to collect, plants that cost them, zombies that eat the plants, peas, waves, lawnmowers and levels. Seven steps, each one a whole program.
Building Spike Runner → — a one-button game in the style of Geometry Dash. The cube runs by itself; you jump over spikes, onto blocks, across gaps and off orbs. The level is built from short sections joined end to end. Six steps, each one a whole program.
Building Dark Blue → — a platform game whose levels are wider than the window, so a camera follows Bean along them. Hold the key to jump higher, collect every coin, and keep away from anything red. Six steps, each one a whole program.
Building Dungeon Dash → — a platform game with five rooms, a new move behind every door, and a boss. Built up a step at a time, with the four bugs that turned up along the way and why none of them produced an error message.
Building a turn-based battle → — the knight on the right, monsters on the left, Attack / Magic / Item / Run. A shorter one, and the interesting part is what it does not use: there is no game object anywhere in it.
Whole games
A door that asks a question — what ask(), say() and pause() are for
python3 quiz_door.py
Arrows to move, space to jump, escape to pause. Walk into the door and the game stops and asks you something; get it right and the door opens.
Not one of the thirteen lessons: it is the smallest complete example of the one thing a teacher asks for that a game engine usually cannot do. Three names here appear nowhere in the lessons:
ask(question, choices, answer=n) stop and ask, then run a callback
say(text) stop and show a line of text
pause() / resume() / isPaused() freeze the game where it stands
The question is a callback rather than a return value because a browser
cannot block: nothing can wait for an answer without stopping the frame that
would draw the question. So ask puts the panel up and hands you the answer
when there is one, and the game is paused in between.
from kaypy import *
kaypy(width=800, height=600, background=[141, 183, 255])
loadSprite("bean", "images/bean.png")
setGravity(2400)
add([rect(width(), 48), pos(0, height() - 48), area(), body(isStatic=True),
color(90, 150, 70)])
player = add([sprite("bean"), pos(80, 100), area(), body(), anchor("bot")])
door = add([rect(40, 90), pos(650, height() - 138), area(), color(140, 90, 40),
"door"])
score = 0
label = add([text("Score: 0", size=26), pos(12, 12), fixed()])
@onKeyDown("left")
def left():
player.move(-320, 0)
@onKeyDown("right")
def right():
player.move(320, 0)
@onKeyPress("space")
def jump():
if player.isGrounded():
player.jump(1000)
@player.onCollide("door")
def at_the_door(d):
@ask("Which keyword starts a loop in Python?", ["if", "for", "def"], answer=1)
def checked(correct):
global score
if correct:
score += 1
label.text = "Score: %d" % score
d.destroy()
say("The door swings open.")
else:
say("Not that one. Have another go.")
@onKeyPress("escape")
def toggle_pause():
resume() if isPaused() else pause()
examples/quiz_door.py · 71 lines
A pet that trails you and a health bar that never does — what follow() is for
python3 pet_and_healthbar.py
Arrows to move, space to jump, F to hurt the enemy. The ghost chases you and falls behind when you run; the red bar over the enemy is welded to it and never falls behind at all.
Both of those are one component. The difference between them is the word
speed:
follow(target) be exactly where it is
follow(target, speed=180) move toward it at 180 pixels a second
The bar is the interesting half. It is pinned to an object that gravity moves, that collision shoves out of the floor, and that a keypress teleports — and it stays put through all three, because follow() runs after the engine has finished deciding where everything actually ended up, rather than before like every other component. Without that it would sit correctly while the enemy stands still and slide off it the moment the enemy moved, which looks like a drawing bug and is not one.
from kaypy import *
kaypy(width=800, height=600, background=[141, 183, 255])
loadSprite("bean", "images/bean.png")
loadSprite("ghosty", "images/ghosty.png")
setGravity(1600)
add([rect(width(), 48), pos(0, height() - 48), area(),
body(isStatic=True), color(90, 150, 70)])
player = add([sprite("bean"), pos(120, 300), area(), body()])
# Locked on: no speed, so it is simply where the player is, plus an offset.
add([text("you", size=18), pos(0, 0), color(30, 40, 60),
follow(player, offset=vec2(4, -28))])
# Chasing: with a speed, it heads for the player and arrives when it arrives.
add([sprite("ghosty"), pos(600, 200), follow(player, speed=180)])
enemy = add([sprite("ghosty"), pos(560, 400), area(), body(), health(5)])
# A health bar over something gravity is pulling down and the floor is
# pushing back up. This is the case that shows whether follow() runs early
# or late.
BAR = 48
backing = add([rect(BAR, 6), pos(0, 0), color(40, 20, 20),
follow(enemy, offset=vec2(0, -14))])
bar = add([rect(BAR, 6), pos(0, 0), color(220, 60, 60),
follow(enemy, offset=vec2(0, -14))])
@onKeyDown("left")
def go_left():
player.move(-320, 0)
@onKeyDown("right")
def go_right():
player.move(320, 0)
@onKeyPress("space")
def jump():
if player.isGrounded():
player.jump(760)
@onKeyPress("f")
def hurt():
if enemy.exists():
enemy.hurt(1)
bar.width = BAR * max(0, enemy.hp) / 5
@enemy.onDeath
def gone():
# A follower whose target is destroyed stops where it stands and goes on
# existing — which is right for a pet and wrong for a health bar, so the
# bars are destroyed here, next to the thing they belonged to.
enemy.destroy()
backing.destroy()
bar.destroy()
add([text("arrows move · space jumps · F hurts the ghost", size=20),
pos(12, 12), color(20, 30, 50), fixed()])
examples/pet_and_healthbar.py · 89 lines
A sword that goes where the knight goes — what child objects are for
python3 knight_and_camera.py
Arrows to move, space to swing, 1/2/3 to change the camera. The sword, the shield and the name tag are all children of the knight: nothing in the movement code mentions any of them, and they follow anyway.
knight = add([sprite("bean"), pos(200, 300), area()])
sword = knight.add([rect(8, 34), pos(26, -4), color(220, 220, 235)])
A child's pos() is measured from its parent, not from the screen. The
sword sits at +26, -4 from the knight and stays there through walking,
turning and being teleported, because there is nothing to keep in step —
it is one position, expressed once. Destroy the knight and the sword goes
with it.
That is the difference between this and follow(): a follower is a separate object being kept in agreement every frame, which is what you want when the two things meet later in the game. A child is part of the thing.
THE CAMERA KEYS
1 setCamScale(1) normal
2 setCamScale(2) zoomed in, both ways
3 setCamScale(vec2(2, 0.6)) wide and squashed
The third is the one worth pressing. The scale is a vec2, so the two axes can differ — a letterboxed cutscene, a squash as something lands, or a deliberately wrong aspect ratio for a dream sequence. Everything obeys it together: sprites, the children, the drawn cone of the swing, and the debug boxes under F1.
import math
from kaypy import *
kaypy(width=800, height=600, background=[26, 30, 42])
loadSprite("bean", "images/bean.png")
add([rect(2000, 40), pos(-600, 470), area(), body(isStatic=True),
color(60, 70, 90)])
setGravity(1800)
knight = add([sprite("bean"), pos(200, 300), area(), body(), "knight"])
# Everything below hangs off the knight. None of it is mentioned again in
# the movement code.
sword = knight.add([rect(8, 34), pos(26, -4), color(220, 220, 235),
anchor("bot"), rotate(0)])
knight.add([rect(6, 24), pos(-10, 4), color(150, 110, 60)]) # shield
knight.add([text("Sir Bean", size=14), pos(-4, -22), color(200, 210, 230)])
add([text("arrows move · space swings · 1 2 3 camera", size=18),
pos(12, 12), color(140, 150, 170), fixed()])
swing = [0.0]
@onKeyDown("left")
def go_left():
knight.move(-240, 0)
@onKeyDown("right")
def go_right():
knight.move(240, 0)
@onKeyPress("space")
def jump_or_swing():
swing[0] = 0.35
if knight.isGrounded():
knight.jump(700)
@onUpdate
def animate_sword():
if swing[0] > 0:
swing[0] = max(0.0, swing[0] - dt())
# A swing is just the child's own angle. The knight knows nothing.
sword.angle = -110 * math.sin((0.35 - swing[0]) / 0.35 * math.pi)
else:
sword.angle = 0
@onUpdate
def chase():
setCamPos(vec2(knight.pos.x, 300))
@onKeyPress("1")
def normal():
setCamScale(1)
@onKeyPress("2")
def close():
setCamScale(2)
@onKeyPress("3")
def letterbox():
setCamScale(vec2(2, 0.6))
examples/knight_and_camera.py · 105 lines
Sneak past a guard who cannot see through walls — what sentry() is for
python3 stealth_guard.py
Arrows to move. The guard sweeps a torch left and right; get into the cone and it spots you. Hide behind a crate and it does not, because the cone is light and the crate is solid.
The whole of the seeing is one component:
sentry("player", fieldOfView=70, range=300, lineOfSight=True)
fieldOfView is the width of the cone in degrees. range is how far the
torch reaches — not KAPLAY's, added because "how close can I get" is the
question a stealth game is made of. lineOfSight casts a ray at you and
lets the crates stop it.
There is no direction here on purpose: without one, the sentry looks
wherever rotate() has the guard turned, so sweeping the torch is a matter
of changing guard.angle and the cone follows.
The cone you can see is drawn by hand in onDraw, out of the same three numbers the component was given. Drawing it is worth the twenty lines: a vision cone you cannot see is a rule the player has to infer by dying.
import math
from kaypy import *
kaypy(width=800, height=600, background=[18, 20, 30])
loadSprite("bean", "images/bean.png")
loadSprite("ghosty", "images/ghosty.png")
FOV = 70
RANGE = 300
player = add([sprite("bean"), pos(80, 500), area(), "player"])
# Crates. Solid, so they stop the guard's ray — and tagged, so the ray can
# be told about them.
for x, y in [(300, 180), (300, 260), (300, 340), (560, 420), (560, 500)]:
add([rect(56, 56), pos(x, y), area(), color(90, 74, 58), outline(2),
"crate"])
guard = add([
sprite("ghosty"), pos(400, 120), area(), rotate(90),
sentry("player", fieldOfView=FOV, range=RANGE, lineOfSight=True),
"guard",
])
caught = add([text("", size=28), pos(12, 12), color(255, 120, 120), fixed()])
add([text("arrows to move · hide behind the crates", size=18),
pos(12, 560), color(150, 160, 180), fixed()])
@onKeyDown("left")
def go_left():
player.move(-220, 0)
@onKeyDown("right")
def go_right():
player.move(220, 0)
@onKeyDown("up")
def go_up():
player.move(0, -220)
@onKeyDown("down")
def go_down():
player.move(0, 220)
# The torch sweeps between 50 and 130 degrees — down and to either side.
@onUpdate
def sweep():
guard.angle = 90 + 40 * math.sin(time() * 0.8)
@guard.onObjectsSpotted
def spotted(objects):
caught.text = "Spotted!"
shake(8)
@onUpdate
def forget():
# .spotted is the current answer, for the frames between edges.
if not guard.spotted and caught.text:
caught.text = ""
@onDraw
def draw_cone():
"""The cone, drawn from the same numbers the sentry was given.
Its far edge is cut short wherever a crate is in the way, which is the
same question the component asks — so what you see and what the guard
sees cannot disagree.
"""
seeing = bool(guard.spotted)
edge = (255, 210, 120) if not seeing else (255, 120, 120)
points = [guard.pos]
steps = 24
for i in range(steps + 1):
angle = guard.angle - FOV / 2 + FOV * i / steps
along = Vec2.fromAngle(angle)
hit = raycast(guard.pos, along, exclude=["player"], ignore=[guard],
max_distance=RANGE)
reach = hit.distance if hit else RANGE
points.append(guard.pos + along * reach)
drawLines(points=points, width=2, color=edge, opacity=0.55, close=True)
examples/stealth_guard.py · 117 lines
Asteroids — what rotate() is for
python3 asteroids.py
Left and right turn the ship, up thrusts in the direction it is facing, space fires. Rocks drift, split when hit, and wrap around the screen.
Not one of the thirteen lessons: it is the game the lessons build towards, and the one that cannot be written at all without a ship that turns. Three things here appear nowhere in the lessons and are the whole point of it:
rotate(angle) the ship turns
Vec2.fromAngle(angle) which way "forward" is, once it has turned
a velocity of its own momentum, so letting go of thrust coasts
from kaypy import *
kaypy(width=800, height=600, background=[8, 8, 20])
TURN = 200 # degrees per second
THRUST = 320 # pixels per second per second
MAX_SPEED = 420
BULLET_SPEED = 560
score = 0
ship = add([
rect(26, 18),
pos(center()),
anchor("center"),
color(200, 230, 255),
rotate(0),
area(),
"ship",
])
ship.vel = vec2(0, 0)
label = add([text("0", size=22), pos(12, 10), fixed()])
def wrap(obj):
"""Off one edge, on at the other — the rule that makes it Asteroids."""
p = obj.pos
if p.x < 0:
obj.pos = vec2(width(), p.y)
elif p.x > width():
obj.pos = vec2(0, p.y)
p = obj.pos
if p.y < 0:
obj.pos = vec2(p.x, height())
elif p.y > height():
obj.pos = vec2(p.x, 0)
def spawn_rock(at=None, chunks=3):
r = add([
circle(chunks * 11),
pos(at or vec2(rand(0, width()), rand(0, 60))),
anchor("center"),
color(150, 140, 130),
outline(2, (90, 85, 80)),
area(),
rotate(rand(0, 360)),
"rock",
])
r.vel = Vec2.fromAngle(rand(0, 360)) * rand(40, 110)
r.spin = rand(-90, 90)
r.chunks = chunks # 3, 2, 1 — splits down to nothing
return r
for _ in range(4):
spawn_rock()
@onKeyDown("left")
def turn_left():
ship.rotateBy(-TURN * dt())
@onKeyDown("right")
def turn_right():
ship.rotateBy(TURN * dt())
@onKeyDown("up")
def thrust():
ship.vel = ship.vel + Vec2.fromAngle(ship.angle) * THRUST * dt()
if ship.vel.len() > MAX_SPEED:
ship.vel = ship.vel.unit() * MAX_SPEED
@onKeyPress("space")
def fire():
b = add([
circle(3),
pos(ship.pos),
anchor("center"),
color(255, 240, 160),
area(),
"bullet",
])
b.vel = Vec2.fromAngle(ship.angle) * BULLET_SPEED
wait(1.2, lambda: b.destroy() if b.exists() else None)
@onUpdate
def fly():
ship.pos = ship.pos + ship.vel * dt()
wrap(ship)
for r in get("rock"):
r.pos = r.pos + r.vel * dt()
r.rotateBy(r.spin * dt())
wrap(r)
for b in get("bullet"):
b.pos = b.pos + b.vel * dt()
wrap(b)
@onUpdate
def shooting():
global score
for b in get("bullet"):
for r in get("rock"):
if not (b.exists() and r.exists()):
continue
if b.pos.dist(r.pos) < r.chunks * 11:
b.destroy()
r.destroy()
score += 10 * r.chunks
label.text = str(score)
shake(6)
if r.chunks > 1:
for _ in range(2):
spawn_rock(r.pos, r.chunks - 1)
break
examples/asteroids.py · 136 lines
The lesson programs
Each of these is the finished program from one lesson of the guide, which explains it a piece at a time. Read the lesson; keep the file to run.
| What it shows | Lines | ||
|---|---|---|---|
| 1 | Adding a game object | 12 | source |
| 2 | Player movement | 25 | source |
| 3 | Collision handling | 48 | source |
| 5 | Gravity | 39 | source |
| 6 | Sprite animation | 67 | source |
| 7 | Scenes | 91 | source |
| 8 | Audio and buttons | 38 | source |
| 9 | Timer and loop | 17 | source |
| 10 | Levels | 43 | source |
| 11 | Camera | 58 | source |
| 12 | Sprite atlas | 100 | source |
| 13 | Using state to handle AI | 61 | source |
Coming next
Coin Collector, a thirteen-step platform game built from nothing — the one to work through once the guide's lessons make sense. It exists as a classroom walkthrough written for a different engine, and is being rewritten for KayPy rather than translated, because the two think about a game in genuinely different ways and a line-by-line translation would teach the seams instead of the game.
If you are teaching with this and want it sooner, or want something else first, say so on the Discord.