singe/docs/lessons/03-decisions.adoc
2026-09-22 21:57:42 -05:00

505 lines
18 KiB
Text

== Lesson 3: Making Decisions
image::learn/03-decisions.png[The finished lesson, 480]
Everything you have written so far does the same thing every frame. The word in
lesson two moved whether you wanted it to or not, and the only reason it ever
turned round was four blocks of code you were told to take on trust.
This lesson is those four blocks, properly. By the end of it your program will
ask questions, act on the answers, and -- because the most interesting question
a game can ask is "what is the player doing?" -- you will be steering a box
around the window with the arrow keys. Keep that box. It is the player of the
game you build in lesson seven.
=== Start a New Folder
A folder called `decisions`, a file in it called `decisions.singe`, and
`Singe -R decisions` running in a terminal. Same as last time.
=== What an `if` Really Says
Here is the block from lesson two on its own.
[source,lua]
----
if wordX > lastColumn then
wordX = lastColumn
end
----
Four parts. The word `if`. A question. The word `then`. And a body of lines
that run only when the answer to the question is yes, closed off by `end`.
The question is `wordX > lastColumn`, and the `>` is doing the real work. It
compares the two numbers either side of it and produces an answer that is
either *true* or *false*. Those two words are not text and are not numbers;
they are values in their own right, called *booleans*, and a boolean is the
only thing an `if` is interested in.
The indentation is the same idea as in lesson one: the lines inside are indented
so you can see at a glance what belongs to the `if`, and the `end` lines up with
the `if` that opened it. Lua would run it without the spaces. You would not be
able to read it.
There are six of these comparisons and you will use all of them.
[cols="1,4"]
|===
| `a < b` | a is less than b
| `a > b` | a is greater than b
| `a <= b` | a is less than or equal to b
| `a >= b` | a is greater than or equal to b
| `a == b` | a is the same as b
| `a ~= b` | a is not the same as b
|===
The last two are the ones to be careful with, for different reasons.
`~=` means "is not equal to". Most languages write that `!=`, and if you have
seen a program before, your fingers will type `!=` and Lua will refuse it. The
squiggle is on your keyboard next to the `1` or under the `#`, depending on
where you live.
`==` is two equals signs, and it is not a typo for one. This is the single most
common mistake in programming, and it is worth stopping on for a moment.
In lesson two you learned that one equals sign means *put the right-hand side
into the name on the left*. That is a thing you do. It changes something.
Two equals signs mean *are these the same?* That is a question you ask. It
changes nothing, and it hands back true or false.
[source,lua]
----
lives = 3
if lives == 3 then
----
The first line sets `lives` to three. The second asks whether it is three.
Different jobs, different symbols, one character apart. Lua will catch you if
you get it the wrong way round, and you will meet the error it uses at the end
of this lesson.
=== When the Answer Is No
An `if` on its own does nothing at all when the answer is false. Very often you
want the other thing to happen instead, and that is `else`.
[source,lua]
----
if lives > 0 then
message = "Ready"
else
message = "Game Over"
end
----
Exactly one of those two bodies runs, never both, never neither, so `message`
always ends up holding one of the two strings and never nothing.
When there are more than two possibilities, `elseif` chains them.
[source,lua]
----
if score >= 10000 then
rank = "ace"
elseif score >= 5000 then
rank = "pilot"
elseif score >= 1000 then
rank = "rookie"
else
rank = "passenger"
end
----
Lua tries the questions from the top and stops at the first one that is true. A
score of 6000 is not greater than 10000, so it tries the next, which is true,
so `rank` becomes `pilot` and the other two questions are never asked. That
last point matters: the `elseif` for 1000 is also true for a score of 6000, and
it is never reached, which is why the order is from biggest to smallest.
The whole chain needs exactly one `end`, no matter how many `elseif` parts are
in it. It is one decision, not four.
=== True, False, and Nothing at All
You do not have to compare two things to get a boolean. You can keep one.
[source,lua]
----
paused = false
----
That is a variable like any other, and the value in it is the boolean `false`
itself, written without quotes because it is not the word "false", it is the
idea. The same goes for `true`. A variable holding one of them can go straight
into an `if` with no comparison at all:
[source,lua]
----
if paused then
----
Read it as "if paused". That is better English than `if paused == true`, which
does the same thing the long way round, and experienced programmers will look
at you oddly for writing it.
Three words join and flip these answers.
[source,lua]
----
if alive and not shielded then
if coins > 0 or freePlay then
----
`and` is true when both sides are. `or` is true when either side is, or both.
`not` turns true into false and false into true. They read like English and,
unusually for programming, they mean what English means.
There is one more value to know about, and it is the one that produces the most
confusing errors of your first year.
[source,lua]
----
overlayPrint(0, 0, "lives " .. tostring(lives))
----
If you have never put anything into `lives`, that prints `lives nil`. Lua does
not complain about a name it has never seen; it hands you `nil`, which is its
word for *nothing here*. It is not zero and it is not an empty string. Zero is
a number you meant to store. `nil` is the absence of a value.
You have met `nil` twice already without being told. `frames = frames + 1`
without the `frames = 0` line above it fails because you asked Lua to add one
to nothing. And a callback you spell wrong is `nil` too, which is why a
misspelled `onOverlayUpdate` gives you a black window and no error at all:
Singe looks for the name, finds nothing there, and quietly gets on with its
day.
`tostring` is in that line because `..` will not join a boolean or a `nil` to
anything. Numbers it converts for you; these it will not. That was the promise
at the end of lesson two, and now you have a use for it.
Finally, the rule that ties the whole section together, because `if` will
accept anything you give it, not only true and false:
[quote]
Everything in Lua is true except `false` and `nil`.
Zero is true. An empty string is true. Only those two are not, and once you know
it, `if movingLeft then` reads perfectly whether `movingLeft` holds `true`,
`false`, or has never been set at all.
=== Reading the Controls
Time to ask the player something. That takes a new callback.
[source,lua]
----
function onInputPressed(what)
if what == SWITCH_RIGHT then
boxX = boxX + 10
end
end
----
`onInputPressed` is the second callback you have written, and it works exactly
the way lesson one described: you write it, Singe calls it. `onOverlayUpdate`
is called on a timer. This one is called when something happens -- when the
player presses anything the engine recognizes.
The difference from `onOverlayUpdate` is the word inside the parentheses.
Singe has to tell you *which* control was pressed, so it hands the function a
value, and `what` is the name you have chosen to receive it under. Lesson one
called the things you pass into a function arguments; from the inside, the name
that catches one is a *parameter*. The name is yours: call it `what`, call it
`button`, call it `k`. Singe fills it in either way.
`SWITCH_RIGHT` is a name the engine has already given to a value, like
`OVERLAY_UPDATED` in lesson one. You do not have to load anything to use it;
Singe defines all of them before your script runs. There is one for everything
a cabinet has:
`SWITCH_LEFT`, `SWITCH_RIGHT`, `SWITCH_UP`, `SWITCH_DOWN`, `SWITCH_BUTTON1`
through `SWITCH_BUTTON4`, `SWITCH_START1`, `SWITCH_COIN1`, and more. The manual
lists them all. They are deliberately not called "the right arrow key", because
the same `SWITCH_RIGHT` arrives whether the player pushed an arrow key, a
joystick, or a control pad's thumbstick. You write the game once and it works
on all three.
Add that callback, give `boxX` a starting value outside the functions, and draw
something at `boxX`. Press the right arrow. The box jumps ten pixels each time
you press it.
=== It Happened, and It Is Happening
Hold the arrow key down. Nothing more happens.
`onInputPressed` is called once, on the way down, and not again until you let
go and press it again. There is no key repeat. That is exactly right for some
things -- firing a shot, inserting a coin, choosing a menu item -- and exactly
wrong for walking, which needs to keep happening for as long as the key is
held.
The two are genuinely different questions. *It happened* is an event, and you
hear about it once. *It is happening* is a state, and it lasts. Singe tells you
about events, and the way you turn events into state is to remember them
yourself.
Two callbacks, a variable between them.
[source,lua]
----
movingRight = false
function onInputPressed(what)
if what == SWITCH_RIGHT then
movingRight = true
end
end
function onInputReleased(what)
if what == SWITCH_RIGHT then
movingRight = false
end
end
----
`onInputReleased` is the third callback, and it is the mirror of the second:
Singe calls it on the way back up, with the same `SWITCH_` value. Between them
they keep `movingRight` telling the truth about what the player's finger is
doing right now.
Nothing has moved yet, and that is the point. The pressing and the moving are
now two separate jobs. The moving goes where all the per-frame work goes.
[source,lua]
----
function onOverlayUpdate()
if movingRight then
boxX = boxX + BOX_SPEED
end
...
----
The box now slides smoothly for as long as the key is down, at a speed you
control, and the arithmetic is back in the one function that runs sixty times a
second where it belongs. This shape -- events set flags, the frame acts on
flags -- is how nearly every game handles its controls, and you will use it
again in every lesson that has a player in it.
=== Drawing a Box
`overlayBox` takes two corners: the left and top of the rectangle, then the
right and bottom.
[source,lua]
----
colorForeground(255, 220, 0)
overlayBox(boxX, boxY, boxX + BOX_SIZE, boxY + BOX_SIZE)
----
It draws an *outline*, four lines one pixel thick, and not a filled rectangle.
There is no filled rectangle in the overlay drawing calls at all. When you want
a solid block later there are ways to get one, and lesson nine has the usual
one.
Its numbers are pixels, not character cells. This catches people out, because
`overlayPrint` in lesson two counted in cells of six pixels by thirteen and
everything else in Singe counts in pixels. `overlayPrint` is the odd one; the
rest of the drawing calls -- `overlayBox`, `overlayLine`, `overlayCircle`,
`overlayEllipse`, and `overlayPlot` -- all work in the same pixels that
`overlayGetWidth` reports.
`colorForeground` sets the colour, as three numbers from 0 to 255: how much
red, how much green, how much blue. All three at 255 is white, all at 0 is
black, and `(255, 220, 0)` is a warm yellow. Notice that it is not an argument
to `overlayBox`. It is a setting: you change it, and everything you draw after
that comes out in the new colour until you change it again. `overlayPrint` is
the exception once more -- the console font arrives with its own colours baked
in and ignores you.
=== The Whole Thing
[source,lua]
----
BOX_SIZE = 20
BOX_SPEED = 2
width = overlayGetWidth()
height = overlayGetHeight()
maxX = width - BOX_SIZE - 1
maxY = height - BOX_SIZE - 1
boxX = width // 2
boxY = height // 2
movingLeft = false
movingRight = false
movingUp = false
movingDown = false
function onInputPressed(what)
if what == SWITCH_LEFT then
movingLeft = true
elseif what == SWITCH_RIGHT then
movingRight = true
elseif what == SWITCH_UP then
movingUp = true
elseif what == SWITCH_DOWN then
movingDown = true
end
end
function onInputReleased(what)
if what == SWITCH_LEFT then
movingLeft = false
elseif what == SWITCH_RIGHT then
movingRight = false
elseif what == SWITCH_UP then
movingUp = false
elseif what == SWITCH_DOWN then
movingDown = false
end
end
function onOverlayUpdate()
if movingLeft and not movingRight then
boxX = boxX - BOX_SPEED
elseif movingRight and not movingLeft then
boxX = boxX + BOX_SPEED
end
if movingUp and not movingDown then
boxY = boxY - BOX_SPEED
elseif movingDown and not movingUp then
boxY = boxY + BOX_SPEED
end
if boxX < 0 then
boxX = 0
elseif boxX > maxX then
boxX = maxX
end
if boxY < 0 then
boxY = 0
elseif boxY > maxY then
boxY = maxY
end
overlayClear()
if movingLeft or movingRight or movingUp or movingDown then
colorForeground(255, 220, 0)
else
colorForeground(80, 160, 255)
end
overlayBox(boxX, boxY, boxX + BOX_SIZE, boxY + BOX_SIZE)
overlayPrint(0, 0, "x " .. boxX .. " y " .. boxY)
return OVERLAY_UPDATED
end
----
Four things in there are worth a second look.
`movingLeft and not movingRight` is there because a player can hold both arrows
at once. Without the `not`, the two would fight and the box would jitter or
drift; with it, holding both means neither wins and the box stands still, which
is what a player expects.
`maxX` is worked out once at the top, using `overlayGetWidth` the way lesson two
did, and the `- 1` is because the surface's last pixel is one less than its
width. Take the `- 1` out and the box's right-hand edge falls off the surface
and vanishes, because Singe throws away anything you draw outside the overlay
without complaining.
The clamping blocks use `elseif` rather than two separate `if` blocks. The box
cannot be off the left edge and off the right edge at the same time, so asking
the second question when the first was true is wasted work.
And the colour decision is an `if` whose only job is to pick a value. Yellow
while the player is doing something, blue while they are not, which is a free
way of seeing that your input handling actually works.
=== Try It
. *Change the speed.* `BOX_SPEED = 1` and `BOX_SPEED = 8`. At eight, watch what
the clamping does when you shove the box into a corner.
. *Show your flags.* Add a line to `onOverlayUpdate`:
`overlayPrint(0, 1, "left " .. tostring(movingLeft))`. Hold and release the
left arrow and watch it flip. Then take the `tostring` off and read the error
Lua gives you.
. *Make it faster when a button is down.* Add a `running` flag driven by
`SWITCH_BUTTON1` in the two input callbacks, and use an `if` in
`onOverlayUpdate` to move by twice `BOX_SPEED` while it is true.
. *Mark the middle.* Draw a second, small box in the centre of the window that
changes colour when the player's box overlaps it. You will need `and` to join
four comparisons, and it is harder than it sounds. Getting it slightly wrong
is normal; lesson eleven does it properly.
. *Take the releases out.* Delete the whole `onInputReleased` function and run
it. The box takes one step and then never stops. Work out which line is now
never reached.
=== Break It on Purpose
Find the clamping block and take one of the equals signs out of a comparison,
so it reads:
[source,lua]
----
if boxX = 0 then
----
Save it. The window sits there empty and the terminal says:
----
Error running script: decisions.singe:57: 'then' expected near '='
----
This is the mistake the whole `==` section was warning about, and the message
is a good example of an error that is completely accurate and completely
unhelpful if you take it literally. Lua is not asking you to add the word
`then`. Lua got as far as `if boxX`, expected a comparison or a `then` next,
and found `=`, which cannot be either.
Whenever you see `'then' expected` after an `if`, look for a single `=` that
should be a double one. It is nearly always that.
Note where this one appeared: Singe never opened the window, the same as lesson
one's unfinished string, because Lua could not read the file. The frame counter
error in lesson two happened while running. Errors in the shape of your code
stop you before the game starts. Errors in what your code does wait until it
runs.
=== What You Learned
* `if question then ... end` runs a body only when the answer is true.
* `else` gives the other case, and `elseif` chains more; the first true
question wins and the rest are never asked.
* The comparisons are `<`, `>`, `<=`, `>=`, `==`, and `~=`.
* `=` puts a value into a name. `==` asks whether two things are the same.
* `true` and `false` are values, and you can keep them in variables.
* `and`, `or`, and `not` join and flip those answers.
* `nil` is what you get from a name you never set, and it is not zero.
* Everything is true except `false` and `nil`.
* `onInputPressed` and `onInputReleased` are callbacks Singe calls with a
`SWITCH_` value when the player presses or lets go of a control.
* A press is an event and happens once; hold the state in a variable and act on
it every frame.
* `overlayBox` draws an outline in pixels, and `colorForeground` is a setting,
not an argument.
=== Next Time
One box is easy. A hundred stars are not, at least not the way you have written
things so far, because typing a hundred `overlayPlot` lines is nobody's idea of
a good evening. Lesson four is the loop: telling the computer to do the same
thing many times, with something different each time round, which is what makes
a screen full of anything possible.