Add two Breakout examples and the tutorials that build them
Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m10s
akbasic CI Build / sanitizers (push) Failing after 4m5s
akbasic CI Build / coverage (push) Failing after 3m29s
akbasic CI Build / akgl_build (push) Failing after 21s
akbasic CI Build / mutation_test (push) Failing after 3m19s

Two complete games in `examples/breakout/`, both 100% BASIC: `characters/`
draws its wall in the text grid with two `DATA` sprites for the ball and
paddle, and `sprites/` loads CC0 artwork and captures its whole screen with
`SSHAPE`/`SPRSAV`. They take opposite shapes for reasons that are entirely
this interpreter's, which is what the chapters are for.

`docs/17-tutorial-breakout.md` and `docs/18-tutorial-breakout-artwork.md`
build each one a step at a time, and end in a checklist of the rules a real
program runs into: create every name before the loop starts, write a text row
whole, loop with `GOTO` rather than `DO`, put the float on the left. Every
trap is a runnable block with its own output rather than a claim -- the value
pool dying at four thousand names, the skipped `BEGIN` block that breaks its
caller's `RETURN`, `SSHAPE` ignoring a subscript, `READ`'s single cursor.

Five figures, generated from the listings beside them by `docs_screenshots`,
and a `breakout_art` setup so the ones that load artwork load the example's
own. The character game's wall cannot be photographed -- the screenshot host
omits the text layer on purpose -- so it is shown as compared output instead.

`docs/07-sound.md` never said `SOUND`'s frequency is a SID register value
rather than hertz, which both games depend on. It says so now, with the
conversion from `src/audio_tables.c:84`.

`TODO.md` gains the thirteen defects the two games turned up -- §6 items 30
to 33 and all of §9 -- each with a reduction that fits on a screen, the file
and line of the cause, and what a fix would touch.

Verified: `docs_examples` passes in both build configurations,
`docs_screenshots --check` re-renders all thirteen figures and byte-compares
them, the full 109-test suite passes in both builds, every quoted fragment
was checked to appear verbatim in the listing it came from, and every
relative link and anchor resolves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-01 22:54:42 -04:00
parent 8a50d07eef
commit cb0e2d0800
28 changed files with 4424 additions and 2 deletions

View File

