Files
akbasic/docs/18-tutorial-breakout-artwork.md
Andrew Kesterson 05f241aca1 Stop a scalar created inside a scope costing value-pool slots
A scalar now lives in the variable record (`akbasic_Variable::inlinevalue`)
rather than drawing from the value pool, so a `GOSUB` local, a `FOR` counter and
a `DEF` parameter cost nothing at all.

The pool is a bump allocator with no free, and its comment justified that with
"nothing in BASIC destroys a variable". Scope exit does: it marks the variable
slot unused, `new_variable()` memsets the slot it hands back -- clearing
`values` -- and `variable_init()` therefore took *fresh* slots for a variable
whose old ones were still counted. Every scope that created a local leaked, with
no diagnostic until the pool ran dry on whichever line happened to be unlucky.

Six thousand `GOSUB`s creating one local used to die on the 4091st at `LOC# = 1`
with "Array of 1 elements does not fit in the 0 remaining value slots". They now
run. A `DEF` called eight thousand times used to die between the four and five
thousandth -- the leaking slot was the call scope's parameter, which is a scalar
-- and both forms now run. A game creating one name per tick was dead in half a
minute; the Breakout in examples/ was, after twenty-five seconds.

**A `@` name is the one exclusion, and it is the whole of it.** A structure or a
pointer to one keeps pool storage, because a pointer into a record outlives the
scope that DIMmed it -- docs/16-structures.md says nothing is reclaimed and
`prev_environment()` relies on it. The name suffix is the right test rather than
`structtype`, which the DIM path sets *after* calling `variable_init()`. A local
array therefore still leaks, deliberately, and is now the narrow rule the
tutorial teaches.

`SWAP` needed the other half: it copies whole variable records, so the `values`
pointer that came over named the other variable's inline slot -- which by then
held this variable's own old value -- and SWAP silently did nothing. Caught by
tests/language/housekeeping/verbs.bas, which is the golden corpus earning its
keep.

tests/value_pool.c is the new coverage. It asserts the mechanism as well as the
consequence: a later change that moved arrays inline too would pass every
behavioural case and quietly break the pointer guarantee. The sharpest case
takes the pool's whole 4096 slots in four arrays after two hundred scope
entries, so one leaked slot has nowhere to go.

Chapter 17 Step 3 taught "declare every name at the top" and no longer needs to.
It now teaches what is still true -- a name first seen inside a subroutine dies
at RETURN, so a routine cannot answer its caller through one -- and its
demonstration is the array case, which still fails.

TODO.md section 6 item 30 and section 9 item 1, both struck.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 23:47:49 -04:00

25 KiB

18. Tutorial: Breakout with artwork

Chapter 17 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.

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.

$ ./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.

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

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, released under CC0; 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.

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 — 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:

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

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:

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:

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:

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

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:

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:

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:

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 LABELs and GOTOs 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.

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

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:

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:

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
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:

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:

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:

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:

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:

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 — 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 —

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:

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

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"
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:

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

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
? 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

DIM SH$(4)
SSHAPE SH$(2), 0, 0, 61, 4
PRINT "[" + SH$(0) + "] [" + SH$(2) + "]"
[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

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
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 for arrays and structures; a scalar costs none six arrays, declared once — see Chapter 17
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 is the sprite reference: every form of SPRSAV, the MOVSPR forms, collision and what RSPPOS reads back.
  • Chapter 6 is the drawing reference, including SSHAPE, GSHAPE and what a shape handle is.
  • Chapter 7 is SOUND, PLAY, ENVELOPE and VOL.
  • Chapter 14 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.