Files
akbasic/docs/18-tutorial-breakout-artwork.md
Tachikoma e2a3f08613 Stop chapter 18 contradicting itself about FILLROW and DRAWHUD
The assembly step still carried the pre-conversion FILLROW, without the
SOLID registration Step 8 spends a section teaching -- a reader following
the assembly order got a wall the ball passes straight through.

DRAWHUD cleared the graphics layer and never stamped the HBL$ eraser Step 6
tells you to make. Both together took the field down every time a digit in
the HUD changed. It is GRAPHIC 1, 0 and a GSHAPE of HBL$ at 0, 0, and the
difference between that and the routines that draw the field is now stated
rather than left to be discovered.

Also say where HUDTEXT is called from, which nothing did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-08-02 13:35:14 -04:00

60 KiB
Raw Blame History

18. Tutorial: Breakout with artwork

This chapter builds Breakout a second time, out of loaded PNG artwork, with a drawn brick field, five powerups, three voices of sound and a HUD in colour. It is a bigger program than Chapter 17's and it uses a completely different set of verbs: everything here is drawn or loaded, and nothing is written into the text grid.

This is what you are building:

The finished game: a coloured HUD across the top, five rows of coloured bricks, a gem falling, the ball above the paddle

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 -, build a random-number generator — all apply here too, and are not repeated.

One of them is worth repeating, because this chapter leans on it harder than Chapter 17 does. # is an integer, % is a float and $ is a string. That is the opposite way round from Commodore BASIC, and it is why ball positions and velocities here are BLX% and BLVX% while counters and flags are BLN# and BLON#. Get one wrong and the ball moves in whole pixels, or stops.

Chapter 3 is the language reference for the rest — MOD, INSTR, MID, LEN — and Chapter 4 is BEGIN/BEND, DO ... LOOP UNTIL and GOSUB.

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

What you will do

  • Step 1 — load PNG artwork into sprites
  • Step 2 — decide what each of the eight sprites is, before writing anything else
  • Step 3 — take the text layer out of the way, so what you draw can be seen and stays seen
  • Step 4 — make one brick per colour and stamp the field out of them
  • Step 5 — draw at most one thing per frame, chosen by dirty flags
  • Step 6 — erase a broken brick in place, so the field is drawn once and not rebuilt
  • Step 7 — draw lettering with a stroke font, because text has no colour
  • Step 8 — move the ball, bounce it off the bars without a square root, and register the bricks as collision geometry so the interpreter finds the hits
  • Step 9 — drop gems and apply what catching one does
  • Step 10 — three voices of sound and a mute that costs nothing
  • Step 11 — the frame loop's states: title, serve, play, lost, cleared
  • Step 12 — put the pieces in one file, in the right order

Step 1: Put artwork on the screen

Goal: a picture from a file, on the screen, at its own size.

SPRSAV takes an image file path as well as an array of pattern bytes, and a sprite loaded from a file keeps the image's own size rather than being squeezed into 24 by 21. That is the whole of it:

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

Two paddles, a ball and three coloured gems

That is the whole game's cast: two bars, a ball and five gems.

Write down the sizes now, because every collision test later in this chapter is built out of them:

Artwork Is Shows up later as
paddleBlu.png, paddleRed.png 104 by 24 PDW# = 104, and the bar's height in HITBAR
ballBlue.png, ballGrey.png 22 by 22 + 21 on every edge of the ball's box
the five gems 48 by 46 the + 48 and + 46 in the catch test

Three more things to note.

The path is tried against the working directory first, then against the directory the program was loaded from. A .bas stored beside its own art/ folder therefore runs from anywhere.

SPRITE n, 1, 2 turns sprite n on in colour 2. A sprite's colour multiplies the artwork rather than replacing it, so colour 2 — white — is the one that leaves loaded artwork looking like itself. Any other colour tints it.