@@ -13,6 +13,12 @@ name, and a machine with no sound card still runs the interpreter — only `SOUN
Voice, frequency, duration. Voices are numbered from 1. The duration is in *jiffies* —
sixtieths of a second — as on a C128, so 60 is one second.
**The frequency is a SID register value, not hertz.** As on a C128 the pitch is
`frequency * 1022730 / 16777216` — the NTSC system clock over the oscillator's 24-bit
accumulator — so the 4000 above is about 244 Hz, and the argument ranges from 0 to
65535. Work the notes you need out once and write the arithmetic down beside them: 4298
is C4, 8579 is C5, 17175 is C6. `PLAY` takes note names and does the conversion for you.
Further arguments give a frequency sweep: a step, a direction and a range, which is
what makes a siren or a laser.

View File

@@ -0,0 +1,763 @@
# 17. Tutorial: Breakout
This chapter builds a complete game — three lives, six rows of bricks, three level
layouts, a high score and an attract mode that plays itself — out of nothing but the
verbs in the earlier chapters. The finished listing is
[`examples/breakout/characters/breakout.bas`](../examples/breakout/characters/breakout.bas),
and it is worth having open beside this.
**The ball and the paddle are sprites; the bricks, the HUD and the messages are
characters in the text grid.** That split is the first decision and everything else
follows from it, so it is Step 1.
Run the finished game with the SDL build:
```sh norun
$ ./build-akgl/basic examples/breakout/characters/breakout.bas
```
| Key | Does |
|---|---|
| left / right | move the paddle |
| space | start a game, then launch the ball |
| P | pause |
| Q or escape | quit |
Chapter 18 builds the same game again out of sprite artwork, and takes a completely
different shape. Read this one first: it is the smaller of the two and it is where the
rules that constrain both are explained.
---
## Step 1: Decide what is a sprite and what is a character
Two layers of this interpreter are redrawn from state it keeps for you, every frame,
without your program doing anything:
- the **text grid**, repainted row by row by the akgl sink, and
- the **sprites**, walked slot by slot after it.
The drawing verbs are not one of them. `DRAW`, `BOX`, `CIRCLE` and `PAINT` go straight
into the renderer's back buffer, and the text layer paints over the whole window on its
way past — so in the standalone SDL build **nothing you draw with them is ever visible**.
That is recorded as a defect (`TODO.md` §9 item 3) rather than a design, but it is the
interpreter you have.
So this game uses no drawing verb at all. Anything that has to move smoothly is a sprite;
anything that can live on a 16-pixel grid is text. Ball and paddle move; bricks, score
and messages do not.
**If you want artwork on the screen, that rule flips** and the whole architecture changes
with it. That is Chapter 18.
## Step 2: Measure the screen once, at the top
```basic norun
CW# = 16
CH# = 16
SCW# = RGR(1)
SCH# = RGR(2)
COLS# = SCW# / CW#
ROWS# = SCH# / CH#
```
`RGR(1)` and `RGR(2)` are the window in pixels — see
[Chapter 6](06-graphics.md#rgr) — and the game asks for them rather than assuming 800 by
600, so it survives a host that opens a different window.
**The cell size is a constant and has to be**, which is the one thing here you cannot
derive. `WINDOW` knows the grid, but the standalone frontend's tee sink never offers it
(`TODO.md` §9 item 3 again), so a program has no way to ask. 16 by 16 is the bundled
C64_Pro_Mono at 16 points. Change the font or the point size and change these two with
it; everything below is derived from them, including the ball's speed relative to the
wall.
Lay the rest of the geometry out in the same block — the wall in cells, the play area in
pixels:
```basic norun
BRW# = 4
BCOLS# = 10
BROWS# = 6
BTOP# = 4
BLEFT# = (COLS# - (BRW# * BCOLS#)) / 2
TOPY# = 2 * CH#
MAXX# = SCW# - 8
PW# = 144
PY# = 528
```
A brick is four cells wide, so ten of them are forty cells, and `BLEFT#` centres that in
whatever `COLS#` turned out to be. Integer division truncates here, which is exactly what
a cell index wants.
## Step 3: Create every name before the game starts
This is the rule that decides whether your game runs for four minutes or for ever, and it
is worth getting straight before you write a single subroutine.
**A name that is created costs value-pool slots that are never handed back.** The pool is
a bump allocator with 4096 slots for the life of the run. Scope exit *does* destroy
variables — a `GOSUB`'s locals go when it returns — but their slots are not reclaimed, so
recreating the same local on the next call takes fresh ones.
Here is the whole defect on one screen:
```basic
X# = 0
FOR T# = 1 TO 6000
GOSUB SUBA
NEXT T#
PRINT "OK " + X#
END
LABEL SUBA
LOC# = 1
X# = X# + LOC#
RETURN
```
```output
? 8 : RUNTIME ERROR Array of 1 elements does not fit in the 0 remaining value slots
```
Six thousand calls, one new name each, and it dies on the four-thousand-and-somethingth
— naming the line that was unlucky rather than the line that was wrong. A game loop that
creates one name per tick is dead in half a minute. This one was, and it took twenty-five
seconds to do it.
The fix is one line, moved:
```basic
X# = 0
LOC# = 0
FOR T# = 1 TO 6000
GOSUB SUBA
NEXT T#
PRINT "OK " + X#
END
LABEL SUBA
LOC# = 1
X# = X# + LOC#
RETURN
```
```output
OK 6000
```
So **declare everything at the top** — variables, scratch temporaries, loop counters, all
of it — and after that the program only ever stores into names that already exist:
```basic norun
DIM BR#(60)
DIM LAY$(6)
DIM BSG$(6)
SCORE# = 0
LIVES# = 3
BX# = 0
BY# = 0
HIT# = 0
BI# = 0
I# = 0
R# = 0
C# = 0
KI# = 0
```
`I#`, `R#`, `C#` and `KI#` are in that list because a `FOR` counter is a name like any
other: name one that does not exist yet and every entry to the loop spends slots.
The same block does a second job. A `GOSUB` gets its own scope, and assignment walks up
the chain to find an outer name — but a name *first seen* inside a subroutine is created
in that subroutine and dies at `RETURN`. `HIT#` and `BI#` are how `HITTEST` answers its
caller, so they have to exist before anybody calls it. See
[Chapter 4](04-control-flow.md#goto-and-gosub) for the scope rules.
This is filed as `TODO.md` §6 item 30. Until it is fixed, the declaration block is not a
style preference — it is the price of a program that runs.
## Step 4: Build the wall, and write a row whole
Levels are `DATA`, one string per row, `1` for a brick and `0` for a hole:
```basic norun
LABEL LAY1
DATA "1111111111"
DATA "1111111111"
DATA "1111111111"
DATA "1111111111"
DATA "1111111111"
DATA "1111111111"
LABEL LAY2
DATA "0011111100"
DATA "0111111110"
DATA "1111111111"
DATA "1111111111"
DATA "0111111110"
DATA "0011111100"
```
`RESTORE` takes a label — it wants a line number and a label evaluates to one — so a
layout is just a named place in the `DATA` stream, and adding a fourth level is a block
and one `IF`:
```basic norun
LABEL LOADLAY
N# = MOD((LEVEL# - 1), 3)
IF N# = 0 THEN RESTORE LAY1
IF N# = 1 THEN RESTORE LAY2
IF N# = 2 THEN RESTORE LAY3
FOR R# = 0 TO 5
READ LAY$(R#)
NEXT R#
RETURN
```
Now draw it — and here is the second rule that shapes this listing:
**`CHAR` terminates the row where it stops.** A row of the grid is a C string, so writing
at column N erases everything from N+1 on, and writing *past* the terminator of a short
row draws nothing at all. There is no way to poke one character into the middle of a row
and leave the rest alone.
So a row is rebuilt whole and written from column 0, in one `CHAR`:
```basic
DIM LAY$(6)
DIM BSG$(6)
BSG$(0) = "[##]"
BSG$(1) = "[##]"
BSG$(2) = "[==]"
BSG$(3) = "[==]"
BSG$(4) = "[--]"
BSG$(5) = "[--]"
LAY$(0) = "1111111111"
LAY$(1) = "1111111111"
LAY$(2) = "1101111011"
LAY$(3) = "1111111111"
LAY$(4) = "1011110111"
LAY$(5) = "0110110111"
S$ = ""
T$ = ""
R# = 0
C# = 0
FOR R# = 0 TO 5
GOSUB BUILDROW
PRINT S$
NEXT R#
END
LABEL BUILDROW
S$ = " "
FOR C# = 0 TO 9
T$ = MID(LAY$(R#), C#, 1)
IF T$ = "1" THEN S$ = S$ + BSG$(R#)
IF T$ = "0" THEN S$ = S$ + " "
NEXT C#
RETURN
```
```output
[##][##][##][##][##][##][##][##][##][##]
[##][##][##][##][##][##][##][##][##][##]
[==][==] [==][==][==][==] [==][==]
[==][==][==][==][==][==][==][==][==][==]
[--] [--][--][--][--] [--][--][--]
[--][--] [--][--] [--][--][--]
```
That block runs anywhere, with or without a graphics device, because `PRINT` and `CHAR`
are building the same string. In the game the last line is `CHAR 1, 0, R2#, S$` instead,
and the leading spaces are `" " * BLEFT#`.
**The rows are told apart by shape, not by colour.** `CHAR` parses a colour argument and
ignores it; the sink draws in one colour. `[##]`, `[==]` and `[--]` are what makes a
60-point row look different from a 10-point one.
Killing a brick clears its cell and redraws that one row:
```basic norun
LABEL KILLBR
BR#(BI#) = 0
LEFTN# = LEFTN# - 1
PTS# = (BROWS# - BR2#) * 10
SCORE# = SCORE# + PTS#
GOSUB DRAWBR
GOSUB DRAWHUD
IF LEFTN# < 1 THEN STATE# = 2
RETURN
```
`LEFTN#` is counted up when the wall is built and down when a brick goes, so "is the
level clear" is a comparison rather than a scan of sixty cells.
## Step 5: Make the ball and the paddle
`SPRSAV` takes an **integer array** of 63 bytes: three per row, twenty-one rows, most
significant bit leftmost. A clear bit is transparent — see
[Chapter 8](08-sprites.md#from-data).
Two patterns are all this game needs. The ball is an 8 by 8 disc in the *top-left corner*
of the pattern, so the sprite's position and its pixel rectangle are the same thing and
no offset arithmetic is needed anywhere. The paddle is a solid 24 by 6 bar with `SPRITE`'s
x-expand bit set, which makes it 48 wide; three of them laid end to end are one 144-pixel
paddle.
```basic requires=akgl screenshot=breakout-paddle size=240x120
DIM SB#(63)
DIM SP#(63)
I# = 0
D# = 0
FOR I# = 0 TO 62
SB#(I#) = 0
SP#(I#) = 0
NEXT I#
FOR I# = 0 TO 7
READ D#
SB#(I# * 3) = D#
NEXT I#
FOR I# = 0 TO 17
SP#(I#) = 255
NEXT I#
SPRSAV SB#, 1
SPRSAV SP#, 2
SPRSAV SP#, 3
SPRSAV SP#, 4
SPRITE 1, 1, 2
SPRITE 2, 1, 15, 0, 1, 0
SPRITE 3, 1, 15, 0, 1, 0
SPRITE 4, 1, 15, 0, 1, 0
MOVSPR 1, 108, 40
MOVSPR 2, 48, 80
MOVSPR 3, 96, 80
MOVSPR 4, 144, 80
DATA 60, 126, 255, 255, 255, 255, 126, 60
```
![](images/breakout-paddle.png)
The three segments meet with no seam, which is the whole point of the arrangement. The
game writes the same 63 bytes out as `DATA` a row at a time, with the bit pattern drawn
in a comment beside it; the loops above are the same two patterns spelled shorter so the
figure fits on a page.
Moving them is four `MOVSPR`s, and each coordinate is worked out into a variable first:
```basic norun
LABEL SHOWSPR
MOVSPR 1, BX#, BY#
MOVSPR 2, PX#, PY#
X2# = PX# + 48
MOVSPR 3, X2#, PY#
X3# = PX# + 96
MOVSPR 4, X3#, PY#
RETURN
```
**Do not write `MOVSPR 3, PX# + 48, PY#`.** `MOVSPR` reads a leading sign as *move by this
much* rather than *move to here* — see [Chapter 8](08-sprites.md#showing-and-moving) — and
an expression that starts with one is the relative form. Computing into `X2#` first is
unambiguous.
Sprites are positioned by their top-left corner in **device pixels**, so ball, paddle and
walls are all plain pixel arithmetic. Only the bricks are in cells, and exactly one
routine converts between the two (Step 8).
## Step 6: Write the main loop as a `GOTO` loop
```basic norun
LABEL TICK
GOSUB READKEY
IF STATE# <> 0 THEN GOTO DISPATCH
IF PAUSED# = 1 THEN GOTO TICKEND
GOSUB MOVEPAD
IF HELD# = 1 THEN GOSUB HOLDBAL
IF HELD# = 0 THEN GOSUB MOVEBAL
GOSUB SHOWSPR
LABEL TICKEND
SLEEP 0.02
GOTO TICK
LABEL DISPATCH
IF STATE# = 1 THEN GOTO LOSTLIF
IF STATE# = 2 THEN GOTO LEVELUP
IF STATE# = 3 THEN GOTO BYE
IF STATE# = 5 THEN GOTO NEWGAME
STATE# = 0
GOTO TICK
```
**`DO ... LOOP` would be the natural way to write this and it is the wrong one.** Every
loop and every `GOSUB` pushes one of 32 scopes, and a state change that jumped out of a
`DO` — losing a life, clearing a level — would leak its scope every single time. Thirty-two
lives later the program stops. A `GOTO` loop pushes nothing at all, so the deepest chain
in play is the six `GOSUB`s of a brick hit.
Nothing here `GOSUB`s into the state changes either. They are branch targets that run and
then `GOTO TICK`, so the stack is empty again by the time the next frame starts.
`SLEEP 0.02` is the frame pacing, and **it does not block**: it records a deadline and
the step loop declines to advance the program until the clock reaches it, so the host
goes on drawing frames and pumping the keyboard the whole time. Fifty ticks a second is
what that asks for. Chapter 18 needs a much sharper instrument than this one and builds
it out of `TI#`.
## Step 7: Read the keyboard through a countdown
`GET` is the only non-blocking read there is. There is **no key-up event and no held-key
state** — what you get instead is the host's own key repeat arriving as ordinary key-down
events, about one a tick while a key is held down.
So drain the queue every tick, and let a keystroke refill a countdown:
```basic norun
LABEL READKEY
FOR KI# = 1 TO 8
GET K#
IF K# <> 0 THEN GOSUB HANDKEY
NEXT KI#
RETURN
LABEL HANDKEY
IF K# = 1073741904 THEN PDIR# = 0 - 1
IF K# = 1073741904 THEN PDEC# = 12
IF K# = 1073741903 THEN PDIR# = 1
IF K# = 1073741903 THEN PDEC# = 12
IF K# = 32 THEN GOSUB KEYSPC
IF K# = 112 THEN GOSUB KEYPAU
IF K# = 113 THEN STATE# = 3
IF K# = 27 THEN STATE# = 3
RETURN
```
```basic norun
LABEL MOVEPAD
IF PDEC# < 1 THEN GOTO PADCLMP
PDEC# = PDEC# - 1
PX# = PX# + (PDIR# * PSPD#)
LABEL PADCLMP
IF PX# < 0 THEN PX# = 0
M# = SCW# - PW#
IF PX# > M# THEN PX# = M#
RETURN
```
The paddle coasts to a stop over twelve ticks when the repeats stop, so **held reads as
held and tapped reads as a nudge** — with no held-key state anywhere. 1073741903 and
1073741904 are the right and left arrows; the codes are the host's, and Chapter 12's
`GET` entry says where they come from.
Note `0 - 1` rather than `-1`. Unary minus on a literal is fine, but the habit of writing
the subtraction out is worth forming here: this parser reads `a - b + c` as `a - (b + c)`,
so every mixed `+` and `-` in the game is parenthesised as a rule rather than where it
happens to matter. It also matches only **one** unparenthesised `AND` or `OR` per
expression. Both are in [Chapter 13](13-differences.md).
## Step 8: Move the ball one axis at a time
```basic norun
LABEL MOVEBAL
OX# = BX#
BX# = BX# + BVX#
IF BX# < 0 THEN GOSUB WALLL
IF BX# > MAXX# THEN GOSUB WALLR
GOSUB XBRICK
OY# = BY#
BY# = BY# + BVY#
IF BY# < TOPY# THEN GOSUB WALLT
GOSUB YBRICK
GOSUB PADHIT
IF BY# > LOSEY# THEN STATE# = 1
RETURN
```
X first and then Y, **each move tested on its own**, so a ball arriving at the corner of
a brick reverses one axis rather than both. `OX#` and `OY#` remember where it came from,
which is what a collision backs it out to.
The brick test converts a pixel to a cell, and it is the only place in the program that
does:
```basic norun
LABEL HITTEST
HIT# = 0
RC# = TY# / CH#
IF RC# < BTOP# THEN RETURN
RB# = RC# - BTOP#
IF RB# > (BROWS# - 1) THEN RETURN
CC2# = TX# / CW#
IF CC2# < BLEFT# THEN RETURN
CB# = (CC2# - BLEFT#) / BRW#
IF CB# > (BCOLS# - 1) THEN RETURN
BI# = (RB# * BCOLS#) + CB#
BR2# = RB#
IF BR#(BI#) = 1 THEN HIT# = 1
RETURN
```
Callers hand it a point in `TX#`/`TY#` and read `HIT#`, `BI#` and `BR2#` back out — which
works only because Step 3 created all three outside the routine.
**Test the ball's leading edge, not its centre:**
```basic norun
LABEL XBRICK
TX# = BX#
IF BVX# > 0 THEN TX# = BX# + 7
TY# = BY# + 4
GOSUB HITTEST
IF HIT# = 0 THEN RETURN
BX# = OX#
BVX# = 0 - BVX#
GOSUB KILLBR
RETURN
```
A ball tested by its centre sinks four pixels into a brick before it turns, and it looks
like the brick was hit late. The cost of the leading edge is that a brick clipped at the
very corner can be missed by up to seven pixels' worth of ball; that is the better trade
of the two, and it is worth knowing which one you took.
## Step 9: Make the paddle bounce mean something
```basic norun
LABEL PADHIT
IF BVY# < 1 THEN RETURN
BB# = BY# + 8
IF BB# < PY# THEN RETURN
IF BY# > (PY# + 10) THEN RETURN
RX# = PX# + PW#
IF (BX# + 8) < PX# THEN RETURN
IF BX# > RX# THEN RETURN
BY# = PY# - 8
BVY# = 0 - BSPD#
Z# = ((BX# + 4) - PX#) / (PW# / 5)
IF Z# < 0 THEN Z# = 0
IF Z# > 4 THEN Z# = 4
PVX# = BVX#
BVX# = (Z# - 2) * 3
IF BVX# <> 0 THEN GOTO PADAIM
BVX# = 2
IF PVX# < 0 THEN BVX# = 0 - 2
LABEL PADAIM
GOSUB BEEPP
RETURN
```
Five zones across the face and the zone decides the angle: `Z#` is 0 to 4, so the new
horizontal speed is -6, -3, 0, +3, +6. Where you catch it decides where it goes, which is
the entire game.
The dead-centre case needs its own handling. A ball with **no** sideways speed rises and
falls down one column for ever, which is how attract mode used to hang itself — so the
middle zone keeps the direction the ball came in on and gives it a shallow angle instead
of zero.
There is a watchdog for the same failure in a subtler form. Ten seconds without touching
a brick means the ball has found an orbit that misses the wall, so it gets a new angle —
**at the next paddle bounce, never in mid-air.** Changing it in flight looks exactly like
the ball hitting something that is not there:
```basic norun
LABEL UNSTICK
NUDGE# = 0
STALL# = 0
RMAX# = 4
GOSUB RANDOM
BVX# = (RND# * 3) - 6
IF BVX# = 0 THEN BVX# = 3
RETURN
```
`RANDOM` is a linear congruential generator, because **this dialect has no `RND`** — and
no `INT`, `SQR`, `ASC` or `TIMER` either. `TI#` is jiffies off the host's clock and is
uptime rather than zero-based, which makes it a fine seed:
```basic norun
LABEL RANDOM
SEED# = MOD(((SEED# * 1103515245) + 12345), 2147483648)
RND# = MOD((SEED# / 65536), RMAX#)
RETURN
```
The multiply stays inside a 64-bit integer for any seed under 2^31, and the answer comes
from the middle bits because the low bits of a power-of-two modulus barely move.
Integer division truncates for free, which is the `INT` you do not have.
## Step 10: The HUD and the messages
Same rule as the wall: build the whole line, write it once.
```basic
V# = 0
P$ = ""
H$ = ""
SCORE# = 1250
LIVES# = 3
LEVEL# = 2
HIGH# = 9900
V# = SCORE#
GOSUB PAD6
H$ = " SCORE " + P$
H$ = (H$ + " LIVES ") + LIVES#
H$ = (H$ + " LEVEL ") + LEVEL#
V# = HIGH#
GOSUB PAD6
H$ = (H$ + " HIGH ") + P$
PRINT H$
END
LABEL PAD6
P$ = "" + V#
DO WHILE LEN(P$) < 6
P$ = "0" + P$
LOOP
RETURN
```
```output
SCORE 001250 LIVES 3 LEVEL 2 HIGH 009900
```
`"" + V#` is how a number becomes a string: `+` concatenates a string with a number, and
that is the conversion — see [Chapter 5](05-strings-and-formatting.md). Every `+` after
the first is parenthesised, for the reason in Step 7.
A centred message is the same idea, and needs no padding on the right because `CHAR`
terminating the row is what erases the rest of it:
```basic norun
LABEL SHOWAT
L# = LEN(MSG$)
C2# = (COLS# - L#) / 2
S$ = MSG$
IF C2# > 0 THEN S$ = (" " * C2#) + MSG$
CHAR 1, 0, MROW#, S$
RETURN
LABEL CLRAT
CHAR 1, 0, MROW#, " "
RETURN
```
Clearing a message is a single space written at column 0 — one character, and the
terminator behind it takes the rest of the row with it.
## Step 11: Ask for sound once, then believe the answer
`SOUND` refuses on a machine with no audio device, and an untrapped refusal ends the
game. Ask once at startup, record the answer, and never ask again:
```basic requires=noakgl
SND# = 1
TRAP NOAUDIO
SOUND 1, 2000, 1
TRAP
IF SND# = 1 THEN PRINT "SOUND IS AVAILABLE"
IF SND# = 0 THEN PRINT "NO AUDIO DEVICE, PLAYING SILENTLY"
END
LABEL NOAUDIO
SND# = 0
RESUME NEXT
```
```output
NO AUDIO DEVICE, PLAYING SILENTLY
```
That output is the stdio build, which has no devices at all. On the SDL build the same
program prints the other line. `TRAP` with no argument disarms the handler again — leave
it armed and the next refusal anywhere in the game is silently swallowed. See
[Chapter 4](04-control-flow.md#trapping-errors).
After that every sound is guarded by the flag it recorded:
```basic norun
LABEL BEEPB
IF SND# = 1 THEN SOUND 1, 12000, 2
RETURN
```
**`SOUND`'s frequency argument is a SID register value, not hertz** — the pitch is
`register * 1022730 / 16777216`, and [Chapter 7](07-sound.md#sound) has the arithmetic.
Work the notes out once and write them down in a comment beside the numbers.
## Step 12: Give it an attract mode, and test with it
The title screen plays itself, and this is not decoration — it is how the game gets
tested without a hand on the keyboard. `DEMO#` is the only difference between the demo
and a real game:
```basic norun
LABEL DEMOPAD
TX# = (BX# + 4) - (PW# / 2)
TX# = TX# + DOFF#
D# = TX# - PX#
IF D# > PSPD# THEN D# = PSPD#
M# = 0 - PSPD#
IF D# < M# THEN D# = M#
PX# = PX# + D#
RETURN
LABEL DEMOAIM
RMAX# = 81
GOSUB RANDOM
DOFF# = RND# - 40
RETURN
```
The demo paddle tracks the ball but **aims off-centre by a random amount**, refreshed at
every bounce, so it plays like something with a hand on it and occasionally misses. A
demo that tracks perfectly never loses a ball and therefore never tests losing one.
Leave it running for two minutes and watch the score climb: that exercises the ball, the
bricks, the level change, the speed-up and — most usefully — the value pool from Step 3,
which is the failure that only shows up after thousands of ticks.
The machine does not get on the scoreboard. Attract mode keeps score so the wall behaves,
but only a player's score is ever the high one:
```basic norun
LABEL HISCORE
IF SCORE# > HIGH# THEN HIGH# = SCORE#
RETURN
```
There is nowhere to save it. There is no disk in this interpreter — see
[Chapter 9](09-files-and-disk.md) — so the high score lasts as long as the process does.
## The rules this listing never breaks
Every one of these was learned by breaking it. They are the checklist to write your own
game against:
| Rule | Because | Where |
|---|---|---|
| Create every name at the top, loop counters included | A name created inside a scope costs pool slots for good; 4096 ends the run | Step 3 |
| Build a text row whole and write it from column 0 | `CHAR` terminates the row where it stops | Step 4 |
| Tell rows apart by shape, not colour | `CHAR` ignores its colour argument | Step 4 |
| Compute a `MOVSPR` coordinate into a variable first | A leading sign means *move by*, not *move to* | Step 5 |
| Loop the game with `GOTO`, not `DO` | 32 scopes, and jumping out of a loop leaks one | Step 6 |
| Parenthesise every mixed `+` and `-` | `a - b + c` parses as `a - (b + c)` | Step 7 |
| One `AND` or `OR` per expression unless parenthesised | The parser matches one | Step 7 |
| Keep a line under 32 tokens | The 33rd kills the interpreter with a stack trace, not a BASIC error you can `TRAP` | — |
| Span a loop across lines, and never `FOR I = 1 TO 1` | A whole loop on one line does not loop; equal bounds run zero times | [Ch 13](13-differences.md) |
| Probe for a device once and remember | An untrapped refusal ends the program | Step 11 |
## Where to go next
- **[Chapter 18](18-tutorial-breakout-artwork.md)** builds Breakout again with sprite
artwork, powerups and a coloured HUD. Every architectural decision comes out different,
and the chapter is about why.
- **[Chapter 13](13-differences.md)** is the full list of what this dialect does
differently from BASIC 7.0.
- **[Chapter 14](14-architecture.md)** is the interpreter itself — the step loop, the
pools and how to debug a program that stops for no visible reason.
- `TODO.md` §6 and §9 carry the defects this game found, each with a reduction that fits
on a screen and what a fix would touch.

View File

@@ -0,0 +1,796 @@
# 18. Tutorial: Breakout with artwork
[Chapter 17](17-tutorial-breakout.md) built Breakout out of text and two `DATA` sprites.
This chapter builds it again out of **downloaded artwork**, with powerups, a coloured HUD
and three voices of sound — and almost nothing about the shape of the program survives the
change. The finished listing is
[`examples/breakout/sprites/breakout.bas`](../examples/breakout/sprites/breakout.bas).
Read Chapter 17 first if you have not. The rules it teaches — declare every name up front,
loop with `GOTO`, parenthesise mixed `+` and `-` — all still apply here and are not
repeated.
```sh norun
$ ./build-akgl/basic examples/breakout/sprites/breakout.bas
```
| Key | Does |
|---|---|
| left / right | move the paddle |
| space | start a game, launch the ball, release a stuck ball |
| P | pause |
| S | sound on and off |
| Q or escape | quit |
A broken brick drops a gem about one time in seven. Catch it with the paddle; the colour
tells you which it is.
| Gem | Name | Does | For |
|---|---|---|---|
| red | EXPAND | doubles the paddle's width | 20 seconds |
| yellow | MULTI | throws two more balls off the one in play | until they are lost |
| green | SLOW | drops the ball's speed to about two thirds | 16 seconds |
| blue | STICKY | the ball sticks where it lands; space fires it | 18 seconds |
| purple | CATCH | a second bar appears higher up the field | 24 seconds |
---
## Step 1: Put the artwork on the screen
`SPRSAV` loads an image file straight into a sprite slot, and a sprite loaded that way
keeps **the image's own size** rather than being forced to 24 by 21 — see
[Chapter 8](08-sprites.md#from-an-image-file).
```basic requires=akgl setup=breakout_art screenshot=breakout-artwork
I# = 0
SPRSAV "art/paddleBlu.png", 3
SPRSAV "art/paddleRed.png", 4
SPRSAV "art/ballBlue.png", 5
SPRSAV "art/element_red_polygon_glossy.png", 6
SPRSAV "art/element_green_polygon_glossy.png", 7
SPRSAV "art/element_purple_polygon_glossy.png", 8
FOR I# = 3 TO 8
SPRITE I#, 1, 2
NEXT I#
MOVSPR 3, 20, 20
MOVSPR 4, 20, 60
MOVSPR 5, 160, 30
MOVSPR 6, 30, 120
MOVSPR 7, 130, 120
MOVSPR 8, 230, 120
```
![](images/breakout-artwork.png)
That is the whole game's cast: two bars, a ball and five gems. The path is tried against
the working directory first and then against the directory the program was loaded from,
so a `.bas` stored beside its `art/` runs from anywhere.
The artwork here is Kenney's [Puzzle Pack 1](https://kenney.nl/assets/puzzle-pack-1),
released under [CC0](http://creativecommons.org/publicdomain/zero/1.0/);
`examples/breakout/sprites/art/PROVENANCE.md` records which file is used for what.
Crediting Kenney is not required by CC0 — do it anyway.
**`SPRITE n, 1, 2` turns a sprite on in colour 2.** A sprite's colour *multiplies* the
artwork rather than replacing it, so colour 2 (white) is what leaves the artwork looking
like itself.
## Step 2: Budget the eight sprite slots before you write anything else
There are eight sprites. That is not a limit you will design your way around, so decide
what they are first:
| Slot | Is |
|---|---|
| 1 | the HUD strip — a captured drawing |
| 2 | the playing field — a captured drawing |
| 3 | the paddle |
| 4 | the catcher bar (the purple gem) |
| 5, 6, 7 | up to three balls |
| 8 | the falling gem |
Two of the eight are **the screen**, and Step 3 is why. That leaves six for everything
else, which is the reason **the bricks are drawn rather than made of artwork** — sixty of
them will not fit in six slots, and there is no way to get artwork onto the screen other
than a sprite. `GSHAPE` cannot stamp a sprite and `SPRSAV` cannot read one back out.
It is also the reason for "one gem at a time": there is one slot for it, so a brick
broken while a gem is falling drops nothing.
## Step 3: Turn what you drew into a sprite
In the standalone SDL build the text layer repaints every row of the window, opaque,
after your program's steps have run and before the frame is presented — so **anything
`DRAW`, `BOX` or `CIRCLE` puts on the screen is painted over before anybody sees it**.
Sprites are drawn after the text layer. A sprite is the only thing on the screen a
program can rely on being visible. (`TODO.md` §9 item 3; the figures in this chapter are
rendered by a tool that omits the text layer, which is why they can show a drawing at
all.)
That leaves exactly one way to put a picture up: **draw it, capture it with `SSHAPE`,
install the capture with `SPRSAV`.**
```basic norun
SSHAPE Z$, 0, 60, 800, 600
SPRSAV Z$, 2
SPRITE 2, 1, 2
MOVSPR 2, 0, 60
```
Four lines, and they are the last four of every draw routine in the game. `Z$` holds a
handle rather than pixels — see [Chapter 6](06-graphics.md#saving-and-stamping-regions) —
which is all `SPRSAV` needs.
### Stamp the bricks; do not paint them
Draw one brick per colour, capture the six of them, and stamp them with `GSHAPE`:
```basic requires=akgl screenshot=breakout-stamps size=100x130
DIM BRC#(6)
I# = 0
R# = 0
K# = 0
T1# = 0
T2# = 0
FOR I# = 0 TO 5
READ BRC#(I#)
NEXT I#
GRAPHIC 1, 1
WIDTH 1
FOR R# = 0 TO 5
COLOR 1, BRC#(R#)
T1# = R# * 20
T2# = T1# + 8
FOR K# = 0 TO 7
DRAW 1, 0, T1# + K# TO 67, T1# + K# : DRAW 1, 0, T2# + K# TO 67, T2# + K#
NEXT K#
NEXT R#
DATA 3, 9, 8, 6, 4, 5
```
![](images/breakout-stamps.png)
Six 68 by 16 bricks, filled **two scan lines at a time** so the set costs about a hundred
and thirty lines rather than the two hundred and seventy a line-at-a-time loop would take.
Step 4 explains why that number matters.
**`PAINT` would be one statement instead of sixteen and is not an option.** It costs
nearly four milliseconds a call; sixty of those is seven frames.
`SSHAPE` has sixteen slots, nothing gives one back, and `GRAPHIC 5` gives back all of
them at once. So the game counts what it has spent and rebuilds the stamps from scratch
whenever the pool runs dry:
```basic norun
LABEL DRAWJOB
IF SHN# < 14 THEN GOTO DRAWJOB2
GOSUB DRAWPROTOS
RETURN
```
### Flatten the field before you draw it
The draw routine should not be deciding anything. When a brick breaks, walk the grid and
write out a **list of the bricks still standing**, row by row — so a row is a contiguous
run of that list, and drawing it is a stamp and an advance:
```basic norun
LABEL BUILDLIVE
LN# = 0
FOR R# = 0 TO 5
RS#(R#) = LN#
RC#(R#) = 0
GOSUB BUILDROW
NEXT R#
RETURN
LABEL BUILDROW
FOR C# = 0 TO 9
IF BRK#(R# * 10 + C#) > 0 THEN BEGIN
LX#(LN#) = BRKX# + C# * 72
LY#(LN#) = BRKY# + R# * 24
LN# = LN# + 1
RC#(R#) = RC#(R#) + 1
BEND
NEXT C#
RETURN
```
`RS#(R#)` is where row `R#`'s run starts and `RC#(R#)` is how long it is. This costs about
four lines a brick and has all the time in the world; the draw costs two and has a
deadline. **That trade is the spine of this program** and it comes back in Step 6 for the
lettering.
Now the whole screen, drawn and captured and dressed with artwork:
```basic requires=akgl setup=breakout_art screenshot=breakout-screen size=800x600
DIM BRC#(6)
I# = 0
R# = 0
C# = 0
K# = 0
T1# = 0
T2# = 0
Z$ = ""
FOR I# = 0 TO 5
READ BRC#(I#)
NEXT I#
GRAPHIC 1, 1
WIDTH 1
FOR R# = 0 TO 5
COLOR 1, BRC#(R#)
T1# = R# * 20
T2# = T1# + 8
FOR K# = 0 TO 7
DRAW 1, 0, T1# + K# TO 67, T1# + K# : DRAW 1, 0, T2# + K# TO 67, T2# + K#
NEXT K#
NEXT R#
SSHAPE Z$, 0, 0, 68, 16 : S0$ = Z$
SSHAPE Z$, 0, 20, 68, 36 : S1$ = Z$
SSHAPE Z$, 0, 40, 68, 56 : S2$ = Z$
SSHAPE Z$, 0, 60, 68, 76 : S3$ = Z$
SSHAPE Z$, 0, 80, 68, 96 : S4$ = Z$
SSHAPE Z$, 0, 100, 68, 116 : S5$ = Z$
GRAPHIC 1, 1
WIDTH 2
COLOR 5, 16 : COLOR 1, 4
BOX 5, 2, 62, 797, 597
BOX 1, 6, 66, 793, 593
FOR C# = 0 TO 9
Z$ = S0$ : GSHAPE Z$, 42 + C# * 72, 108
Z$ = S1$ : GSHAPE Z$, 42 + C# * 72, 132
Z$ = S2$ : GSHAPE Z$, 42 + C# * 72, 156
Z$ = S3$ : GSHAPE Z$, 42 + C# * 72, 180
Z$ = S4$ : GSHAPE Z$, 42 + C# * 72, 204
Z$ = S5$ : GSHAPE Z$, 42 + C# * 72, 228
NEXT C#
SSHAPE Z$, 0, 60, 800, 600
SPRSAV Z$, 2
SPRITE 2, 1, 2
MOVSPR 2, 0, 60
SPRSAV "art/paddleBlu.png", 3
SPRSAV "art/ballBlue.png", 5
SPRITE 3, 1, 2
SPRITE 5, 1, 2
MOVSPR 3, 348, 540
MOVSPR 5, 389, 517
DATA 3, 9, 8, 6, 4, 5
```
![](images/breakout-screen.png)
Everything above the last four lines is a drawing nobody would ever see. `SSHAPE` and
`SPRSAV` are what make it the screen.
The game keeps its six stamps in **six separate scalars** rather than an array, and that
is not a style choice — see the fourth trap at the end of this chapter.
## Step 4: Find the frame boundary
The host runs **256 source lines and then presents the frame**, and presenting throws the
drawing buffer away. So everything between a `GRAPHIC 1, 1` and its `SSHAPE` has to
happen inside one of those batches. Draw more than that and the capture comes back
holding only the tail of what you drew, over whatever the frame before it left behind —
which looks exactly like a ghost.
`TI#` is refreshed from the host's clock once per batch, so **the step on which `TI#`
changes is the first step of a batch**. Spinning until it changes is the only way a
program in this dialect can locate a frame boundary:
```basic norun
LABEL PACE
LASTT# = TI#
LABEL PACEEDGE
IF TI# - LASTT# < 2 THEN GOTO PACEEDGE
RETURN
```
Two jiffies is thirty frames a second. **`LASTT#` is sampled on entry rather than carried
over from the last frame, and that is the whole correctness of the routine.** Carried
over, a frame whose work ran long finds the time already spent, returns immediately from
somewhere in the middle of a batch, and the capture that follows is ruined. Sampling here
means the loop always sees `TI#` change under it, and a change is only ever seen on the
first step of a batch.
Measured on one machine: after a jiffy edge, 220 lines of drawing survive the capture
intact and 250 do not. Every draw routine in the game is written to stay near 200.
**Arithmetic is free.** A routine that computes for two thousand steps costs frame rate
and nothing else. Only drawing has a deadline — which is what makes Step 3's flattening
and Step 6's stroke lists worth their complexity.
## Step 5: Do at most one capture per frame
The frame loop paces first, then draws at most one thing, then plays the game:
```basic norun
LABEL FRAME
GOSUB PACE
GOSUB DRAWJOB
GOSUB READKEYS
IF STATE# = 0 THEN GOSUB TITLETICK
IF STATE# = 1 THEN GOSUB SERVETICK
IF STATE# = 2 THEN GOSUB PLAYTICK
IF STATE# = 3 THEN GOSUB LOSTTICK
IF STATE# = 4 THEN GOSUB CLEARTICK
IF RUNNING# = 0 THEN GOTO SHUTDOWN
GOTO FRAME
```
`DRAWJOB` is a queue of one, chosen by dirty flags — stamps first because everything else
draws with them, then the field, then the HUD:
```basic norun
LABEL DRAWJOB
IF SHN# < 14 THEN GOTO DRAWJOB2
GOSUB DRAWPROTOS
RETURN
LABEL DRAWJOB2
IF DPLAY# = 0 THEN GOTO DRAWJOB3
GOSUB DRAWPLAY
DPLAY# = 0
RETURN
LABEL DRAWJOB3
IF DHUD# = 0 THEN RETURN
GOSUB DRAWHUD
DHUD# = 0
RETURN
```
The game sets `DPLAY# = 1` or `DHUD# = 1` when something changes and never draws
directly. One consequence is visible and deliberate: **the score lags the bricks by one
frame**, because the field goes first. At thirty frames a second nobody can see it.
Those are `LABEL`s and `GOTO`s rather than `BEGIN` blocks, and again that is not a style
choice — a `RETURN` inside a block leaves the interpreter with no `GOSUB` to return from.
The last trap in this chapter is exactly that.
## Step 6: Draw the lettering, because text has no colour
The interpreter's text sink is white and has no verb that changes it; `CHAR` parses a
colour argument and ignores it. A coloured HUD therefore has to be *drawn*, which means
carrying a font.
The one here is four units wide and seven tall, one glyph per `DATA` line: how many
strokes, then that many pairs of points. **A point is coded `X * 10 + Y`**, so 0 is the
top-left corner, 30 the top right and 36 the bottom right.
```basic requires=akgl screenshot=breakout-lettering size=260x110
DIM FNC#(5)
DIM FNI#(5)
DIM FNS#(60)
I# = 0
K# = 0
N# = 0
D# = 0
GP# = 0
GX# = 0
GX2# = 0
P1# = 0
P2# = 0
X1# = 0
Y1# = 0
X2# = 0
Y2# = 0
FOR I# = 0 TO 4
READ N#
FNC#(I#) = N#
FNI#(I#) = GX#
GOSUB READGLYPH
NEXT I#
GRAPHIC 1, 1
WIDTH 2
COLOR 1, 8
SZ# = 9
FOR I# = 0 TO 4
GOSUB DRAWGLYPH
NEXT I#
END
LABEL READGLYPH
FOR K# = 1 TO N# * 2
READ D#
FNS#(GX#) = D#
GX# = GX# + 1
NEXT K#
RETURN
LABEL DRAWGLYPH
GP# = FNI#(I#)
GX2# = 20 + I# * SZ# * 5
FOR K# = 1 TO FNC#(I#)
P1# = FNS#(GP#)
P2# = FNS#(GP# + 1)
GP# = GP# + 2
X1# = GX2# + (P1# / 10) * SZ#
Y1# = 20 + MOD(P1#, 10) * SZ#
X2# = GX2# + (P2# / 10) * SZ#
Y2# = 20 + MOD(P2#, 10) * SZ#
DRAW 1, X1#, Y1# TO X2#, Y2#
NEXT K#
RETURN
REM S
DATA 5, 0,30, 0,3, 3,33, 33,36, 6,36
REM C
DATA 3, 0,30, 0,6, 6,36
REM O
DATA 4, 0,30, 6,36, 0,6, 30,36
REM R
DATA 5, 0,6, 0,30, 3,33, 30,33, 13,36
REM E
DATA 4, 0,6, 0,30, 3,33, 6,36
```
![](images/breakout-lettering.png)
`SZ#` is the scale, so the same table draws a 12-unit `BREAKOUT` on the title screen and
a 4-unit `SCORE` in the HUD. The game reads a character to a glyph number with
`INSTR(ALPHA$, MID(TX$, TXI#, 1))` over a 41-character alphabet, which is why the order of
the `DATA` lines matters.
**Building the strokes and drawing them are separate jobs, on different frames.** Turning
a string into strokes costs about sixteen lines a character and happens when a number
changes; the draw routine merely replays the list at two lines a stroke, and it is the
one with the deadline:
```basic norun
LABEL DRAWHUD
GRAPHIC 1, 1
WIDTH 1
COLOR 0, 1 : COLOR 1, 4 : COLOR 2, 8 : COLOR 3, 5
COLOR 4, 11 : COLOR 5, 16 : COLOR 6, 6
BOX 5, 0, 56, 799, 57
IF HN# = 1 THEN DRAW HC#(0), HX1#(0), HY1#(0) TO HX2#(0), HY2#(0)
IF HN# < 2 THEN GOTO HUDONE
FOR I# = 0 TO HN# - 1
DRAW HC#(I#), HX1#(I#), HY1#(I#) TO HX2#(I#), HY2#(I#)
NEXT I#
LABEL HUDONE
SSHAPE Z$, 0, 0, 800, 60
SPRSAV Z$, 1
SPRITE 1, 1, 2
MOVSPR 1, 0, 0
RETURN
```
Each stroke carries its own colour source, so one pass over the list draws in as many
colours as it likes: labels cyan, the score yellow, the lives green, the level red.
**Seven colour sources is the ceiling** — which is why the purple gem's banner is the
closest purple the palette has rather than the gem's own.
The one-stroke case is written out beside the loop. That is the second trap below, and
every loop over a list in this program has it.
## Step 7: Bounce the ball without a square root
There is no `SQR` in this dialect, so the game never computes a magnitude. Eight landing
zones across the bar, each holding **very nearly a unit vector**, and a velocity is
always one of them times the current speed:
```basic norun
DATA -0.85, -0.62, -0.40, -0.18, 0.18, 0.40, 0.62, 0.85
DATA -0.53, -0.78, -0.92, -0.98, -0.98, -0.92, -0.78, -0.53
```
```basic norun
LABEL HITBAR
IF BLY%(B#) + 21 < T2# THEN RETURN
IF BLY%(B#) > T2# + 23 THEN RETURN
IF BLX%(B#) + 21 < T1# THEN RETURN
IF BLX%(B#) > T1# + T3# THEN RETURN
T4# = (BLX%(B#) + 11 - T1#) * 8 / T3#
IF T4# < 0 THEN T4# = 0
IF T4# > 7 THEN T4# = 7
BLVX%(B#) = ZVX%(T4#) * SPD%
BLVY%(B#) = ZVY%(T4#) * SPD%
BLY%(B#) = T2# - 23
RETURN
```
A bounce off a wall or a brick only ever flips a sign, so a ball is always travelling at
exactly the `SPD%` that was in force when it last left a bar. That makes the SLOW gem a
**ratio of two speeds** rather than a change of magnitude:
```basic norun
LABEL RESCALE
IF BSPD% < 0.1 THEN BSPD% = SPD%
RAT% = SPD% / BSPD%
BSPD% = SPD%
FOR B# = 0 TO 2
IF BLON#(B#) = 1 THEN BEGIN
BLVX%(B#) = BLVX%(B#) * RAT%
BLVY%(B#) = BLVY%(B#) * RAT%
BEND
NEXT B#
RETURN
```
`RAT%` is a **float** variable on purpose: an integer one holds the 0.75 of a slowdown as
0 and stops the ball dead. That is the first trap below, and it is the expensive one.
Scaling by the vertical component instead — which is what this routine did first — is not
a slowdown at all. A shallow ball's small vertical gets stretched up to the new speed and
drags the large horizontal with it, so SLOW made the ball *faster*. Sixty degrees of the
eight zones are shallow enough to do it.
Brick collision reflects off whichever face the ball has less of itself past, which is the
standard box resolution and the reason a ball clipping the end of a row goes sideways
instead of straight back down:
```basic norun
T1# = RGT# : IF BX1# + 67 < T1# THEN T1# = BX1# + 67
T2# = LFT# : IF BX1# > T2# THEN T2# = BX1#
T3# = BOT# : IF BY1# + 15 < T3# THEN T3# = BY1# + 15
T4# = TOP# : IF BY1# > T4# THEN T4# = BY1#
IF T1# - T2# < T3# - T4# THEN BLVX%(B#) = 0.0 - BLVX%(B#)
IF T1# - T2# >= T3# - T4# THEN BLVY%(B#) = 0.0 - BLVY%(B#)
```
`T1#` to `T4#` are the overlapping rectangle; its width against its height is the whole
test. A ball can only be over four cells at once, so the cells are worked out from its box
rather than by walking sixty bricks.
## Step 8: Gems and powerups
A gem is one sprite, one type number and two timers. The type picks the artwork, the
banner colour and what `TAKEGEM` does:
```basic norun
LABEL SPAWNGEM
RNMAX# = 5
GOSUB NEXTRAND
GMTYP# = RNVAL# + 1
GMX% = BX1# + 10
GMY% = BY1#
IF GMTYP# = 1 THEN SPRSAV "art/element_red_polygon_glossy.png", 8
IF GMTYP# = 2 THEN SPRSAV "art/element_yellow_polygon_glossy.png", 8
IF GMTYP# = 3 THEN SPRSAV "art/element_green_polygon_glossy.png", 8
IF GMTYP# = 4 THEN SPRSAV "art/element_blue_polygon_glossy.png", 8
IF GMTYP# = 5 THEN SPRSAV "art/element_purple_polygon_glossy.png", 8
GMON# = 1
SPRITE 8, 1, 2
MOVSPR 8, GMX%, GMY%
RETURN
```
**Reloading slot 8 is how one sprite becomes five gems.** `SPRSAV` over a live slot
replaces the artwork; there is no need for a slot per gem, and there was never a slot to
spare.
Every timer is a frame count decremented in one place, so an expiry is where the effect is
undone:
```basic norun
IF PDEXP# > 0 THEN BEGIN
PDEXP# = PDEXP# - 1
IF PDEXP# = 0 THEN BEGIN
PDW# = 104
SPRITE 3, 1, 2, 0, 0, 0
BEND
BEND
```
EXPAND is `SPRITE 3, 1, 2, 0, 1, 0` — the x-expand bit, which is the only scaling a sprite
has, and doubling the artwork is exactly what it wants.
The CATCH bar **mirrors** the paddle rather than following it:
```basic norun
CTX% = 0.0 - PDX% + WALLL# + WALLR# - 104
```
That is deliberate. A second bar directly above the first is worth nothing; a mirrored one
turns a dive across the field into a save at both ends. Note the leading `0.0` — trap one
again, and this line was wrong before it was right.
## Step 9: Three voices, and a mute that costs nothing
Voice 1 is the ball hitting things, voice 2 is the gem and the ball being lost, and voice
3 is reserved for `PLAY` so a brick going cannot cut a tune off mid-note.
**`SOUND`'s frequency argument is a SID register value, not hertz.** The pitch is
`register * 1022730 / 16777216` — see [Chapter 7](07-sound.md#sound) — so work the notes
out once and write them down: 17175 is C6, 8579 C5, 4298 C4, 3609 A3. The brick tone is
pitched by the row it came from —
```basic norun
SOUND 1, 17175 - R# * 2100, 4
```
— so the top row rings at C6, each row down drops about a third, and a wall coming apart
plays itself down a scale.
Mute with `VOL 0` rather than a flag tested at eleven call sites:
```basic norun
LABEL PRESSMUTE
SNDON# = 1 - SNDON#
IF SNDON# = 1 THEN VOL 8
IF SNDON# = 0 THEN VOL 0
RETURN
```
A silenced voice costs nothing to issue. One line beats eleven scattered through the game.
## Five things in this dialect that do not do what they look like
Each of these cost an evening. All five are filed in `TODO.md` §9 with a reduction and
the file and line of the cause.
### 1. The left operand decides integer or float arithmetic
```basic
LEVEL# = 4
SPD% = 5.6 + LEVEL# * 0.45
PRINT "INTEGER FIRST " + SPD%
SPD% = 5.6 + 0.45 * LEVEL#
PRINT "FLOAT FIRST " + SPD%
V% = 6.4
PRINT "0 - V% " + (0 - V%)
PRINT "0.0 - V% " + (0.0 - V%)
FOR I# = 0 TO 0
PRINT "THE BODY RAN"
NEXT I#
PRINT "AFTER THE LOOP"
```
```output
INTEGER FIRST 5.600000
FLOAT FIRST 7.400000
0 - V% -6
0.0 - V% -6.400000
AFTER THE LOOP
```
**This is the dangerous one, because nothing fails.** The program computes something else
and carries on. Two live bugs in this game came from it: `SPD% = 5.6 + LEVEL# * 0.45` was
a flat 5.6, so no level ever got faster than level one; and `0 - BLVX%(B#)`, the obvious
way to reverse a ball, quantised its velocity to whole pixels on every bounce and bled
speed out of it. Neither produced a diagnostic.
**Put the float on the left, and put the answer somewhere with a `%` on it.** A float
expression landing in a `#` variable truncates.
### 2. A `FOR` whose bounds are equal does not run its body
The last two lines of that output are the second trap: `FOR I# = 0 TO 0` runs zero times.
Knowing it and remembering it while writing a loop over "the bricks still standing" are
different things, and the last brick of a row is exactly that case — which is why every
loop over a list in this program has its one-item case written out beside it:
```basic norun
LABEL STAMPROW
IF T2# < 1 THEN RETURN
IF T2# = 1 THEN GSHAPE Z$, LX#(T1#), LY#(T1#)
IF T2# < 2 THEN RETURN
FOR I# = T1# TO T1# + T2# - 1
GSHAPE Z$, LX#(I#), LY#(I#)
NEXT I#
RETURN
```
### 3. A skipped `BEGIN` block containing a loop breaks the enclosing `RETURN`
```basic
T# = 0
GOSUB DOIT
PRINT "CAME BACK"
END
LABEL DOIT
IF 1 = 0 THEN BEGIN
FOR I# = 0 TO 2
T# = T# + 1
NEXT I#
BEND
RETURN
```
```output
? 11 : RUNTIME ERROR RETURN outside the context of GOSUB
```
The routine plainly *was* called by a `GOSUB`. `FOR` creates its environment when the
line is **parsed**, and the block skip is decided when it is **evaluated**, so a skipped
loop pushes a scope that its skipped `NEXT` never pops — and the orphan sits between the
routine and its caller. Outside a routine the same thing exhausts the pool of 32 after
thirty-two skips instead.
Guard loops with `GOTO` rather than wrapping them in a block, which is what every draw
routine in this game does.
### 4. `SSHAPE` and `GSHAPE` ignore the subscript on a string array
```basic requires=akgl
DIM SH$(4)
SSHAPE SH$(2), 0, 0, 61, 4
PRINT "[" + SH$(0) + "] [" + SH$(2) + "]"
```
```output
[SHAPE:0] []
```
The handle lands in element zero whatever subscript you write, and `GSHAPE SH$(2)` then
stamps whatever is in `SH$(0)`. Ordinary assignment and `PRINT` honour the subscript, so
this is the two verbs reading their leaf directly rather than evaluating it. The symptom
is that every brick comes out the colour of the last stamp captured.
**Keep saved shapes in separate scalars.** The six brick stamps are `S0$` to `S5$` for
this reason, and the field is drawn as six runs of one colour rather than brick by brick
— which turned out to be cheaper anyway.
### 5. `READ` walks one cursor through every `DATA` item in the file
```basic
DIM T#(3)
DIM F#(3)
I# = 0
GOSUB LOADFONT
GOSUB LOADTABLE
PRINT "TABLE " + T#(0) + " " + T#(1) + " " + T#(2)
PRINT "FONT " + F#(0) + " " + F#(1) + " " + F#(2)
END
LABEL LOADTABLE
FOR I# = 0 TO 2
READ T#(I#)
NEXT I#
RETURN
LABEL LOADFONT
FOR I# = 0 TO 2
READ F#(I#)
NEXT I#
RETURN
DATA 11, 12, 13
DATA 21, 22, 23
```
```output
TABLE 21 22 23
FONT 11 12 13
```
The `DATA` written first went to whichever routine ran first, not to the one it was
written for. Two loaders means the one whose `DATA` comes first in the file has to be
called first — obvious in hindsight, and not obvious when the symptom is a font table
full of brick colours. `RESTORE` to a label is the way out when the order cannot be
arranged; Chapter 17 uses it to pick a level layout.
## The budgets
This program sits close to four ceilings at once, and knowing where they are is what
stops a feature costing an afternoon before it is abandoned:
| Resource | There are | This game uses |
|---|---|---|
| Sprites | 8 | 8 |
| Variables | 128 | 121, plus 4 the interpreter makes |
| Labels | 64 | 61 |
| `SSHAPE` slots | 16, none reclaimed except by `GRAPHIC 5` | 6 stamps plus 1 capture a frame |
| Value-pool slots | 4096, none reclaimed | none after startup — see [Chapter 17](17-tutorial-breakout.md#step-3-create-every-name-before-the-game-starts) |
| Scopes | 32 | 6 deep at most |
| Tokens on a line | 32, and the 33rd kills the interpreter rather than raising | short lines, temporaries instead of long conditions |
That is also why several things are **not** in the game, and they are worth naming
honestly rather than leaving to be discovered: no music under the play, only event sounds
and two four-note stings; one gem at a time; sticky and multiball share one offset, so two
balls stuck to the paddle sit on top of each other; and no high score on disk, because
there is no disk.
## Where to go next
- **[Chapter 8](08-sprites.md)** is the sprite reference: every form of `SPRSAV`, the
`MOVSPR` forms, collision and what `RSPPOS` reads back.
- **[Chapter 6](06-graphics.md)** is the drawing reference, including `SSHAPE`, `GSHAPE`
and what a shape handle is.
- **[Chapter 7](07-sound.md)** is `SOUND`, `PLAY`, `ENVELOPE` and `VOL`.
- **[Chapter 14](14-architecture.md)** explains the step loop and the pools this chapter
keeps running into, from the interpreter's side.
- `TODO.md` §9 is the eight defects this game found, each with a reduction, the cause and
what a fix would touch.

View File

@@ -13,6 +13,10 @@ everything that behaves differently here and why. **[Chapter 14](14-architecture
the odd one out: it is about the interpreter rather than the language, for anyone
embedding it, debugging it or changing it.
**[Chapters 17](17-tutorial-breakout.md)** and **[18](18-tutorial-breakout-artwork.md)**
are tutorials rather than reference: they build one complete game twice, two different
ways, and are where the rules that a real program runs into are collected.
## Chapters
| | |
@@ -33,6 +37,8 @@ embedding it, debugging it or changing it.
| **[14. Architecture](14-architecture.md)** | How the interpreter is put together, how to debug it, how to change it |
| **[15. Error codes](15-error-codes.md)** | Appendix: every value `ER#` can hold and every error line the interpreter prints |
| **[16. Structures](16-structures.md)** | `TYPE`, records, strict pointers, and sharing a C struct with an embedding host |
| **[17. Tutorial: Breakout](17-tutorial-breakout.md)** | Build a whole game out of the text grid and two `DATA` sprites, a step at a time |
| **[18. Tutorial: Breakout with artwork](18-tutorial-breakout-artwork.md)** | Build it again out of loaded artwork, powerups and a drawn HUD |
## The shortest possible start

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 B