Building Goblin Hit
Play it first → Monsters pop up out of nine holes. Click one before it ducks back down. Every kind is worth something different, and the quick ones are worth the most. Don't click the angel. After thirty seconds you see what you hit, and your best score is remembered.
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. The pictures are the dungeon monsters from the playground's Sprites button.
It is the game for after
Lesson 8 — Dictionaries, and it
comes after Ghost Chase and
Bomb Dodge. It uses the same pieces as Bomb
Dodge: functions, global, wait() and a scene. If
any of those are new, build Bomb Dodge first. After this one, try
Log Hacker, then
Paddle Ball.
The one big idea: the name looks everything up
Every monster has a name: "goblin", "imp",
"angel". That name is the key to everything about it. It is the
name its picture is loaded under, so sprite("imp") shows it. It
is the key in a dictionary of points, so POINTS["imp"] is what an
imp is worth. It is the key in a dictionary of times, so
STAY["imp"] is how long an imp stays up. And at the end, it is
the key in a dictionary that counts how many imps you hit.
Nothing in the game ever says if it is an imp, then…. It looks the name up. That is why adding a monster of your own is one new line in each dictionary, and no new code at all.
Step 1 — a goblin to click
Nine holes and one goblin. Click the goblin and it jumps to another hole.
from kaypy import *
kaypy(width=800, height=600, background=[72, 62, 78])
loadSprite("goblin", "dungeon/goblin.png", sliceX=8)
# The nine holes, in three rows of three. A hole is a dark rectangle
# with round ends: radius=20 rounds its corners.
for row in range(3):
for column in range(3):
x = 220 + column * 180
y = 230 + row * 150
add([rect(120, 40, radius=20), pos(x, y), anchor("center"),
color(20, 14, 24), "hole"])
score = 0
score_label = add([text("0", size=48), pos(width() / 2, 40),
anchor("center")])
# anchor("bot") puts the goblin's feet in a hole. area() is what lets
# isHovering() ask whether the mouse is over it.
goblin = add([sprite("goblin"), pos(400, 388), anchor("bot"), scale(5),
area()])
def jump_to_a_hole():
hole = choose(get("hole"))
goblin.pos = vec2(hole.pos.x, hole.pos.y + 8)
# onClick runs on every click, wherever it is. A tap on a phone counts.
@onClick
def click():
global score
if goblin.isHovering():
score = score + 1
score_label.text = str(score)
jump_to_a_hole()
- The holes are two loops, one inside the other. The
outside loop goes round three times, once for each row. Each time round, the
inside loop goes round three times, once for each column. That's nine holes.
220 + column * 180puts the columns at 220, 400 and 580 across;230 + row * 150puts the rows at 230, 380 and 530 down. Each hole is tagged"hole", soget("hole")can find them all again. - A hole is a rectangle with round ends.
rect(120, 40, radius=20)is 120 pixels wide and 40 tall, and a radius of 20 (half its height) rounds the corners all the way into a curve. - The goblin is
scale(5). The dungeon pictures are tiny, 16 pixels across, so the goblin is drawn five times as big.sliceX=8where it is loaded says the picture is eight goblins in a row (the frames of its animation), and to cut it into eight. The game only ever shows the first. @onClickrunsclickon every click, wherever it is. A tap on a phone counts as a click.goblin.isHovering()asks whether the mouse is over the goblin right now. If it is, that's a point, and the goblin jumps.jump_to_a_hole()picks one of the nine holes withchoose()and moves the goblin's feet to it. It is picked at random, so now and then it is the hole the goblin was already in, and the goblin seems not to move at all.
Break it. Take area()
out of the goblin's list. The first click, anywhere at all, stops the game
with
AttributeError: GameObj has no attribute 'isHovering' — did you forget a component that provides it?
area() is what gives the goblin a shape the mouse can be
over. Without it there is no isHovering() to ask, and the
message says so: "has no attribute" in kaypy nearly always means a component
is missing from the list.
Step 2 — popping up and ducking
Now the goblins come up by themselves. Every 0.7 seconds one comes up in a hole, and 1.4 seconds later it ducks back down, so there are often two up at once. Click one before it goes.
from kaypy import *
STAY = 1.4 # how long a goblin stays up, in seconds
WAIT = 0.7 # seconds between goblins
kaypy(width=800, height=600, background=[72, 62, 78])
loadSprite("goblin", "dungeon/goblin.png", sliceX=8)
# The nine holes. The dictionary at the end, {"busy": False}, gives each
# hole a value of its own: hole.busy is True while a goblin is in it.
for row in range(3):
for column in range(3):
x = 220 + column * 180
y = 230 + row * 150
add([rect(120, 40, radius=20), pos(x, y), anchor("center"),
color(20, 14, 24), {"busy": False}, "hole"])
score = 0
score_label = add([text("0", size=48), pos(width() / 2, 40),
anchor("center")])
def pop_up():
# A hole at random. If a goblin is already in it, there is no new one
# this time -- the wait() below still comes round again.
hole = choose(get("hole"))
if not hole.busy:
hole.busy = True
# The dictionary gives the goblin two values of its own: which
# hole it is in, and how long it has left before it ducks.
add([sprite("goblin"), pos(hole.pos.x, hole.pos.y + 8),
anchor("bot"), scale(5), area(), z(5),
{"hole": hole, "stay": STAY}, "monster"])
wait(WAIT, pop_up)
wait(0.5, pop_up)
# Every goblin runs this, every frame: its time runs down, and when there
# is none left it goes back down its hole.
@onUpdate("monster")
def stay_up(monster):
monster.stay = monster.stay - dt()
if monster.stay <= 0:
monster.hole.busy = False
monster.destroy()
@onClick
def click():
for monster in get("monster"):
if monster.isHovering():
hit(monster)
return
def hit(monster):
global score
score = score + 1
score_label.text = str(score)
monster.hole.busy = False
monster.destroy()
This is where the dictionaries start, in the shape kaypy uses for an
object's own values. A dictionary in the list given to add()
doesn't draw anything. Each of its keys becomes something the object
remembers, read with a dot.
- Each hole has
{"busy": False}, sohole.busyisFalseto start with.pop_upsets it toTruewhen a goblin comes up in it, and only puts a goblin in a hole that isn't busy. Two goblins never stand in the same hole. - Each goblin has
{"hole": hole, "stay": STAY}.monster.holeis the hole it came up in, so it can set that hole back to not busy when it goes.monster.stayis how long it has left. @onUpdate("monster")runsstay_uponce a frame for every goblin.dt()is how long the last frame took, in seconds, so taking it away every frame makesstayrun down like a clock. When it gets to zero the goblin's hole is free again, and the goblin is destroyed.pop_upends withwait(WAIT, pop_up). It books its own next turn, the waydrop_bombdid in Bomb Dodge. Thewait()is outside theif, so when the hole it picked is busy there is no goblin this time, but there is still a next time.clickgoes through every goblin and hits the first one the mouse is over. Thereturnstops there, so one click is one goblin.
The goblins are tagged "monster", not "goblin",
because soon they won't all be goblins.
Break it. Take
monster.hole.busy = False out of stay_up, then run
it and don't click anything. There's no error message. Nine goblins come up,
one in each hole, and then there are never any more. In five runs the last
goblin came up somewhere between 13 and 21 seconds in.
A goblin that gets away now leaves its hole busy for ever, and once all
nine are busy, pop_up never finds a free one. A goblin you
hit still frees its hole, because hit has that line
too, so if you click every one, the game seems fine. It is the ones you miss
that jam it.
Step 3 — what each one is worth
Five kinds of monster, each worth a different number of points. The angel is worth −5, so don't click it. The points come up in yellow where the monster was, or in red when you lose them.
from kaypy import *
STAY = 1.4 # how long a monster stays up, in seconds
WAIT = 0.7 # seconds between monsters
kaypy(width=800, height=600, background=[72, 62, 78])
loadSprite("goblin", "dungeon/goblin.png", sliceX=8)
loadSprite("skeleton", "dungeon/skelet.png", sliceX=8)
loadSprite("imp", "dungeon/imp.png", sliceX=8)
loadSprite("chort", "dungeon/chort.png", sliceX=8)
loadSprite("angel", "dungeon/angel.png", sliceX=8)
# What each monster is worth. The key is the monster's name -- the same
# name its picture was loaded under -- and the value is its points.
POINTS = {
"goblin": 1,
"skeleton": 2,
"imp": 3,
"chort": 5,
"angel": -5,
}
# Which monster comes up next is picked from this list. The more times a
# name is in it, the more often that monster comes up.
PICK_FROM = ["goblin", "goblin", "goblin", "goblin",
"skeleton", "skeleton", "skeleton",
"imp", "imp", "chort", "angel", "angel"]
for row in range(3):
for column in range(3):
x = 220 + column * 180
y = 230 + row * 150
add([rect(120, 40, radius=20), pos(x, y), anchor("center"),
color(20, 14, 24), {"busy": False}, "hole"])
score = 0
score_label = add([text("0", size=48), pos(width() / 2, 40),
anchor("center")])
def pop_up():
hole = choose(get("hole"))
if not hole.busy:
kind = choose(PICK_FROM)
hole.busy = True
# sprite(kind) shows the picture loaded under that name, and
# "kind" in the dictionary lets the monster remember what it is.
add([sprite(kind), pos(hole.pos.x, hole.pos.y + 8),
anchor("bot"), scale(5), area(), z(5),
{"kind": kind, "hole": hole, "stay": STAY}, "monster"])
wait(WAIT, pop_up)
wait(0.5, pop_up)
@onUpdate("monster")
def stay_up(monster):
monster.stay = monster.stay - dt()
if monster.stay <= 0:
monster.hole.busy = False
monster.destroy()
@onClick
def click():
for monster in get("monster"):
if monster.isHovering():
hit(monster)
return
def hit(monster):
global score
# Look the monster's points up by its name.
points = POINTS[monster.kind]
score = score + points
score_label.text = str(score)
# How many points it was, just where the monster was: yellow for
# points, red for losing them.
if points > 0:
words = "+" + str(points)
colour = YELLOW
else:
words = str(points)
colour = RED
add([text(words, size=32), pos(monster.pos.x, monster.pos.y - 80),
anchor("center"), color(colour), opacity(1),
lifespan(0.5, fade=0.3), z(10)])
monster.hole.busy = False
monster.destroy()
POINTS is a dictionary. Curly brackets, and
every value has a key in front of it with a colon between. The keys are the
monsters' names, and each value is what that monster is worth.
POINTS["imp"] is 3. POINTS["angel"] is −5.
Follow one monster through:
pop_uppicks a name withkind = choose(PICK_FROM). Say it picks"imp".sprite(kind)issprite("imp"), the picture loaded under that name. That is why each monster is loaded under the same name it has inPOINTS: the skeleton's picture is the fileskelet.png, but it is loaded as"skeleton".{"kind": kind, ...}puts the name on the monster, somonster.kindis"imp".- When it is clicked,
POINTS[monster.kind]isPOINTS["imp"], which is 3.
PICK_FROM is a list, not a dictionary, because the order and
the repeats are the point of it. "goblin" is in it four times
out of twelve, so a third of the monsters are goblins. The chort is in it
once, so one monster in twelve is a chort.
Break it. Add a monster to two places and forget the third. Load a new one,
loadSprite("wogol", "dungeon/wogol.png", sliceX=8)
and put "wogol" on the end of PICK_FROM, but
leave POINTS alone. Wogols come up, and nothing is wrong until
you click one. Then the game stops with
KeyError: 'wogol'
A KeyError means a dictionary was asked for a key it doesn't
have, and it tells you which key. POINTS has no
"wogol", so it can't say what a wogol is worth. Add
"wogol": 10, to POINTS and it works.
Step 4 — how long each one stays
Every monster stayed up for 1.4 seconds. Now each kind has its own time, and the ones worth the most are the quickest. A chort, worth 5, is gone in 0.7 seconds. Clicking the angel shakes the screen as well.
from kaypy import *
WAIT = 0.7 # seconds between monsters
kaypy(width=800, height=600, background=[72, 62, 78])
loadSprite("goblin", "dungeon/goblin.png", sliceX=8)
loadSprite("skeleton", "dungeon/skelet.png", sliceX=8)
loadSprite("imp", "dungeon/imp.png", sliceX=8)
loadSprite("chort", "dungeon/chort.png", sliceX=8)
loadSprite("angel", "dungeon/angel.png", sliceX=8)
# What each monster is worth. The key is the monster's name -- the same
# name its picture was loaded under -- and the value is its points.
POINTS = {
"goblin": 1,
"skeleton": 2,
"imp": 3,
"chort": 5,
"angel": -5,
}
# How long each one stays up before it ducks back down, in seconds. The
# same keys as POINTS, with a different fact about each monster.
STAY = {
"goblin": 1.4,
"skeleton": 1.2,
"imp": 0.9,
"chort": 0.7,
"angel": 1.4,
}
# Which monster comes up next is picked from this list. The more times a
# name is in it, the more often that monster comes up.
PICK_FROM = ["goblin", "goblin", "goblin", "goblin",
"skeleton", "skeleton", "skeleton",
"imp", "imp", "chort", "angel", "angel"]
for row in range(3):
for column in range(3):
x = 220 + column * 180
y = 230 + row * 150
add([rect(120, 40, radius=20), pos(x, y), anchor("center"),
color(20, 14, 24), {"busy": False}, "hole"])
score = 0
score_label = add([text("0", size=48), pos(width() / 2, 40),
anchor("center")])
def pop_up():
hole = choose(get("hole"))
if not hole.busy:
kind = choose(PICK_FROM)
hole.busy = True
# STAY[kind] looks up how long this kind stays up.
add([sprite(kind), pos(hole.pos.x, hole.pos.y + 8),
anchor("bot"), scale(5), area(), z(5),
{"kind": kind, "hole": hole, "stay": STAY[kind]},
"monster"])
wait(WAIT, pop_up)
wait(0.5, pop_up)
@onUpdate("monster")
def stay_up(monster):
monster.stay = monster.stay - dt()
if monster.stay <= 0:
monster.hole.busy = False
monster.destroy()
@onClick
def click():
for monster in get("monster"):
if monster.isHovering():
hit(monster)
return
def hit(monster):
global score
# Look the monster's points up by its name.
points = POINTS[monster.kind]
score = score + points
score_label.text = str(score)
# How many points it was, just where the monster was: yellow for
# points, red for losing them.
if points > 0:
words = "+" + str(points)
colour = YELLOW
else:
shake(10)
words = str(points)
colour = RED
add([text(words, size=32), pos(monster.pos.x, monster.pos.y - 80),
anchor("center"), color(colour), opacity(1),
lifespan(0.5, fade=0.3), z(10)])
monster.hole.busy = False
monster.destroy()
STAY is a second dictionary with the same keys as
POINTS. Same names, a different fact about each one.
The number STAY was in step 3 has become
STAY[kind] where the monster is made, and that is the only line
that changed to use it. Everything else that reads
monster.stay carries on as before.
You could put both facts in one dictionary, with a dictionary for each
monster inside it. That works, and bigger games do it. Two flat dictionaries
are easier to read, and easier to change: to make imps quicker, you go
to STAY and change one number.
Break it. Put quotes round
kind where the monster is made, so it says
STAY["kind"]. Half a second in, as the first monster comes up,
the game stops with
KeyError: 'kind'
With quotes, "kind" is the word kind, four letters,
and there is no monster called that. Without them, kind is the
variable, which holds a name like "imp". It is one of the easiest
mistakes to make with a dictionary, and the message shows you the exact key
it looked for.
Step 5 — the clock, and the tally
A game now lasts thirty seconds. The time counts down in the corner. When it gets to zero, the monsters and the holes go, and you see each kind you hit: its picture, how many, and how many points they came to. Click or press space to play again.
Playing again means building everything fresh, which is what a scene is
for. Everything from the holes down moves inside
@scene("game"), the way it did in Bomb Dodge.
from kaypy import *
WAIT = 0.7 # seconds between monsters
ROUND = 30 # how long a game lasts, in seconds
kaypy(width=800, height=600, background=[72, 62, 78])
loadSprite("goblin", "dungeon/goblin.png", sliceX=8)
loadSprite("skeleton", "dungeon/skelet.png", sliceX=8)
loadSprite("imp", "dungeon/imp.png", sliceX=8)
loadSprite("chort", "dungeon/chort.png", sliceX=8)
loadSprite("angel", "dungeon/angel.png", sliceX=8)
POINTS = {
"goblin": 1,
"skeleton": 2,
"imp": 3,
"chort": 5,
"angel": -5,
}
STAY = {
"goblin": 1.4,
"skeleton": 1.2,
"imp": 0.9,
"chort": 0.7,
"angel": 1.4,
}
PICK_FROM = ["goblin", "goblin", "goblin", "goblin",
"skeleton", "skeleton", "skeleton",
"imp", "imp", "chort", "angel", "angel"]
score = 0
time_left = ROUND
playing = True
# How many of each monster you hit. It starts empty, and a monster's
# name goes in the first time you hit one.
hits = {}
@scene("game")
def game():
global score, time_left, playing, hits
# A new game, so everything starts again.
score = 0
time_left = ROUND
playing = True
hits = {}
for row in range(3):
for column in range(3):
x = 220 + column * 180
y = 230 + row * 150
add([rect(120, 40, radius=20), pos(x, y), anchor("center"),
color(20, 14, 24), {"busy": False}, "hole"])
score_label = add([text("0", size=48), pos(width() / 2, 40),
anchor("center"), z(10)])
time_label = add([text(str(ROUND), size=28), pos(width() - 12, 12),
anchor("topright"), z(10)])
message = add([text("", size=26), pos(width() / 2, 100),
anchor("center"), z(10)])
def pop_up():
# Time is up: no monster, and no wait() for the next one either.
if not playing:
return
hole = choose(get("hole"))
if not hole.busy:
kind = choose(PICK_FROM)
hole.busy = True
add([sprite(kind), pos(hole.pos.x, hole.pos.y + 8),
anchor("bot"), scale(5), area(), z(5),
{"kind": kind, "hole": hole, "stay": STAY[kind]},
"monster"])
wait(WAIT, pop_up)
wait(0.5, pop_up)
@onUpdate("monster")
def stay_up(monster):
monster.stay = monster.stay - dt()
if monster.stay <= 0:
monster.hole.busy = False
monster.destroy()
@onClick
def click():
if not playing:
return
for monster in get("monster"):
if monster.isHovering():
hit(monster)
return
def hit(monster):
global score
kind = monster.kind
points = POINTS[kind]
score = score + points
score_label.text = str(score)
# One more of this kind. hits.get(kind, 0) is how many there
# were, or 0 if this is the first.
hits[kind] = hits.get(kind, 0) + 1
if points > 0:
words = "+" + str(points)
colour = YELLOW
else:
shake(10)
words = str(points)
colour = RED
add([text(words, size=32), pos(monster.pos.x, monster.pos.y - 80),
anchor("center"), color(colour), opacity(1),
lifespan(0.5, fade=0.3), z(10)])
monster.hole.busy = False
monster.destroy()
# Once a second, one second less.
def tick():
global time_left
if not playing:
return
time_left = time_left - 1
time_label.text = str(time_left)
if time_left == 0:
time_up()
loop(1, tick)
def time_up():
global playing
playing = False
destroyAll("monster")
destroyAll("hole")
message.text = "Time! " + str(score) + " points -- click to play again"
# One line for each kind you hit: its picture, how many, and
# what they came to. .items() hands over each key with its value.
y = 180
for kind, count in hits.items():
add([sprite(kind), pos(300, y), anchor("center"), scale(3)])
add([text("x " + str(count) + " = " + str(count * POINTS[kind]),
size=28), pos(350, y), anchor("left")])
y = y + 60
def again():
if not playing:
go("game")
onKeyPress("space", again)
onClick(again)
go("game")
hits is a dictionary that starts empty,
{}, and fills up as you play. Each time you hit a monster,
hits[kind] = hits.get(kind, 0) + 1
adds one to that kind's count. hits.get(kind, 0) means
how many of this kind there are so far, or 0 if there are none yet.
The first goblin you hit, "goblin" isn't in
hits yet, so .get gives 0, and
hits["goblin"] is set to 1. Assigning to a key that isn't
there adds it. The second goblin, .get gives 1, and it becomes
2.
hit changes hits without saying
global. It changes what is inside the
dictionary, which needs nothing special. The scene does say
global hits, because hits = {} doesn't
change the dictionary: it replaces it with a new, empty one.
The tally is a for loop over
hits.items(), which hands over each key with its
value: kind is the monster's name and count is how
many. The name shows the picture, sprite(kind), and
count * POINTS[kind] is the points they came to. Each line goes
60 pixels below the last. A dictionary remembers the order its keys went in,
so the tally starts with the kind you hit first. A kind you never hit
isn't in hits at all, so it has no line.
The clock is loop(1, tick), which runs
tick once a second. When time_left reaches 0,
tick calls time_up, which sets
playing to False. That stops everything else:
pop_up makes no more monsters, click hits
nothing, and tick stops counting.
Two things to break.
Use square brackets instead of .get:
hits[kind] = hits[kind] + 1. The first monster you hit
stops the game with
KeyError: 'goblin'
or whichever kind it was. To add one, Python first has to read
hits["goblin"], and the first time, there isn't one. That is
what the 0 in .get(kind, 0) is for.
Take hits = {} out of the scene. There's no error message,
and the first game looks right. Play a second one, and the tally at the end
has both games in it. In one run the second game scored 84, and the tally
under it said 24 goblins, 16 skeletons, 22 imps and 9 chorts, which comes to
167. The score starts again, because score = 0 is still there,
but the dictionary carries on from the last game.
Step 6 — the whole game
This is the game exactly as it is on the games page. It adds four things to step 5:
- Faster as you go. The time between monsters was always
0.7 seconds. Now
pop_wait()works it out from how long the game has gone on: 0.7 seconds at the start, 0.015 less each second, and never less than 0.3, which it reaches about 27 seconds in. By then there are three or four monsters up at once. - A best score that is remembered.
getData(BEST_KEY, 0)reads it, andsetDatasaves a new one when you beat it, as in Bomb Dodge. - Sounds, a thump for a hit and a bang for the angel, and a message at the start saying what to do.
- A second before you can play again. In step 5, a click
a moment too late, meant for the last monster, starts a new game straight
away, and the tally is gone before you have seen it. Now
time_upends withwait(1, ready_again), and only after that second doesoverbecomeTrue, and a click start a new game.
It also has all the numbers worth changing in one settings block near the top, and comments that explain why each part is there.
"""Goblin Hit -- monsters pop out of holes. Hit them before they duck.
click a monster (or tap it) to hit it
Nine holes, thirty seconds. Every kind of monster is worth something
different, and the quick ones are worth the most. Don't hit the angel.
When the time is up you see what you hit, and your best score is
remembered.
HOW THIS FILE IS PUT TOGETHER
1. the window, the pictures and the sounds
2. the monsters: what each one is worth, and how long it stays up
3. the settings
4. how hard the game is
5. the game scene: the holes, popping up, hitting, time up
6. start the game
This is the game for after dictionaries. Every monster has a name, and the
name is the key that looks up everything else about it: its picture, its
points, how long it stays up, and how many of it you hit. Add a monster
of your own and you will see -- it is one new line in each dictionary.
"""
from kaypy import *
# ----------------------------------------------------------------- 1. setup
kaypy(width=800, height=600, background=[72, 62, 78])
# Each monster's picture is eight little pictures in a row. sliceX=8 cuts it
# into eight, and the game only ever shows the first one.
loadSprite("goblin", "dungeon/goblin.png", sliceX=8)
loadSprite("skeleton", "dungeon/skelet.png", sliceX=8)
loadSprite("imp", "dungeon/imp.png", sliceX=8)
loadSprite("chort", "dungeon/chort.png", sliceX=8)
loadSprite("angel", "dungeon/angel.png", sliceX=8)
loadSound("hit", "sounds/thump.wav")
loadSound("ouch", "sounds/small_boom.wav")
# ------------------------------------------------------------ 2. the monsters
#
# Two dictionaries, with the same keys. A key is a monster's name -- the
# same name its picture was loaded under above -- and the value is one fact
# about it. POINTS["imp"] is how many points an imp is worth; STAY["imp"] is
# how long an imp stays up.
POINTS = {
"goblin": 1,
"skeleton": 2,
"imp": 3,
"chort": 5,
"angel": -5,
}
STAY = {
"goblin": 1.4,
"skeleton": 1.2,
"imp": 0.9,
"chort": 0.7,
"angel": 1.4,
}
# Which monster comes up next is picked from this list. The more times a
# name is in it, the more often that monster comes up.
PICK_FROM = ["goblin", "goblin", "goblin", "goblin",
"skeleton", "skeleton", "skeleton",
"imp", "imp", "chort", "angel", "angel"]
# -------------------------------------------------------------- 3. settings
#
# Everything else you would want to change is here. Change one at a time
# and play it.
ROUND = 30 # how long a game lasts, in seconds
MONSTER_SIZE = 5 # how big a monster is: the pictures are 16 pixels
WAIT_START = 0.7 # seconds between monsters, to begin with
WAIT_SHORTEST = 0.3 # ...the shortest wait there ever is
WAIT_DROP = 0.015 # ...and how much shorter each second 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 = "goblin_hit_best"
# -------------------------------------------------- 4. how hard the game is
#
# The longer the game has gone on, the less time between monsters. It
# stops somewhere, so the game ends up hard but never impossible.
time_left = ROUND
def pop_wait():
seconds = WAIT_START - (ROUND - time_left) * WAIT_DROP
if seconds < WAIT_SHORTEST:
seconds = WAIT_SHORTEST
return seconds
# ------------------------------------------------------- 5. the game scene
#
# These change while you play. A function that CHANGES one has to say
# `global` first, which means "the one up here, not a new one of my own".
score = 0
playing = True
over = False
# How many of each monster you hit. It starts empty, and a monster's
# name goes in the first time you hit one.
hits = {}
@scene("game")
def game():
global score, time_left, playing, over, hits
# A new game, so everything starts again.
score = 0
time_left = ROUND
playing = True
over = False
hits = {}
best = getData(BEST_KEY, 0)
# The nine holes, in three rows of three. A hole is a dark rectangle
# with round ends: radius=20 rounds its corners.
#
# The dictionary at the end, {"busy": False}, gives each hole a value
# of its own: hole.busy is True while a monster is in it, so two never
# come up in the same hole.
for row in range(3):
for column in range(3):
x = 220 + column * 180
y = 230 + row * 150
add([rect(120, 40, radius=20), pos(x, y), anchor("center"),
color(20, 14, 24), {"busy": False}, "hole"])
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)])
time_label = add([text(str(ROUND), size=28), pos(width() - 12, 12),
anchor("topright"), z(10)])
message = add([text("Click the monsters! Not the angel.", size=26),
pos(width() / 2, 100), anchor("center"), z(10)])
# --------------------------------------------------------- popping up
def pop_up():
# Time is up: no monster, and no wait() for the next one either.
if not playing:
return
# A hole at random. If a monster is already in it, there is no new
# one this time -- the wait() below still comes round again.
hole = choose(get("hole"))
if not hole.busy:
kind = choose(PICK_FROM)
hole.busy = True
# anchor("bot") puts the monster's feet in the hole. The
# dictionary gives it three values of its own: what kind it is,
# which hole it is in, and how long it has left before it ducks.
add([sprite(kind), pos(hole.pos.x, hole.pos.y + 8),
anchor("bot"), scale(MONSTER_SIZE), area(), z(5),
{"kind": kind, "hole": hole, "stay": STAY[kind]},
"monster"])
wait(pop_wait(), pop_up)
# The first monster, half a second after the game starts.
wait(0.5, pop_up)
# Every monster runs this, every frame: its time runs down, and when
# there is none left it goes back down its hole.
@onUpdate("monster")
def stay_up(monster):
monster.stay = monster.stay - dt()
if monster.stay <= 0:
monster.hole.busy = False
monster.destroy()
# ------------------------------------------------------------ hitting
#
# onClick runs on every click, wherever it is, and a tap on a phone
# counts as a click. isHovering() asks whether the mouse is over a
# monster, which needs area() on it.
@onClick
def click():
if not playing:
return
for monster in get("monster"):
if monster.isHovering():
hit(monster)
return
def hit(monster):
global score
kind = monster.kind
points = POINTS[kind]
score = score + points
score_label.text = str(score)
message.text = ""
# One more of this kind. hits.get(kind, 0) is how many there
# were, or 0 if this is the first.
hits[kind] = hits.get(kind, 0) + 1
# How many points it was, just where the monster was.
if points > 0:
play("hit")
words = "+" + str(points)
colour = YELLOW
else:
play("ouch")
shake(10)
words = str(points)
colour = RED
add([text(words, size=32), pos(monster.pos.x, monster.pos.y - 80),
anchor("center"), color(colour), opacity(1),
lifespan(0.5, fade=0.3), z(10)])
monster.hole.busy = False
monster.destroy()
# ------------------------------------------------------------- the clock
def tick():
global time_left
if not playing:
return
time_left = time_left - 1
time_label.text = str(time_left)
if time_left == 0:
time_up()
loop(1, tick)
# -------------------------------------------------------------- time up
def time_up():
global playing
playing = False
destroyAll("monster")
destroyAll("hole")
if score > best:
setData(BEST_KEY, score)
best_label.text = "best " + str(score)
message.text = "Time! " + str(score) + " points"
# One line for each kind you hit: its picture, how many, and
# what they came to. .items() hands over each key with its value.
y = 180
for kind, count in hits.items():
add([sprite(kind), pos(300, y), anchor("center"), scale(3)])
add([text("x " + str(count) + " = " + str(count * POINTS[kind]),
size=28), pos(350, y), anchor("left")])
y = y + 60
# A second before you can start again, so a click meant for the
# last monster does not skip past your score.
wait(1, ready_again)
def ready_again():
global over
over = True
add([text("press SPACE or click to play again", size=24),
pos(width() / 2, height() - 40), anchor("center")])
def again():
if over:
go("game")
onKeyPress("space", again)
onClick(again)
# ------------------------------------------------------- 6. 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.
- Add a monster of your own. Four lines, and no new code:
load its picture, give it points in
POINTS, a time inSTAY, and put its name inPICK_FROM. For a wogol worth 10 that is gone in half a second:
andloadSprite("wogol", "dungeon/wogol.png", sliceX=8) "wogol": 10, # in POINTS "wogol": 0.5, # in STAY"wogol"on the end ofPICK_FROM. It shows up in the tally by itself. Click Sprites in the playground to see the other monsters. - Change what they are worth. Make the angel −20 and you will be a lot more careful. Make goblins worth 0 and they are only in the way.
- A longer game.
ROUND = 60. Thenpop_wait()stops getting shorter at about 27 seconds, so the second half is all at full speed. - A sound for each kind. Make a third dictionary,
SOUNDS = {"goblin": "hit", "angel": "ouch", ...}, load the sounds, andplay(SOUNDS[kind])inhit. Leave a monster out of it and you know what you'll see.