Put the art where the licence lets you. The artwork here is Kenney's Puzzle Pack 1, released under CC0, and examples/breakout/sprites/art/PROVENANCE.md records which file is used for what. CC0 does not require crediting Kenney. Do it anyway.

Step 2: Budget the eight sprite slots

Goal: know what all eight sprites are before you write the program.

There are eight sprite slots and no more. That is not a limit you will design your way around later, so spend them on paper first. This game spends them like this:

Slot Is
1 free
2 free
3 the paddle
4 the catcher bar (the purple gem)
5, 6, 7 up to three balls
8 the falling gem

Six are spent and two are left over, which is more room than the program needs. Two consequences fall straight out of the six that are spent:

  • The bricks are drawn rather than made of artwork. Sixty bricks will not fit in six slots, and a sprite is the only way to get an image file onto the screen — GSHAPE cannot stamp a sprite and SPRSAV cannot read one back out.
  • Only one gem falls at a time. There is one slot for it, so a brick broken while a gem is already falling drops nothing.

Deciding this first is what stops a feature costing an afternoon before it is abandoned.

Step 3: Make room for what you draw

Goal: something you drew, still on the screen on the next frame.

Two things have to be true before a drawing is any use, and only one of them is automatic.

A drawing persists. DRAW, BOX, CIRCLE and PAINT render into a layer the frame composites underneath the text and the sprites, so a picture you draw once is there on every frame after. You do not redraw it and you do not have to keep it anywhere.

But the text layer covers it. It repaints every row it owns, opaque, every frame — and by default it owns the whole window. So the first executable line of this program is:

WINDOW 0, 35, 49, 36

Two rows at the bottom, which is enough for the final score, and the other thirty-five belong to the drawing verbs. WINDOW l, t, r, b takes character cells, and RWINDOW(0) and RWINDOW(1) report what you ended up with.

That is the whole of it. Draw your field once and it stays; draw your HUD once and it stays.

Two consequences shape the rest of this chapter, and both are things you no longer have to do.

There is no drawing deadline. The host runs a fixed number of source lines and then presents the frame, and a drawing longer than one batch used to be a problem — it does not matter here, because a drawing that spans two batches simply arrives over two frames and the layer keeps both halves.

And there is nothing to redraw. If you change one brick, erase that brick; the other fifty-nine are still on the screen. Which is why this chapter has no routine that walks a list of everything still standing.

Step 4: Make the brick stamps

Goal: six coloured bricks, and a field stamped out of them.

Draw one brick per row colour, capture the six of them, and then stamp them wherever a brick belongs with GSHAPE. Drawing six things and stamping sixty is far cheaper than drawing sixty.

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 bricks, one per row colour, stacked vertically

Reading that from the top: GRAPHIC 1, 1 selects the graphics mode and clears it. COLOR 1, BRC#(R#) sets colour source 1 to the palette entry that row wants — a drawing verb names a source, not a colour, and there are seven sources. DRAW 1, x1, y1 TO x2, y2 draws a line using source 1.

Each brick is 68 by 16 and is filled with horizontal lines. The inner loop draws two scan lines per pass, T1# + K# and T2# + K#, so eight passes fill sixteen rows. That costs about a hundred and thirty lines for the whole set instead of the two hundred and seventy a line-at-a-time loop would take.

Capturing the six is six SSHAPEs:

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$

SSHAPE has sixteen slots and nothing gives one back, and this program spends eight of them: six brick colours and the two erasers in Step 6. Spent once, at startup, and never again — nothing here captures per frame, so there is no pool to run dry and no rebuild:

LABEL DRAWPROTOS
GRAPHIC 1, 1
WIDTH 1

with the six DRAW loops, Step 6's two erasers, and eight SSHAPEs after it. Called once, from the setup block.

PAINT would be one statement instead of sixteen, and is not usable here. It costs nearly four milliseconds a call; sixty of those is seven frames' worth of time.

Now the whole screen — walls, the brick field stamped out of the six, and the artwork on top:

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

The full field: a double border, six rows of coloured bricks, the paddle and the ball

