singe/docs/lessons/07-a-game.adoc
2026-09-22 21:57:42 -05:00

840 lines
27 KiB
Text

== Lesson 7: A Game
image::learn/07-a-game.png[The finished lesson, 480]
Six lessons ago you put one word on a black screen. Since then you have
learned every idea a small game is made of: variables, decisions, loops,
functions of your own, and lists of things. This lesson does not teach a new
idea. It spends the ones you have.
You are going to build a game called Dodge. Blocks fall out of the sky, you
slide a bar along the bottom to get out of their way, and every block that
misses you is a point. Hit three of them and it is over. You will build it in
six stages, and every one of them runs: you never have to type a hundred
lines and hope.
=== Make the Folder
Make a folder called `dodge`, and in it a file called `dodge.singe`. Run it
the way you have been running everything since lesson one:
----
Singe -R dodge
----
Leave that running in a terminal for the whole lesson. Every time you save,
the game restarts with your change in it.
=== Stage One: Something You Can Steer
Type this in, save, and steer it with the left and right arrow keys.
[source,lua]
----
dofile("Singe/Framework.singe")
local PLAYER_WIDTH = 40
local PLAYER_HEIGHT = 8
local PLAYER_SPEED = 4
local PLAYER_MARGIN = 4
local screenWidth = overlayGetWidth()
local screenHeight = overlayGetHeight()
local playerY = screenHeight - PLAYER_HEIGHT - PLAYER_MARGIN
local playerX = (screenWidth - PLAYER_WIDTH) / 2
local goingLeft = false
local goingRight = false
function onInputPressed(what)
if what == SWITCH_LEFT then
goingLeft = true
elseif what == SWITCH_RIGHT then
goingRight = true
end
end
function onInputReleased(what)
if what == SWITCH_LEFT then
goingLeft = false
elseif what == SWITCH_RIGHT then
goingRight = false
end
end
function onOverlayUpdate()
if goingLeft then
playerX = playerX - PLAYER_SPEED
end
if goingRight then
playerX = playerX + PLAYER_SPEED
end
if playerX < 0 then
playerX = 0
elseif playerX > screenWidth - PLAYER_WIDTH then
playerX = screenWidth - PLAYER_WIDTH
end
overlayClear()
colorForeground(80, 255, 160)
overlayBox(playerX, playerY, playerX + PLAYER_WIDTH - 1, playerY + PLAYER_HEIGHT - 1)
return OVERLAY_UPDATED
end
----
Fifty lines, and almost none of it is new. Two things in it are, so take the
first line first.
[source,lua]
----
dofile("Singe/Framework.singe")
----
`dofile` belongs to Lua rather than to Singe, and it means "go and run that
file, then carry on here". The file it runs is `Singe/Framework.singe`, one of
the files the engine unpacked into your work folder the first time you ran
anything, back in lesson one. Open it in your text editor and look: it is
ordinary Lua, no different in kind from what you have been writing since
lesson two, and there is nothing magic in it.
Running it hands your script a set of names it can then use. The ones this
book reaches for are these:
* `DIR`, which holds the folder your own script is sitting in. Lesson nine
needs it the moment a game loads a picture, and it is the real reason this
line is here.
* `SCANCODE` and `MODIFIER`, names for every key on a keyboard, for a game
that reads typing rather than a joystick.
* `GAMEPAD_0` to `GAMEPAD_3` and `MOUSE_0` to `MOUSE_3`, for a cabinet with
more than one of something plugged into it.
* A few helpers whose names begin with `util`, and the six calls that post a
score to a leaderboard, which is lesson twenty-eight.
* The second way of writing a game altogether, which is lesson fifteen.
One thing it does *not* give you, because it is the natural thing to assume.
`SWITCH_LEFT`, and every other name beginning with `SWITCH_`, comes from the
engine itself, before any script runs at all. That is why lessons three to six
could read the arrow keys without loading anything. The framework adds to what
is already there; it does not provide it.
This particular game uses nothing from the framework. The line is here because
every game from lesson nine to the end of the book does need it, and because
it is how nearly every real Singe game starts. It is read once, at startup,
and then forgotten about.
The second new thing is the way the arrow keys are handled, because it is the
first thing beginners get wrong.
`onInputPressed` does not move the player. It sets `goingLeft` to `true`, and
`onInputReleased` sets it back to `false`. The moving happens in
`onOverlayUpdate`, which asks "is the player holding left?" sixty times a
second. If you moved the player inside `onInputPressed` instead, you would get
one step per press: Singe sends a press exactly once when the key goes down
and never repeats it, which the manual's entry for `onInputPressed` says
plainly. Holding the key would do nothing at all.
So the press handler records what is true about the world, and the update
decides what to do about it. Keep that split and your input code will stay
five lines long forever.
The three names in capitals are ordinary variables. Writing `PLAYER_SPEED` in
capitals is a message to yourself and to anyone reading: this one is set once
at the top and never changes. Nothing in Lua enforces it. It matters because
in a minute there will be ten of them, and when the game is too hard you want
one obvious place to go and turn a number down.
`screenWidth` and `screenHeight` come from the engine rather than being typed
in, so the game lays itself out to whatever size the overlay happens to be.
`playerY` is worked out once from the height: the bar sits `PLAYER_MARGIN`
pixels off the bottom and never moves up or down, so there is no reason to
compute it again every frame.
The `- 1` in the `overlayBox` line is not a typo. `overlayBox` draws through
both corners you give it, so a box from `playerX` to `playerX + 40` is
forty-one pixels wide. Subtracting one makes the bar you see exactly forty
wide, which will matter in stage four when something has to decide whether it
touched you.
=== Stage Two: Something to Dodge
One block, falling. Add two constants to the list at the top:
[source,lua]
----
local BLOCK_SIZE = 12
local BLOCK_SPEED = 1.5
----
Two more variables under `goingRight`:
[source,lua]
----
local blockX = math.random(0, screenWidth - BLOCK_SIZE)
local blockY = -BLOCK_SIZE
----
In `onOverlayUpdate`, just above the `overlayClear()` line, make it fall:
[source,lua]
----
blockY = blockY + BLOCK_SPEED
if blockY > screenHeight then
blockX = math.random(0, screenWidth - BLOCK_SIZE)
blockY = -BLOCK_SIZE
end
----
And under the line that draws the player, draw it:
[source,lua]
----
colorForeground(255, 90, 90)
overlayBox(blockX, blockY, blockX + BLOCK_SIZE - 1, blockY + BLOCK_SIZE - 1)
----
Save. A red square falls, reaches the bottom, and reappears at the top in a
new place.
It starts at `-BLOCK_SIZE`, which is above the top edge of the screen. There
is nothing wrong with a negative position: the engine draws the part of the
box that is on the overlay and quietly throws away the part that is not. The
block slides into view instead of popping into existence.
`BLOCK_SPEED` is `1.5`, not a whole number, so `blockY` spends most of its
life being something like `83.5`. That is fine too. Drawing chops the fraction
off, and keeping it in the variable is what lets the block move slower than
one pixel a frame.
=== Stage Three: A Sky Full of Them
One block is not a game. In lesson six you learned to keep many of something
in a list, and this is what it was for.
Delete `blockX` and `blockY`, and put these in their place:
[source,lua]
----
local blocks = {}
local spawnTimer = 0
----
Add one more constant at the top:
[source,lua]
----
local SPAWN_FRAMES = 40
----
Now a function that makes one block and puts it in the list. Put it above
`onInputPressed`:
[source,lua]
----
function spawnBlock()
local block = {}
block.x = math.random(0, screenWidth - BLOCK_SIZE)
block.y = -BLOCK_SIZE
table.insert(blocks, block)
end
----
Replace the falling code from stage two with this:
[source,lua]
----
spawnTimer = spawnTimer - 1
if spawnTimer <= 0 then
spawnBlock()
spawnTimer = SPAWN_FRAMES
end
for i = #blocks, 1, -1 do
local block = blocks[i]
block.y = block.y + BLOCK_SPEED
if block.y > screenHeight then
table.remove(blocks, i)
end
end
----
And replace the one line that drew the block with a loop over all of them:
[source,lua]
----
for i, block in ipairs(blocks) do
overlayBox(block.x, block.y, block.x + BLOCK_SIZE - 1, block.y + BLOCK_SIZE - 1)
end
----
Save. Blocks rain down.
A block is a record: one little table with an `x` and a `y` in it, which is
how you keep two numbers that belong together from drifting apart. `blocks` is
a list of those records, which is the shape almost every game's world has.
`spawnTimer` counts down one per frame, and when it reaches zero a block is
born and the timer is wound back up. Forty frames is about two thirds of a
second. There is no clock in this game and there does not need to be one:
`onOverlayUpdate` is the heartbeat, and counting heartbeats is a perfectly
good way to measure time.
The falling loop runs *backwards* -- from `#blocks` down to `1`, a step at a
time -- because it removes things as it goes. That is the rule from lesson
six, and this is the first time you have had a real reason to obey it. Remove
item three while walking forwards and everything shuffles down one, so the
block that was item four is now item three and the loop never looks at it.
Walking backwards, everything that moves has already been dealt with.
The drawing loop uses `ipairs` instead, because it removes nothing and only
wants each block in turn.
=== Stage Four: Getting Hit
The blocks fall straight through you. Time to notice.
The engine has a function for exactly this question. Change the test inside
the falling loop from this:
[source,lua]
----
if block.y > screenHeight then
table.remove(blocks, i)
end
----
to this:
[source,lua]
----
if collideRects(block.x, block.y, BLOCK_SIZE, BLOCK_SIZE, playerX, playerY, PLAYER_WIDTH, PLAYER_HEIGHT) or block.y > screenHeight then
table.remove(blocks, i)
end
----
Save, and drive the bar into a falling block. It vanishes.
`collideRects` takes two rectangles and answers `true` if they overlap. Each
rectangle is four numbers: a corner, then a width and a height -- *not* two
corners, which is the other reasonable way to describe a rectangle and the way
`overlayBox` does it. The manual's entry for `collideRects` gives the order,
and it is worth looking at now, because getting the eight arguments in the
wrong order is the kind of mistake that produces no error at all, just a game
where nothing ever hits anything.
You could write this yourself. It is four comparisons, and it is genuinely not
hard. Use the engine's anyway, for three reasons. It is right, including the
awkward cases: a rectangle with no width, two rectangles that only touch along
an edge. It says what it means, so the line reads as "did these two overlap"
instead of four `and`s you have to decode every time you come back to it. And
when you reach lesson eleven, where things hit each other constantly, you will
already know the family: `collidePointRect` for a mouse click, `collideCircles`
for two round things, and `collideSegments` for a shot against a wall. The manual
lists them together.
The one thing the engine cannot do for you is decide what a rectangle *means*.
The block's rectangle here is the whole block. In a real game you often want
the hit rectangle to be a little smaller than the picture, so a near miss
feels like a near miss. That is a design decision, and lesson eleven is where
you make it.
=== Stage Five: Score and Lives
Getting hit should cost something, and surviving should be worth something.
Three more constants at the top:
[source,lua]
----
local BLOCK_FASTER = 0.05
local START_LIVES = 3
----
Three more variables, next to `spawnTimer`:
[source,lua]
----
local blockSpeed = BLOCK_SPEED
local score = 0
local lives = START_LIVES
----
`BLOCK_SPEED` is now the speed the game *starts* at, and `blockSpeed` is the
speed it is going at right now. Change the falling line to use the variable:
[source,lua]
----
block.y = block.y + blockSpeed
----
Then split the two cases apart, because they now do different things:
[source,lua]
----
if collideRects(block.x, block.y, BLOCK_SIZE, BLOCK_SIZE, playerX, playerY, PLAYER_WIDTH, PLAYER_HEIGHT) then
table.remove(blocks, i)
lives = lives - 1
elseif block.y > screenHeight then
table.remove(blocks, i)
score = score + 1
blockSpeed = blockSpeed + BLOCK_FASTER
end
----
And show the numbers. Put this just before the `return`:
[source,lua]
----
overlayPrint(1, 1, "SCORE " .. score .. " LIVES " .. lives)
----
Save, and play it for a minute. It gets harder, which is the whole trick:
every block you dodge makes the next ones fall a twentieth of a pixel per
frame faster. Twenty points in and they are noticeably quicker. You did not
have to write a single word about difficulty levels.
You will also watch `LIVES` go to `-4`, because nothing stops the game yet.
That is the next stage, and it is the one that matters most.
=== Stage Six: Game Over, and Going Again
Here is the trap. The obvious way to add a game over screen is a variable
called `gameOver`, set to `true` when the lives run out. Then you want a title
screen before the first game, so you add `started`. Then you want a pause key,
so you add `paused`. Now you have three true-or-false variables, eight
combinations between them, and five of those combinations are nonsense that
your code has to be careful never to produce. Every beginner writes this game
at least once, and it is where small games go to die.
Do this instead. One variable holds *what the game is doing*, as a word:
[source,lua]
----
local state = "waiting"
----
It is `"waiting"` before the first game, `"playing"` during one, and `"over"`
when the lives are gone. Three states, one variable, and no such thing as an
impossible combination. There is nothing magic about the strings; they are
words you chose, and you must spell them the same way every time.
Now the function that begins a game. Every variable a game owns goes back to
its starting value here, in one place:
[source,lua]
----
function startGame()
playerX = (screenWidth - PLAYER_WIDTH) / 2
blocks = {}
blockSpeed = BLOCK_SPEED
spawnTimer = 0
score = 0
lives = START_LIVES
state = "playing"
end
----
Losing the last life ends it. Inside the collision branch, under
`lives = lives - 1`:
[source,lua]
----
if lives == 0 then
state = "over"
end
----
The space bar starts a game, but only when one is not already running. Add an
arm to `onInputPressed`:
[source,lua]
----
elseif what == SWITCH_BUTTON1 and state ~= "playing" then
startGame()
----
`~=` is "is not equal to". `state ~= "playing"` is true while waiting and true
while over, which are exactly the two moments when space should start a game.
`SWITCH_BUTTON1` is the space bar by default, and also the A button on a
controller.
Two small functions put the words on screen. `printCentered` does the
arithmetic once so that the three screens do not each do it badly:
[source,lua]
----
function drawOverText()
printCentered(7, "GAME OVER")
printCentered(9, "PRESS SPACE TO PLAY AGAIN")
end
function drawWaitingText()
printCentered(6, "DODGE THE BLOCKS")
printCentered(8, "ARROW KEYS TO MOVE")
printCentered(10, "PRESS SPACE TO START")
end
function printCentered(row, text)
overlayPrint(math.floor((TEXT_COLUMNS - #text) / 2), row, text)
end
----
with one more constant at the top:
[source,lua]
----
local TEXT_COLUMNS = overlayGetWidth() // overlayGetFontWidth()
----
Finally, the important part. Move everything that draws out of
`onOverlayUpdate` into a function called `drawGame`, move everything that
moves into a function called `updatePlaying` -- both of them are in the full
listing below -- and leave `onOverlayUpdate` looking like this:
[source,lua]
----
function onOverlayUpdate()
if state == "playing" then
updatePlaying()
end
overlayClear()
drawGame()
if state == "waiting" then
drawWaitingText()
elseif state == "over" then
drawOverText()
end
return OVERLAY_UPDATED
end
----
Save. You have a game: a title screen, a game, a game over, and a way back
round.
Read those thirteen lines again, because they are the shape of every game you
will ever write. The world only moves while the state is `"playing"`, so the
game over screen freezes with the last blocks hanging exactly where they were.
The world is drawn every frame whatever the state, because the picture is the
same in all three; the state decides only what words go on top of it. And
there is precisely one place that makes that decision.
That is what you bought with the `state` variable. When you add a pause, you
add one arm to that `if` and one line to `onInputPressed`, and nothing else in
the game needs to know a thing about it.
=== The Whole Thing
Here it is, finished. If yours does not match, the difference is worth
finding.
[source,lua]
----
dofile("Singe/Framework.singe")
local PLAYER_WIDTH = 40
local PLAYER_HEIGHT = 8
local PLAYER_SPEED = 4
local PLAYER_MARGIN = 4
local BLOCK_SIZE = 12
local BLOCK_SPEED = 1.5
local BLOCK_FASTER = 0.05
local SPAWN_FRAMES = 40
local START_LIVES = 3
local TEXT_COLUMNS = overlayGetWidth() // overlayGetFontWidth()
local screenWidth = overlayGetWidth()
local screenHeight = overlayGetHeight()
local playerY = screenHeight - PLAYER_HEIGHT - PLAYER_MARGIN
local playerX = (screenWidth - PLAYER_WIDTH) / 2
local goingLeft = false
local goingRight = false
local blocks = {}
local blockSpeed = BLOCK_SPEED
local spawnTimer = 0
local score = 0
local lives = START_LIVES
local state = "waiting"
function drawGame()
colorForeground(80, 255, 160)
overlayBox(playerX, playerY, playerX + PLAYER_WIDTH - 1, playerY + PLAYER_HEIGHT - 1)
colorForeground(255, 90, 90)
for i, block in ipairs(blocks) do
overlayBox(block.x, block.y, block.x + BLOCK_SIZE - 1, block.y + BLOCK_SIZE - 1)
end
overlayPrint(1, 1, "SCORE " .. score .. " LIVES " .. lives)
end
function drawOverText()
printCentered(7, "GAME OVER")
printCentered(9, "PRESS SPACE TO PLAY AGAIN")
end
function drawWaitingText()
printCentered(6, "DODGE THE BLOCKS")
printCentered(8, "ARROW KEYS TO MOVE")
printCentered(10, "PRESS SPACE TO START")
end
function printCentered(row, text)
overlayPrint(math.floor((TEXT_COLUMNS - #text) / 2), row, text)
end
function spawnBlock()
local block = {}
block.x = math.random(0, screenWidth - BLOCK_SIZE)
block.y = -BLOCK_SIZE
table.insert(blocks, block)
end
function startGame()
playerX = (screenWidth - PLAYER_WIDTH) / 2
blocks = {}
blockSpeed = BLOCK_SPEED
spawnTimer = 0
score = 0
lives = START_LIVES
state = "playing"
end
function updatePlaying()
if goingLeft then
playerX = playerX - PLAYER_SPEED
end
if goingRight then
playerX = playerX + PLAYER_SPEED
end
if playerX < 0 then
playerX = 0
elseif playerX > screenWidth - PLAYER_WIDTH then
playerX = screenWidth - PLAYER_WIDTH
end
spawnTimer = spawnTimer - 1
if spawnTimer <= 0 then
spawnBlock()
spawnTimer = SPAWN_FRAMES
end
for i = #blocks, 1, -1 do
local block = blocks[i]
block.y = block.y + blockSpeed
if collideRects(block.x, block.y, BLOCK_SIZE, BLOCK_SIZE, playerX, playerY, PLAYER_WIDTH, PLAYER_HEIGHT) then
table.remove(blocks, i)
lives = lives - 1
if lives == 0 then
state = "over"
end
elseif block.y > screenHeight then
table.remove(blocks, i)
score = score + 1
blockSpeed = blockSpeed + BLOCK_FASTER
end
end
end
function onInputPressed(what)
if what == SWITCH_LEFT then
goingLeft = true
elseif what == SWITCH_RIGHT then
goingRight = true
elseif what == SWITCH_BUTTON1 and state ~= "playing" then
startGame()
end
end
function onInputReleased(what)
if what == SWITCH_LEFT then
goingLeft = false
elseif what == SWITCH_RIGHT then
goingRight = false
end
end
function onOverlayUpdate()
if state == "playing" then
updatePlaying()
end
overlayClear()
drawGame()
if state == "waiting" then
drawWaitingText()
elseif state == "over" then
drawOverText()
end
return OVERLAY_UPDATED
end
----
About a hundred and twenty lines of code, and you wrote all of them a handful
at a time.
=== What Just Happened
A few pieces deserve a second look.
[source,lua]
----
local blocks = {}
local score = 0
function startGame()
score = 0
end
----
The data is `local` and the functions are not. Those are two deliberate and
opposite choices.
The data is `local` so that it belongs to this file and nothing outside can
reach in and change it. The functions are global so that they can call each
other in any order: `drawGame` is defined before `printCentered` and calls it
anyway, because a global name is looked up at the moment of the call, by which
time the whole file has been read. Make those functions `local` and the file
has to be sorted so that nothing is used before it appears, which is a rule
you would spend the rest of the book tripping over. Three of the names here
have no choice at all: `onOverlayUpdate` and the two input callbacks must be
global, because that is how Singe finds them.
Inside the file, the order is alphabetical, with the three callbacks last.
That is not a rule of Lua. It is so that in six months you can find
`spawnBlock` without reading anything.
[source,lua]
----
overlayPrint(math.floor((TEXT_COLUMNS - #text) / 2), row, text)
----
`+#text+` is the length of a string, in characters. You met `+#+` in lesson six
counting the items in a list, and it does the same job here. `"GAME OVER"` is
nine characters, so on a sixty column screen it starts at column twenty-five,
which leaves twenty-five cells of space on its left and twenty-six on its
right.
`math.floor` throws away the fraction, because there is no such thing as
column twenty-five and a half.
`TEXT_COLUMNS` works out how many character cells fit across the screen.
`overlayPrint` counts in cells while everything else counts in pixels, so the
width of the overlay is divided by the width of one cell, and
`overlayGetFontWidth` is what tells you that. Two slashes rather than one,
because a fraction of a column is not a place to print. Work it out this way
and the centring stays right at any overlay size, which is the fourth thing in
*Try it*.
[source,lua]
----
colorForeground(255, 90, 90)
----
`colorForeground` sets a colour that stays set. It is not an argument to
`overlayBox`; it is a switch you flip, and everything drawn afterwards comes
out in that colour until you flip it again. That is why `drawGame` sets green,
draws the player, sets red, and then draws all the blocks without mentioning
colour again.
It does not affect `overlayPrint`. The console font is copied onto the overlay
exactly as it is, in its own colours, which the manual's entry for
`overlayPrint` says in as many words. Coloured text needs a loaded font, and
that is lesson nineteen.
=== Try It
. *Make it easier, then unfair.* Change `PLAYER_WIDTH` to `80` and play a
round. Then change it to `12`, the same size as a block. Notice that you did
not have to touch the drawing or the collision to do either one.
. *Speed up faster.* Change `BLOCK_FASTER` from `0.05` to `0.5` and see how
many points you can get. Then try `0`, and ask yourself whether the game is
still worth playing.
. *An extra life.* Give the player a life back every ten points. Everything
you need is already in `updatePlaying`, in the branch that adds to the
score.
. *Play it bigger.* Run it with `Singe -R --canvas=1280x720 dodge`. The
game lays itself out correctly, because it asked the engine how big the
screen was -- except for the centred text. Work out why, and fix it.
. *Pause it.* Add a fourth state, `"paused"`. `SWITCH_BUTTON2` should swap
between `"playing"` and `"paused"`, and nothing should move while paused.
Done right, this is three lines and you do not have to touch `updatePlaying`
at all.
=== Break It on Purpose
Go into `spawnBlock` and delete the line that sets `block.y`, so that it reads:
[source,lua]
----
function spawnBlock()
local block = {}
block.x = math.random(0, screenWidth - BLOCK_SIZE)
table.insert(blocks, block)
end
----
Save, press space, and Singe stops with this:
----
Error executing function 'onOverlayUpdate': dodge.singe:97: attempt to perform arithmetic on a nil value (field 'y')
stack traceback:
dodge.singe:97: in function 'updatePlaying'
dodge.singe:135: in function 'onOverlayUpdate'
----
Line 97 is `block.y = block.y + blockSpeed`. The complaint is that something in
that sum is not a number, and the part in parentheses tells you which one:
`field 'y'`, meaning the `y` inside a table. It has no value, because you
never gave it one, and a thing with no value in Lua is `nil`.
Notice where the error is and where the mistake is. The error is in
`updatePlaying`, forty lines away from `spawnBlock`, where nothing is wrong at
all. `updatePlaying` is only the first piece of code unlucky enough to touch
the damage. This is the normal case, not the exception, and following that
trail backwards is a skill.
Put the line back. Then turn the page, because the next lesson is about
nothing else.
=== What You Learned
* A game is a world that changes, a world that gets drawn, and a decision
about which of those to do -- and those are three separate pieces of code.
* Input handlers should record what is held down; the update should decide
what that means.
* A list of records is the shape of almost every game world.
* Remove things from a list by walking it backwards.
* `collideRects` answers whether two rectangles overlap, and the engine's
version is better than yours because it is right, it reads well, and it has
a family you will meet again.
* One `state` variable holding a word beats three true-or-false flags, and
keeps beating them as the game grows.
* Start a game from one function that resets everything, so that the second
game is exactly like the first.
* Numbers you might want to tune belong in named constants at the top of the
file.
* Build a game in stages that each run. A program that has never worked is
much harder to fix than one that worked five minutes ago.
=== Next Time
You have now written enough code to make interesting mistakes. The next lesson
is the one no beginner's book bothers with: what Singe's error messages
actually say, how to find out what a variable really holds instead of guessing,
and how to cut a problem in half until there is nowhere left for it to hide.