singe/docs/lessons/08-when-it-goes-wrong.adoc
2026-09-22 21:57:42 -05:00

472 lines
19 KiB
Text

== Lesson 8: When It Goes Wrong
image::learn/08-when-it-goes-wrong.png[The finished lesson, 480]
You have written a game. From here on you will spend more of your time fixing
programs than writing them, and that is not a sign that anything is wrong with
you. It is the job. The difference between someone who enjoys programming and
someone who gives it up is almost never talent; it is whether they learned to
read what the computer is telling them.
So this lesson has nothing new in it to build. It is the five messages you will
see most often for the rest of your life, what each one actually means, and
four ways of hunting down the mistake behind it. Keep `dodge.singe` from
lesson seven open, because you are going to break it on purpose several times.
=== Two Kinds of Stop
Singe stops for two different reasons, and it says so in two different ways.
When the script will not even start, you get this shape:
----
Error running script: dodge.singe:92: 'then' expected near 'spawnBlock'
----
Lua reads your whole file before running a word of it, and this means it could
not make sense of what it read. Nothing ran at all.
When the script started fine but something went wrong later, you get this
shape instead:
----
Error executing function 'onOverlayUpdate': dodge.singe:98: attempt to perform arithmetic on a nil value (global 'blockSped')
stack traceback:
dodge.singe:98: in function 'updatePlaying'
dodge.singe:136: in function 'onOverlayUpdate'
----
The game was running. Singe called your `onOverlayUpdate`, as it does sixty
times a second, and this time the call did not come back.
Both shapes contain the same three things, and they are the three things you
want: a file, a line, and a complaint. Read them in that order, every time.
The line numbers in this lesson are from my copy of the file. Yours will be a
line or two out. Nothing about reading an error depends on the number matching.
=== The Ones That Stop It Before It Starts
These are the easy ones, and they are also the ones that feel worst, because
the screen stays black and there is nothing to look at.
Go into `updatePlaying` and delete the word `then` from the line
`if spawnTimer <= 0 then`. Save:
----
Error running script: dodge.singe:92: 'then' expected near 'spawnBlock'
----
Lua got to the end of the `if` line, wanted the word `then`, and found the
start of the next line instead -- which is `spawnBlock()`, and that is the
`near 'spawnBlock'` part. "Near" means "this is where I noticed", and it is
usually the first thing *after* what you left out.
Now a nastier one. Put `then` back, and instead delete the `end` on the last
line of `spawnBlock`. Save:
----
Error running script: dodge.singe:148: 'end' expected (to close 'function' at line 58) near <eof>
----
Line 148 is the last line of the file. There is nothing wrong with line 148.
Lua read to the very end -- `<eof>` is what it calls the end of a file -- still
waiting for an `end` that never came, and by then it had swallowed every
function after `spawnBlock` as if it were part of it.
The useful half of that message is `(to close 'function' at line 58)`, which is
where `spawnBlock` begins. When a message points at the end of the file, believe
what it says in the parentheses, not the line number. This is also why the indenting matters so
much: a missing `end` is almost invisible in a list of lines, and obvious the
moment everything below it is indented one step too far.
Two more worth recognising, because you will meet both this week:
----
Error running script: dodge.singe:41: unfinished string near '"GAME OVER)'
----
A quote you opened and never closed, which you met in lesson one.
----
Error running script: dodge.singe:135: 'then' expected near '='
----
That one is `if state = "playing" then`. One `=` sets a variable, two `==` ask
a question, and Lua will not let you set one inside an `if`. It looks like a
strange complaint, and it always means the same thing.
=== Attempt to Index a Nil Value
This is the one you will see most, by a wide margin.
Go into `spawnBlock` and misspell `block` on the first line that uses it, so
it reads `blocl.x = math.random(0, screenWidth - BLOCK_SIZE)`. Save, press
space:
----
Error executing function 'onOverlayUpdate': dodge.singe:60: attempt to index a nil value (global 'blocl')
stack traceback:
dodge.singe:60: in function 'spawnBlock'
dodge.singe:92: in function 'updatePlaying'
dodge.singe:136: in function 'onOverlayUpdate'
----
*Indexing* is what the dot does. `blocl.x` means "go into the thing called
`blocl` and find the `x` in it". You can only do that to a table.
*Nil* is what Lua calls a name with nothing behind it. You have not seen much
of it because you have been careful to give everything a value, but every name
you have never used is `nil` already, and typing a name wrong invents a brand
new one.
So the whole message says: *you used a dot on something that does not exist*.
The part in parentheses is the gift. `global 'blocl'` is the exact name of the
thing that was empty. Search your file for it. If it appears exactly once, it
is a spelling mistake, and you have found it in ten seconds.
The word before the name tells you where Lua looked for it:
* `global` -- a name with no `local` in front of it anywhere.
* `local` -- a name you declared with `local` in the function you are in.
* `field` -- a name after a dot, like the `y` in `block.y`.
* `upvalue` -- a `local` from further out that this function can see, which
is what all of `dodge.singe`'s variables are, since they are declared at the
top of the file and used inside functions.
`field` is the other common cause, and it is not a spelling mistake at all. If
you wrote `block.hit.when` and never set `block.hit`, you get
`attempt to index a nil value (field 'hit')`. The dot that fails is the second
one. Lua tells you which by naming it.
=== Attempt to Call a Nil Value
Put `blocl` back. Now misspell an engine function: in `drawGame`, change
`overlayPrint` to `overlayPirnt`. Save:
----
Error executing function 'onOverlayUpdate': dodge.singe:36: attempt to call a nil value (global 'overlayPirnt')
stack traceback:
dodge.singe:36: in function 'drawGame'
dodge.singe:140: in function 'onOverlayUpdate'
----
Same `nil`, different crime. *Calling* is what the parentheses do, and you can
only do that to a function. There is no function called `overlayPirnt`, so
`overlayPirnt` is `nil`, so the parentheses have nothing to call.
This happens for four reasons, in order of how often:
. You misspelled one of your own functions.
. You misspelled one of Singe's. Nothing in the engine checks your spelling
when the file loads, so a misspelled engine name is just another name that
happens to be empty, and it costs you nothing until the line runs.
. The function does not exist in Singe at all -- it was a reasonable guess, and
reasonable guesses are wrong about half the time. The manual's function list
is the only way to be sure.
. You made it `local` and called it from somewhere above where it is declared.
This is exactly why the functions in `dodge.singe` are global.
There is a much worse version of this mistake, and you met it in lesson one
without knowing: spell a *callback* wrong and you get no message at all.
`onOverlayUpdated` is not an error. It is a perfectly good function that Singe
has never heard of and will never call, so the screen stays black and nothing
complains. When a whole feature does nothing whatsoever, suspect the name.
=== Attempt to Perform Arithmetic on a Nil Value
Put `overlayPrint` back. In `updatePlaying`, misspell `blockSpeed` in the
falling line, so it reads `block.y = block.y + blockSped`:
----
Error executing function 'onOverlayUpdate': dodge.singe:98: attempt to perform arithmetic on a nil value (global 'blockSped')
stack traceback:
dodge.singe:98: in function 'updatePlaying'
dodge.singe:136: in function 'onOverlayUpdate'
----
Arithmetic is `+`, `-`, `*`, and `/`. You cannot add nothing to a number.
Again the parentheses name the empty thing, and again the commonest cause is a
typo -- but this one has a second cause that is worth knowing, because it is
harder to see.
A variable you never gave a number to is `nil`, not zero. If you add a high
score to the game and write `local hiScore` at the top without `= 0`, it holds
`nil` until something sets it, and the first `hiScore + score` stops the game.
Lua does not start numbers at zero for you. You start them at zero.
Two close relatives, with the same cause and the same cure:
----
dodge.singe:37: attempt to concatenate a nil value (upvalue 'hiScore')
----
`..` joining a string to something that is not there. You will see this one
whenever you print a score you forgot to set.
----
dodge.singe:118: attempt to compare number with nil
----
That is `if lives > 0 then` with nothing in `lives`. Note that this message
does not name anything in parentheses. When Lua cannot tell you which side was
empty, the next section is how you find out.
=== An Argument of the Wrong Kind
Every message so far came from Lua. This one comes from Singe, and it looks
completely different, so it is worth meeting on purpose.
Suppose you want to see what is in `blocks`, and you try to print it. Add this
line to `drawGame`, under the score:
[source,lua]
----
overlayPrint(1, 3, blocks)
----
----
37:overlayPrint: Argument 3 must be a string.
----
No file name, no traceback, no `attempt to` anything. An engine complaint
starts with the line number, then the name of the function that refused, then
what it wanted. Here `overlayPrint` was handed a table where it needs text, and
it stopped rather than draw something meaningless.
The sibling message counts rather than checks:
----
34:overlayBox: Expected 4 argument(s), got 3.
----
That is an `overlayBox` with a corner missing. The odd-looking `argument(s)` is
the engine's, not a typo of yours.
When you get one of these, the manual's entry for that function is the answer,
and it will take you a minute. Every entry lists the arguments in order with
what each one has to be. Singe checks them because the alternative is a game
that runs for ten minutes and then draws garbage.
=== Finding Out What Is Really in There
Half of debugging is not reading errors at all. It is the case where nothing
crashes and nothing works: blocks that never appear, a player who will not
move, a score stuck at zero. For that you need to see inside the running game,
and there are two ways.
The first is `debugPrint`, which writes a line to the terminal you started
Singe from. Take the line you cannot make sense of and put one above it:
[source,lua]
----
debugPrint("blocks " .. #blocks .. ", timer " .. spawnTimer)
----
Now you are looking at what is actually there instead of what you are sure is
there, and that gap is where bugs live. Run it and you will see the truth
about `playerX`, too: it prints as `160.0`, not `160`, because dividing with
`/` in Lua always gives a number with a fractional part, even when the
fraction is zero.
Two things will bite you. The first is that `debugPrint` insists on text or a
number; hand it anything else and it stops the game the way `overlayPrint`
did. `debugPrint(blocks)` will not show you a list, and
`debugPrint("left " .. goingLeft)` will not show you a `true` or a `false`
either:
----
dodge.singe:78: attempt to concatenate a boolean value (upvalue 'goingLeft')
----
`tostring` is the fix. It takes anything at all and gives you text for it, so
`tostring(goingLeft)` is `"true"` or `"false"` and the line works.
The second is that `onOverlayUpdate` runs sixty times a second, so a
`debugPrint` inside it prints sixty lines a second and you cannot read any of
them. The manual says as much in its entry: the call is cheap, but nothing
throttles it.
Before anything else, put one switch in front of every debug line you add, so
that you can turn the noise off without hunting them down again:
[source,lua]
----
local DEBUG = true
function debugLog(text)
if DEBUG then
debugPrint(text)
end
end
----
Every `debugLog` in the game obeys that one `true`. Change it to `false` and
the game goes quiet; change it back and everything returns.
Then print when something *happens* rather than every frame:
[source,lua]
----
debugLog("hit at y " .. math.floor(block.y) .. ", lives now " .. lives)
----
Or print on a timer, counted down exactly the way blocks are spawned:
[source,lua]
----
reportTimer = reportTimer - 1
if reportTimer <= 0 then
debugReport()
reportTimer = REPORT_FRAMES
end
----
The `learn` folder has the whole thing as `08-when-it-goes-wrong.singe`: it is
lesson seven's game with a report about every two seconds, a line when you get
hit, and a line when the game ends.
The second way to see inside is to put it on screen, which is better for
anything that changes every frame, because sixty lines a second in a terminal
is useless but a number that ticks in the corner is easy to watch:
[source,lua]
----
if DEBUG then
overlayPrint(1, 3, "BLOCKS " .. #blocks .. " PLAYER " .. playerX)
end
----
=== Cutting the Problem in Half
When you cannot see which of twenty lines is wrong, stop looking and start
removing.
Put two dashes at the start of a line and Lua ignores the rest of it. That is
a *comment*, and the usual use is to leave notes for yourself, but the better
use is this one:
[source,lua]
----
-- overlayPrint(1, 1, "SCORE " .. score .. " LIVES " .. lives)
----
Comment out half of `drawGame` and run it. If the problem is still there, it
is in the half you kept. If it went away, it is in the half you removed. Put
that half back and cut *it* in half. Twenty lines takes five rounds of this,
and each round is one save and one look.
It works on more than lines. Comment out the body of `updatePlaying` and the
blocks stop moving, which tells you whether the thing you are chasing is in
the moving or in the drawing. Comment out the `collideRects` branch and see
whether the game stops crashing. You are not trying to fix anything while you
do this. You are trying to find out where it is not.
=== Going Round Faster
All of this depends on being able to try something in a couple of seconds, and
that is what `-R` is for. You have been using it since lesson one; here is what
it is actually doing, and it is worth knowing now that you are going to lean on
it.
Singe watches every script file your game loaded. When you save one, the game
starts again from the beginning: everything the script made is thrown away and
your file is run afresh, in the time it takes to read the file rather than the
time it takes to start the engine. `F5` does the same on demand, without
saving anything.
A key you are holding through a reload is ignored until you let go, so the
player will not run off on his own after a reload.
The two kinds of stop behave differently here, and it is useful to know which
you are looking at without reading the message. A script that will not compile
is *printed and survived*: the window stays open and empty, the file stays
watched, and saving the fix brings the game back. A crash inside a callback --
all the `attempt to` messages, and the engine's argument complaints -- takes
Singe down, and you start it again by hand.
=== Reading a Traceback
The lines under `stack traceback:` are the list of functions that were in
progress when everything stopped, innermost first. Here is the one from
`blocl` again:
----
dodge.singe:60: in function 'spawnBlock'
dodge.singe:92: in function 'updatePlaying'
dodge.singe:136: in function 'onOverlayUpdate'
----
Read it from the bottom up and it is a sentence. Singe called
`onOverlayUpdate`. At line 136, `onOverlayUpdate` called `updatePlaying`. At
line 92, `updatePlaying` called `spawnBlock`. At line 60, `spawnBlock` gave up.
The top line is where it broke. The lines under it are how it got there, and
they are what you need when the top line looks innocent. A function that adds
a number to `block.y` is not wrong; a function that put a block in the list
without a `y` is. Only the trail joins the two, and the trail is printed for
you every single time.
=== The Habit
Everything above is technique. This is the habit, and it matters more:
*Change one thing. Run it. Look at what happened.*
When something will not work, the temptation is to change four things at once,
because one of them is bound to be it. Do that and it stops working in a new
way, and now you do not know which of the four did it, or whether two of them
are cancelling out. You have made the problem bigger and hidden it better.
One change. Run. Look. It feels slower. It is the fastest thing there is, and
every experienced programmer you will ever meet does it, for exactly the
reason you are about to find out.
=== Try It
. *Meet them all.* Make each of the five mistakes in this lesson in
`dodge.singe` on purpose, one at a time, and read the message before you fix
it. You will never be afraid of them again.
. *Watch the spawner.* Put a `debugPrint` inside `spawnBlock` that prints the
new block's `x`. Play for thirty seconds, then look at the numbers. Are they
spread across the screen, or do they favour one end?
. *Turn it off.* Change `DEBUG` to `false` in the lesson script, and satisfy
yourself that nothing prints and the game is unchanged.
. *A bug with no error.* Change the falling loop to run forwards, so it reads
`for i = 1, #blocks do`. Play a round. Nothing crashes and nothing is
printed, but the game is wrong. Put a `debugPrint` inside that loop and work
out what it is really doing.
. *Break a callback's name.* Rename `onInputPressed` to `onInputPress` and run
it. Nothing is printed, nothing crashes, the player will not move, and space
will not start a game. Then explain to yourself why that is the most
dangerous mistake in this lesson.
=== What You Learned
* Errors have a file, a line, and a complaint. Read them in that order.
* `Error running script` means nothing ran; the shape with a traceback means it
was running and stopped.
* `nil` is a name with nothing behind it, and it is behind most error messages
you will ever see.
* The word in parentheses names the empty thing, and says whether it was a
global, a local, a field, or an upvalue.
* A missing `end` is reported at the end of the file, and the parentheses tell
you where the mistake really is.
* Engine complaints look different: a line number, a function name, and what it
wanted instead.
* `debugPrint` shows you what a variable really holds. `tostring` makes
anything printable, and a switch like `DEBUG` turns the noise off.
* Comment a half out with `--` to find out which half the problem is in.
* A misspelled callback produces no error at all, which makes it worse than one
that does.
* Change one thing, run it, look.
=== Next Time
That is the end of part one. You can write a game, and now you can fix one,
which means everything from here is addition rather than foundation. Part two
starts by replacing those coloured boxes with actual pictures, and from there
the game you have been building begins to look like a game somebody else
would want to play.