Everything above the SSHAPE Z$, 0, 60, 800, 600 is a drawing nobody would ever see. Those four lines are what make it the screen.

A brick is 68 by 16 on a 72 by 24 pitch, ten columns by six rows, with the field's top-left corner at (42, 108). Those numbers are worth writing down once; they come back in Steps 7 and 9.

The finished game keeps its six stamps in six separate scalars — S0$ to S5$ — rather than in an array. An array works too.

Step 5: Draw at most one thing per frame

Goal: a frame loop that paces, draws one thing, and then plays the game.

LABEL FRAME
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

At most one drawing job a frame, then the game. Nothing here has a deadline any more — a drawing that spans two batches arrives over two frames — but a queue of one still keeps the work even, and it keeps the decision about what needs redrawing in one place instead of scattered through the game.

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 DPLAY# = 0 THEN GOTO DRAWJOB3
GOSUB DRAWPLAY
DPLAY# = 0
RETURN
LABEL DRAWJOB3
IF DHUD# = 0 THEN RETURN
GOSUB DRAWHUD
DHUD# = 0
RETURN

The rest of the game never draws. It sets DPLAY# = 1 or DHUD# = 1 when something has changed and gets on with its frame. One consequence is visible and deliberate: the score lags the bricks by one frame, because the field is drawn first. At thirty frames a second nobody can see it.

Those are LABELs and GOTOs rather than BEGIN blocks. Either works; the labels keep each arm to one RETURN and read the same.

Step 6: Erase one brick, not the whole field

Goal: take a broken brick off the screen without redrawing the other fifty-nine.

Because a drawing stays, removing something means covering it up. There is no filled-rectangle verb — BOX outlines and PAINT costs about four milliseconds a call — so the cheapest way to blank a region is a stamp of something blank:

COLOR 1, 1
FOR K# = 0 TO 15
  DRAW 1, 0, 130 + K# TO 67, 130 + K#
NEXT K#
SSHAPE Z$, 0, 130, 68, 146 : BL$ = Z$

The same sixteen lines the brick stamps are made of, in the background colour. Erasing is then one statement:

LABEL ERASEBRICK
Z$ = BL$
GSHAPE Z$, BRKX# + C# * 72, BRKY# + R# * 24
RETURN

Make a second one the width of the HUD strip while you are here, for the same reason: the strip is rewritten whenever a number in it changes, and the old digits have to go somewhere before the new ones are drawn. It is the same code with a bigger rectangle:

FOR K# = 0 TO 59
  DRAW 1, 0, 160 + K# TO 799, 160 + K#
NEXT K#
SSHAPE Z$, 0, 160, 800, 220 : HBL$ = Z$

Both blocks belong at the end of DRAWPROTOS in Step 4, drawn below the six brick prototypes and captured with them, so the eight SSHAPE slots are all spent in one place. HBL$ is stamped at the top of the HUD redraw and BL$ by ERASEBRICK.

This is what makes the whole field a draw-once job. Draw the walls and all sixty bricks when a level is laid out, and after that touch only what changes. A program that had to redraw the field to remove one brick would need a list of what is still standing, kept in step with the array, walked in runs per row — and none of that is here, because none of it is needed.

Step 7: Draw lettering with a stroke font

Goal: a HUD in more than one colour.

The text grid draws in one colour and has no verb that changes it — CHAR accepts a colour argument and ignores it. A coloured HUD therefore has to be drawn, which means carrying a font.

The font 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, 6 the bottom left 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

The word SCORE drawn as line strokes

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.

Turning a character into a glyph number is one call: INSTR(ALPHA$, MID(TX$, TXI#, 1)) over an alphabet string. MID and INSTR both count from zero in this dialect, which is the opposite of Commodore BASIC and the opposite of most other BASICs — MID(A$, 0, 1) is the first character, and INSTR answers 0 for a match at the front and 1 for no match at all. That is what makes the glyph number an array index with no adjustment, and it is also why MID("0123456789", D#, 1) turns a digit straight into its character. That is why the order of the DATA lines matters — they have to match the order of the characters in ALPHA$. The finished game uses a 41-character alphabet covering A to Z, 0 to 9, space, colon, dash, full stop and exclamation mark.

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, 0
WIDTH 1
COLOR 0, 1 : COLOR 1, 4 : COLOR 2, 8 : COLOR 3, 5
COLOR 4, 11 : COLOR 5, 16 : COLOR 6, 6
Z$ = HBL$
GSHAPE Z$, 0, 0
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
RETURN

GRAPHIC 1, 0 selects the graphics mode without clearing it, and that is the whole difference between this routine and the ones that draw the field. Clearing here would take the field down with the old digits, so the strip is covered by Step 6's HBL$ eraser at 0, 0 instead — the drawing stays, so the only way to remove something is to draw over it. GRAPHIC 1, 1 is right in a routine that is about to redraw everything anyway.

HUDTEXT builds the stroke list and DRAWHUD replays it, on different frames. Call HUDTEXT wherever a number in the HUD changes — level setup, a broken brick, a lost life, a caught gem — and it sets DHUD# = 1, which is all it takes to get the strip drawn.

HX1#() to HY2#() are the stroke endpoints and HC#() is the colour source each stroke wants, 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.

Note the one-stroke case written out beside the loop. A FOR with equal bounds does not run its body at all in this dialect, so FOR I# = 0 TO HN# - 1 with one stroke in the list draws nothing. Write the one-item case out beside the loop with a GOTO past the loop, and do it wherever a list can hold exactly one thing — a one-character word, a one-stroke glyph, a field with one brick left. Chapter 13 records the rule; TODO.md §6 item 19 records why it stands.

Step 8: Move and bounce the ball

Goal: bounces at sensible angles, computed without a square root.

There is no SQR in this dialect, so never compute a magnitude. Instead, tabulate eight landing zones across the bar, each holding very nearly a unit vector, and make every velocity 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

Read those into ZVX%() and ZVY%(). A bar bounce is then: work out which zone the ball landed in, and assign.

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
SOUND 1, 4298, 5
HIT# = 1
RETURN

T1# is the bar's left edge, T2# its top and T3# its width, so the same routine serves the paddle and the catcher bar — BALLPADDLE in Step 12 calls it twice with different values and reads HIT# to find out whether the first call already caught the ball. The four tests at the top are the overlap; each can bail out, and reaching the fifth line means the ball is on the bar.

Step 9 adds two more lines before that RETURN, for the sticky gem.

Which type a variable is decides the arithmetic

Two rules matter here and they matter a lot, because breaking either produces no error at all — the program computes something else and carries on.

A ball's velocity must be a float. BLVX% has a % suffix; a # variable holds -0.85 * 6.0 as -5, and a ball whose velocity is quantised to whole pixels bleeds speed away at every bounce.

The left operand of an expression decides whether it is done in integers or floats. Put the float on the left:

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%)
INTEGER FIRST 5.600000
FLOAT FIRST   7.400000
0 - V%        -6
0.0 - V%      -6.400000

LEVEL# * 0.45 is zero because LEVEL# is an integer, so reversing a ball is 0.0 - BLVX%(B#) and never 0 - BLVX%(B#). This is a decision of the dialect rather than a defect — Chapter 3 has the two rules that keep you out of it, and Chapter 13 names it as the difference from BASIC 7.0 most likely to turn a working listing into a quietly wrong one.

Bricks

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 changing the speed a ratio of two speeds rather than a change of magnitude, which is what the SLOW gem needs:

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 on purpose: an integer one holds the 0.75 of a slowdown as 0 and stops the ball dead.

Scaling by the vertical component instead — setting BLVY% to the new speed and taking the horizontal along with it — is not a slowdown at all. A shallow ball's small vertical gets stretched up and drags the large horizontal with it, so SLOW makes the ball faster. The table above is why: the outermost zones keep only 0.53 of their speed in the vertical, so scaling that up to the new speed multiplies everything by 1/0.53, and the ball ends up nearly twice as fast as intended. Every zone inflates; only the middle two are close enough to vertical for it not to show.

Bricks are collision geometry, not arithmetic

A brick is a rectangle the interpreter knows about. Register one per brick as the level is laid out:

LABEL FILLROW
FOR C# = 0 TO 9
  BRK#(R# * 10 + C#) = 1
  BRN# = BRN# + 1
  SLX# = BRKX# + C# * 72
  SLY# = BRKY# + R# * 24
  SLI# = (R# * 10 + C#) + 1
  SOLID SLI#, SLX#, SLY#, SLX# + 68, SLY# + 16
NEXT C#
RETURN

The id is the array index plus one, so what comes back out indexes BRK#() with no lookup. Compute it into SLI# first rather than writing the expression inline — a parenthesised first argument to a verb is worth avoiding, and it reads better anyway.

Retire the previous level's rectangles before laying the next one out, not after. That is one line in SETUPLEVEL, and putting it in the wrong place is silent: registering all sixty and then clearing them leaves a wall the ball passes straight through, with no error and nothing to see.

Then arm a handler and let it do the work:

COLLISION 2, BRICKHIT
LABEL BRICKHIT
MB# = BUMP(2)
IF MB# = 0 THEN RETURN
B# = 0
IF (MB# AND 16) <> 0 THEN GOSUB ONEBRICK
B# = 1
IF (MB# AND 32) <> 0 THEN GOSUB ONEBRICK
B# = 2
IF (MB# AND 64) <> 0 THEN GOSUB ONEBRICK
RETURN

The mask is by sprite, and the balls are sprites 5, 6 and 7 — so their bits are 16, 32 and 64, not 1, 2 and 4. Sprite n sets bit 2 to the power of n 1: sprite 1 is 1, sprite 2 is 2, sprite 3 is 4, sprite 4 is 8, sprite 5 is 16, and so on to sprite 8 at 128. That is the same 5 + B# arithmetic MOVSPR uses for them everywhere else, and getting it wrong costs nothing visible: the handler runs, tests bits nothing sets, and returns.

LABEL ONEBRICK
IF BLON#(B#) = 0 THEN RETURN
N# = RCOLLISION(5 + B#, 1)
IF N# < 1 THEN RETURN
N# = N# - 1
IF BRK#(N#) = 0 THEN RETURN
DP% = RCOLLISION(5 + B#, 4)
BLX%(B#) = BLX%(B#) + (RCOLLISION(5 + B#, 2) * DP%)
BLY%(B#) = BLY%(B#) + (RCOLLISION(5 + B#, 3) * DP%)
AX# = RCOLLISION(5 + B#, 7)
IF AX# = 1 THEN BLVX%(B#) = 0.0 - BLVX%(B#)
IF AX# = 2 THEN BLVY%(B#) = 0.0 - BLVY%(B#)
R# = N# / 10
C# = MOD(N#, 10)
BRK#(N#) = 0
SOLID N# + 1
BRN# = BRN# - 1
SCORE# = SCORE# + BRV#(R#)
SOUND 1, 17175 - R# * 2100, 4
BX1# = BRKX# + C# * 72
BY1# = BRKY# + R# * 24
GOSUB ERASEBRICK
GOSUB HUDTEXT
IF GMON# = 1 THEN RETURN
RNMAX# = 7
GOSUB NEXTRAND
IF RNVAL# = 0 THEN GOSUB SPAWNGEM
RETURN

BX1# and BY1# are the broken brick's top-left corner in pixels, and they are set here because this is the only routine that knows it — Step 9's SPAWNGEM starts the gem falling from that spot. The last four lines are the whole of "a broken brick drops a gem about one time in seven": one roll, and IF GMON# = 1 THEN RETURN first because there is one gem sprite and a gem already falling means no roll at all.

Fields 2, 3 and 4 push the ball exactly clear along the contact normal. Field 7 is which axis to reverse — the minimum translation axis, which is why a ball clipping the end of a row goes sideways rather than straight back down. Working that out by hand means building an overlap rectangle and comparing its width to its height; it is one field here.

SOLID N# + 1 retires the rectangle in the same statement that clears the array, so the next frame cannot hit a brick that is no longer drawn. GOSUB ERASEBRICK takes it off the screen with Step 6's blank stamp.

Both records exist on purpose. BRK#() is the program's — it is what "is the level clear" counts and what the field redraw reads. The rectangles are the interpreter's. Keeping them in step is two statements and the ids line up, which is the whole reason the id is the index plus one.

Step 9: Gems and powerups

Goal: one falling gem that can be any of five things.

A gem is one sprite, one type number and two timers. The type picks the artwork, the banner colour and what catching it 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 its artwork, so there is no need for a slot per gem — which is just as well, because Step 2 did not leave one.

Catching one is one routine with five arms. Each sets a timer, sets the banner text, and does whatever that gem does:

LABEL TAKEGEM
SOUND 2, 8579, 14, 1, 17175, 400
SCORE# = SCORE# + 50
GOSUB HUDTEXT
IF GMTYP# = 1 THEN BEGIN
  PDW# = 208
  PDEXP# = 600
  SPRITE 3, 1, 2, 0, 1, 0
  BAN$ = "EXPAND"
BEND
IF GMTYP# = 2 THEN BEGIN
  BAN$ = "MULTI"
  GOSUB SPLITBALLS
BEND
IF GMTYP# = 3 THEN BEGIN
  SLOWT# = 480
  SPD% = 4.2
  GOSUB RESCALE
  BAN$ = "SLOW"
BEND
IF GMTYP# = 4 THEN BEGIN
  STKON# = 1
  STKT# = 540
  BAN$ = "STICKY"
BEND
IF GMTYP# = 5 THEN BEGIN
  CTON# = 1
  CTTM# = 720
  SPRITE 4, 1, 2
  BAN$ = "CATCH"
BEND
BANT# = 90
GOSUB SETBANNER
RETURN

EXPAND is SPRITE 3, 1, 2, 0, 1, 0 — the x-expand flag, which is the only scaling a sprite has, and doubling the artwork is exactly what it wants. PDW# is the width the collision test uses, so it has to be doubled with it.

MULTI throws two more balls off the one in play, at slightly different angles so they do not travel as a stack:

LABEL SPLITBALLS
IF BLN# > 2 THEN RETURN
IF BLON#(0) = 0 THEN RETURN
FOR B# = 1 TO 2
  IF BLON#(B#) = 0 THEN BEGIN
    BLON#(B#) = 1
    BLST#(B#) = 0
    BLX%(B#) = BLX%(0)
    BLY%(B#) = BLY%(0)
    BLVX%(B#) = ZVX%(B# * 5 - 4) * SPD%
    BLVY%(B#) = ZVY%(B# * 5 - 4) * SPD%
    BLN# = BLN# + 1
    SPRITE 5 + B#, 1, 2
  BEND
NEXT B#
RETURN

STICKY is three lines at the end of HITBAR in Step 8, replacing its final RETURN — the ball stops where it landed and remembers its offset along the bar:

IF STKON# = 0 THEN RETURN
BLST#(B#) = 1
BLOFF# = BLX%(B#) - PDX%
RETURN

and space releases it, straight up the middle:

LABEL UNSTICK
FOR B# = 0 TO 2
  IF BLST#(B#) = 1 THEN BEGIN
    BLST#(B#) = 0
    BLVX%(B#) = ZVX%(4) * SPD%
    BLVY%(B#) = ZVY%(4) * SPD%
  BEND
NEXT B#
RETURN

The CATCH bar mirrors the paddle rather than following it — the line is in MOVE