Files
akbasic/docs/17-tutorial-breakout.md
Andrew Kesterson 1fb808f480
Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m21s
akbasic CI Build / sanitizers (push) Failing after 4m32s
akbasic CI Build / coverage (push) Failing after 3m37s
akbasic CI Build / akgl_build (push) Failing after 22s
akbasic CI Build / mutation_test (push) Failing after 3m30s
Rewrite the two game tutorials as instructions rather than commentary
Chapters 17 and 18 read as a code review of a finished listing: they explained
why each decision had been made, walked through the project's own history, and
led with what had once been broken. A reader who wanted to build the game got
the reasoning and had to reconstruct the program.

Both are now step-by-step. Each opens with a picture of the finished game and a
bullet list of the steps, each bullet is a section, and each section states its
goal, shows the code, and says how to check it. Chapter 17 is sixteen steps and
Chapter 18 is thirteen, and the last of each is the assembly: the order of the
file, the full declaration block, and the routines the earlier steps referred to.
Project history is gone -- it belongs in Chapter 14 and in git -- and where a
listing has to do something awkward, the tutorial shows how first and names the
`TODO.md` item that will make it unnecessary second.

**Three defects had no entry anywhere**, which the rewrite found by trying to
state each rule as a rule. §6 item 35: `a - b + c` computes `a - (b + c)`,
because `subtraction()` sits above `addition()` as its own precedence level and
the inner loop eats the `+`. Item 36: only one unparenthesised `AND` or `OR` is
matched, which is item 12's `if`-where-`while` on the one operator pair item 12
did not reach. Item 37: a `GOTO` out of a `FOR` or a `DO` leaks the loop's scope,
so a main loop written that way stops on the thirty-second lost life -- which is
why both games are built out of `LABEL` and `GOTO`, and it is a workaround rather
than a preference. Chapter 3 gains the identifier rule the third trial ran into:
there is no underscore in a name, and the error says `UNKNOWN TOKEN _`.

**`tools/screenshot.c` learned to draw the text layer**, behind a new `text=1`
fence attribute, because Chapter 17's game is characters in the grid and a figure
without that layer is two sprites on black. It opens the bundled font at the size
the standalone frontend uses, so a figure's cell size is the reader's cell size,
and it uses the akgl sink alone rather than a tee so the program's output lands in
the picture instead of on the stdout the caller reads to decide a figure failed.
Both new figures -- `breakout-game.png` and `breakout-game-artwork.png` -- are
generated from listings in the chapters like every other one.

Verified by handing each chapter, alone, to an agent on a much smaller model and
telling it to build the game from the tutorial text with the `examples/` tree off
limits. The first pass scored 3.5 and 3 out of 10 and named what was missing:
routines referred to but never shown, the third level layout, the sprite `DATA`,
the font table, edits to earlier routines that were never marked as edits. Those
are now in. The second pass built a 658-line Chapter 17 game that plays itself
for ninety seconds with the score at 1890 and no error line, and the third built
a 1053-line Chapter 18 game with 61 labels, no invented routines, no gaps found,
and forty seconds clean. 9/10 and 8/10.

One real bug in the new prose, caught in review: Chapter 18's `HITBAR` did not
set the `HIT#` that `BALLPADDLE` reads to decide whether the paddle already
caught the ball. Both suites green in both configurations, `docs_examples` and
`docs_screenshots --check` pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL
2026-08-02 08:08:23 -04:00

48 KiB

17. Tutorial: Breakout

This chapter builds a complete game from an empty file: three lives, six rows of bricks, three level layouts, a score, a high score, sound, and a title screen that plays itself. Nothing is loaded from disk — the ball and the paddle are drawn from numbers in the listing, and the wall, the HUD and the messages are characters written into the text grid.

This is what you are building:

The finished game: a HUD line across the top, six rows of bricks, the ball resting on the paddle

The finished listing is examples/breakout/characters/breakout.bas. You do not need it to follow along, but it is the same program assembled, and it is worth opening once you have your own running.

Run it with the SDL build:

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

What you will do

  • Step 1 — get a program onto the screen, and find out which build you need
  • Step 2 — measure the screen and work the geometry out from what you measured
  • Step 3 — declare every name the program will use, before anything uses it
  • Step 4 — make the ball and the paddle out of sprite data
  • Step 5 — build the brick wall out of characters, and keep a grid saying which bricks are still there
  • Step 6 — write the main loop and the frame pacing
  • Step 7 — read the keyboard without blocking
  • Step 8 — move the paddle and keep it on the screen
  • Step 9 — move the ball and bounce it off the walls
  • Step 10 — work out which brick the ball is touching, and break it
  • Step 11 — bounce off the paddle at an angle the player chooses
  • Step 12 — draw the HUD and the messages
  • Step 13 — handle lives, levels and game over
  • Step 14 — add sound, on a machine that may not have any
  • Step 15 — add a title screen that plays itself
  • Step 16 — put the pieces in one file, in the right order

Each step is a piece you can type in and run. Work through them in order, and Step 16 is where they become one program.


Step 1: Get a program onto the screen

Goal: a program that runs, and a build that can draw.

A BASIC program here is a text file. Line numbers are optional, so a file is just statements, one per line:

PRINT "HELLO"
END
HELLO

Save that as first.bas and run it:

$ ./build/basic first.bas

There are two builds and the difference matters for this whole chapter. ./build/basic is the plain one: it reads and writes text and has no graphics device, no sprites and no sound. ./build-akgl/basic opens a window and has all three. A game needs the second.

Ask for something that needs a device and you get a refusal naming what is missing rather than a crash:

PRINT "BEFORE"
SPRITE 1, 1, 2
PRINT "AFTER"
BEFORE
? 2 : RUNTIME ERROR SPRITE needs a sprite device and this runtime has none

That is worth seeing once, because it is how the finished game behaves if you start it with the wrong build: it stops at the first graphics verb and tells you why.

For the rest of this chapter, run everything with ./build-akgl/basic.

Two more things to know before writing anything longer.

Every variable carries a type suffix. A# is an integer, A% is a float, A$ is a string. There is no such thing as a plain A — a bare word is a label. This is different from Commodore BASIC, where A% is the integer.

+ joins a string to a number. "SCORE " + 40 is "SCORE 40", and "" + N# is how you turn a number into a string. There is no STR$.

N# = 40
PRINT "SCORE " + N#
END
SCORE 40

Step 2: Measure the screen

Goal: every position in the game derived from the window's real size, so the game fits whatever window and font the player has.

Do not guess the size of anything. Four functions tell you what you have:

Call Gives
RGR(1) the window's width in pixels
RGR(2) the window's height in pixels
RGR(3) the width of one character cell, in pixels
RGR(4) the height of one character cell
RWINDOW(0) the text grid's height, in rows
RWINDOW(1) the text grid's width, in columns

Put them at the very top of the program:

SCW# = RGR(1)
SCH# = RGR(2)
CW# = RGR(3)
CH# = RGR(4)
COLS# = RWINDOW(1)
ROWS# = RWINDOW(0)

RGR(1) is first on purpose. It is the first statement in the program that needs a graphics device, so on the wrong build this is the line that refuses, and it refuses before anything else has happened.

Now derive the layout from those numbers rather than writing pixel positions down. The wall is measured in character cells, because it is made of characters:

BRW# = 4
BCOLS# = 10
BROWS# = 6
BTOP# = 4
BLEFT# = (COLS# - (BRW# * BCOLS#)) / 2

A brick is four cells wide and there are ten of them, so the wall is forty cells across. BLEFT# centres those forty in however many columns there turned out to be. BTOP# is the row the wall starts on. Integer division truncates, which is exactly what a column index wants.

The ball and the paddle are sprites, and sprites are positioned in pixels, so the play area is measured in pixels:

TOPY# = 2 * CH#
MAXX# = SCW# - 8
PW# = 144
PY# = 528
LOSEY# = PY# + 32
PSPD# = 10
MSGROW# = 35
TITROW# = 20

TOPY# is the ceiling the ball bounces off, two rows down from the top so it clears the HUD line. MAXX# is as far right as the ball can go — the window less the ball's own eight pixels. PW# is the paddle's width and PY# the row of pixels it sits on. MSGROW# and TITROW# are text rows, counted from the top, for messages.

Cells and pixels are two different coordinate systems and only one routine in the whole game converts between them. That routine is in Step 10. Everywhere else, bricks are in cells and everything that moves is in pixels.

Step 3: Declare every name first

Goal: a subroutine that can hand an answer back to its caller.

A GOSUB gets its own scope. That has one consequence you must design around:

  • Assigning to a name that already exists outside the subroutine walks up and finds it. The change is visible to the caller.
  • Assigning to a name that has never been seen before creates it inside the subroutine. It disappears at RETURN, and the caller reads zero — with no error and no warning of any kind.

So a subroutine cannot answer through a name it invented itself. Declare the names first, at the top of the program, and then any routine can write to them:

X# = 0
GOSUB SUBA
PRINT "THE CALLER SEES " + X#
END

LABEL SUBA
X# = 99
RETURN
THE CALLER SEES 99

Take X# = 0 off the top of that program and it prints 0.

This is why the game opens with a block that names everything before the first GOSUB. Arrays go in the same block. Here is the start of it — the names the later steps refer to most; Step 16 has the finished block, all 66 of them, and you can paste that in now if you would rather not come back to it:

DIM BR#(60)
DIM SB#(63)
DIM SP#(63)
DIM LAY$(6)
DIM BSG$(6)

SCORE# = 0
HIGH# = 0
LIVES# = 3
LEVEL# = 1
LEFTN# = 0
BSPD# = 4
STATE# = 0
HELD# = 0
PX# = 0
BX# = 0
BY# = 0
BVX# = 0
BVY# = 0
HIT# = 0
BI# = 0
BR2# = 0

HIT#, BI# and BR2# are the three the rule is really about: they are how the collision test in Step 10 answers the routine that called it.

DIM belongs at the top and nowhere else. An array declared inside a GOSUB or a loop takes storage from a pool of 4096 elements that is never given back, so a DIM that runs repeatedly will eventually end the program:

X# = 0
FOR T# = 1 TO 6000
  GOSUB SUBA
NEXT T#
PRINT "OK " + X#
END

LABEL SUBA
DIM LOC#(4)
LOC#(0) = 1
X# = X# + LOC#(0)
RETURN
? 8 : RUNTIME ERROR Array of 4 elements does not fit in the 0 remaining value slots

A plain number costs nothing, though, so you can create as many of those inside a routine as you like:

X# = 0
FOR T# = 1 TO 20000
  GOSUB SUBA
NEXT T#
PRINT "OK " + X#
END

LABEL SUBA
LOC# = 1
X# = X# + LOC#
RETURN
OK 20000

Twenty thousand calls, each creating a local, and the program is fine. The rule that survives is the narrow one: DIM at the top, never inside anything.

Two rules about writing expressions

Both apply everywhere in this chapter, so they are worth fixing in your fingers now.

Parenthesise any expression that mixes + and -. Write (A# - B#) + C#, not A# - B# + C#:

A# = 10
B# = 3
C# = 2
PRINT (A# - B#) + C#
END
9

Parenthesise a condition that uses more than one AND or OR. Write IF (A# = 1 AND B# = 2) AND C# = 3 THEN:

A# = 1
B# = 2
C# = 3
IF (A# = 1 AND B# = 2) AND C# = 3 THEN PRINT "ALL THREE"
END
ALL THREE

Both habits are free, and both are being made unnecessary: the parser's additive precedence is TODO.md §6 item 35 and the second AND is item 36 beside it. Once those land, the plain forms will mean what they look like they mean, and parentheses you wrote in the meantime will still be correct.

Step 4: Make the ball and the paddle

Goal: two sprites on the screen, in the right places.

A sprite is a 24-by-21 pattern of pixels. SPRSAV loads one from an integer array of 63 numbers: three bytes per row, twenty-one rows, most significant bit on the left. A bit that is 1 draws; a bit that is 0 is transparent.

       byte 0            byte 1            byte 2
  7 6 5 4 3 2 1 0   7 6 5 4 3 2 1 0   7 6 5 4 3 2 1 0
  ^ column 0                                        ^ column 23

The ball is an 8-by-8 disc drawn in the top-left corner of the pattern. Putting it there means the sprite's position and the ball's pixel rectangle are the same thing, so no part of the game needs to add an offset to work out where the ball actually is:

  . . # # # # . .    60
  . # # # # # # .   126
  # # # # # # # #   255

The paddle is a solid bar, 24 wide and 6 deep — the full width of a sprite. SPRITE's x-expand flag doubles it to 48 pixels, and three of those laid end to end make one 144-pixel paddle with no seam.

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

The ball above three paddle segments meeting with no seam

Read that from the middle. SPRSAV SB#, 1 installs the ball's pattern into slot 1. SPRITE 1, 1, 2 turns slot 1 on in colour 2. SPRITE 2, 1, 15, 0, 1, 0 turns slot 2 on in colour 15 with the fourth argument after the colour — the x-expand flag — set to 1. MOVSPR puts a sprite at a pixel position, measured from its top-left corner.

The finished game writes the same 63 numbers out as DATA, one row per line, with the bit pattern drawn in a comment beside each, and loads them in a routine called once at startup. Step 16 has that routine and both patterns in full; the loops above are the same two patterns spelled shorter so the figure fits on a page.

Moving the four sprites is one routine, and every coordinate is worked out into a variable before it is passed:

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, and an expression beginning with a sign is the relative form. Computing into X2# first says exactly what you mean. See Chapter 8.

Step 5: Build the brick wall

Goal: a wall on the screen, and a grid in memory saying which bricks are still standing.

Two things are needed and they are separate: the state (which of the sixty bricks are alive) and the picture (what the player sees). Keep them in step by always redrawing the row you just changed.

The layouts

A level is six strings of ten characters, 1 for a brick and 0 for a hole, stored as DATA:

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"

LABEL LAY3
DATA "1010101010"
DATA "0101010101"
DATA "1111001111"
DATA "1100110011"
DATA "1011111101"
DATA "0110110110"

READ takes the next DATA item, and RESTORE moves that cursor to a given place. RESTORE accepts a label, so each layout is simply a named position in the DATA stream and choosing one is three IFs:

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

Adding a fourth level is a DATA block and one more IF.

One READ cursor walks every DATA item in the file, in the order they were written, no matter which routine is doing the reading. If you have two things to load — a font and a table, say — the one whose DATA comes first in the file must be loaded first, or use RESTORE to say where to start.

The grid

Turn those strings into a flat array of sixty numbers, and count how many bricks there are while you are at it:

LABEL BUILDW
LEFTN# = 0
FOR R# = 0 TO 5
  S$ = LAY$(R#)
  GOSUB BUILDR
NEXT R#
RETURN

LABEL BUILDR
FOR C# = 0 TO 9
  T$ = MID(S$, C#, 1)
  I# = (R# * BCOLS#) + C#
  BR#(I#) = 0
  IF T$ = "1" THEN BR#(I#) = 1
  IF T$ = "1" THEN LEFTN# = LEFTN# + 1
NEXT C#
RETURN

MID counts from zero here, unlike Commodore BASIC. BR#(row * 10 + col) is the brick at that cell; LEFTN# is how many are left, so "is the level finished" is a comparison rather than a scan of sixty cells.

Drawing a row

CHAR writes a string into the text grid at a row and column. It has one property that decides how the rest of this section is written: CHAR terminates the row where it stops. Writing at column 12 erases everything from column 13 to the end of the line.

So do not write bricks one at a time. Build the whole row as a string and write it once, from column 0:

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
     [##][##][##][##][##][##][##][##][##][##]
     [##][##][##][##][##][##][##][##][##][##]
     [==][==]    [==][==][==][==]    [==][==]
     [==][==][==][==][==][==][==][==][==][==]
     [--]    [--][--][--][--]    [--][--][--]
         [--][--]    [--][--]    [--][--][--]

A missing brick contributes four spaces, so every row is the same length and the columns line up. That block uses PRINT, so it runs on either build — which makes it a good way to check your layouts before you have a window.

In the game, replace the PRINT S$ with a CHAR, and use the real left margin:

LABEL DRAWBR
S$ = ""
IF BLEFT# > 0 THEN S$ = " " * BLEFT#
BSEG$ = BSG$(BR2#)
FOR CC# = 0 TO 9
  I# = (BR2# * BCOLS#) + CC#
  IF BR#(I#) = 1 THEN S$ = S$ + BSEG$
  IF BR#(I#) = 0 THEN S$ = S$ + "    "
NEXT CC#
R2# = BTOP# + BR2#
CHAR 1, 0, R2#, S$
RETURN

" " * BLEFT# repeats a string, which is how you get a margin of any width.

Tell the rows apart by shape, not by colour. CHAR accepts a colour argument and ignores it — the text layer draws in one colour — so [##], [==] and [--] are what makes a 60-point row look different from a 10-point one.

Drawing the whole wall is then six calls:

LABEL DRAWW
FOR R# = 0 TO 5
  BR2# = R#
  GOSUB DRAWBR
NEXT R#
RETURN

Step 6: Write the main loop

Goal: a loop that runs one frame's worth of work and comes back, for as long as the game lasts.

Write the main loop with LABEL and GOTO:

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

A label is one bare word: letters and digits, no underscore. TICKEND is a label; TICK_END is a parse error, and the message you get is UNKNOWN TOKEN _ rather than anything about naming, so it is worth not writing in the first place. The same goes for variable names.

STATE# is a plain number that the rest of the game writes when something has happened: 0 is playing, 1 is "the ball was lost", 2 is "the level is clear", 3 is "quit", 5 is "start a new game". Nothing acts on it where it is set — the loop notices on the next pass and branches. That keeps every state change in one place and out of the middle of the physics.

The branch targets end in GOTO TICK rather than RETURN. Nothing GOSUBs into a state change, so the subroutine stack is empty again by the time the next frame begins.

SLEEP 0.02 is the frame pacing, and it does not block the host. It records a deadline and the interpreter declines to advance your program until the clock reaches it, so the window keeps redrawing and the keyboard keeps being read the whole time. 0.02 seconds asks for fifty frames a second.

Why GOTO rather than DO ... LOOP

A DO ... LOOP around the frame would read better, and it is not usable here: a GOTO that jumps out of a FOR or a DO does not release the loop's scope. There are 32 scopes, so a game that leaves its main loop once per lost life stops on the thirty-second one:

N# = 0
LABEL TOP
DO
  N# = N# + 1
  IF N# < 100 THEN GOTO TOP
LOOP UNTIL N# > 99
PRINT "SURVIVED " + N#
? 3 : PARSE ERROR Environment pool exhausted at line 3 (32 in use)

A LABEL/GOTO loop pushes no scope at all, so it can run for as long as the machine is on. Use FOR and DO freely for work that finishes inside a frame — the wall builder in Step 5 is a FOR inside a FOR — and use GOTO for anything you will branch out of. This is TODO.md §6 item 37; when it is fixed, DO ... LOOP will be available for a main loop too, and a GOTO loop will still be correct.

Step 7: Read the keyboard

Goal: a paddle that moves smoothly while a key is held, using the only input verb there is.

GET takes one keystroke from a queue the host fills, and returns 0 if there is nothing waiting. It never blocks. There is no key-up event and no way to ask whether a key is currently down.

What you get instead is the operating system's own key repeat, arriving as ordinary keypresses about once a frame while a key is held. So: drain the queue every frame, and let each keystroke refill a countdown.

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

Eight GETs a frame is enough to keep the queue from backing up. PDIR# is which way to move and PDEC# is how many more frames to keep moving; a keypress sets both.

The key codes are the host's. 1073741903 and 1073741904 are the right and left arrows, 32 is space, 112 is P, 113 is Q and 27 is escape. Chapter 12 says where they come from. A quick way to find any other key is to print what you get:

LABEL SHOWKEY
GET K#
IF K# <> 0 THEN PRINT "KEY " + K#
GOTO SHOWKEY

Note 0 - 1 rather than -1 in PDIR# = 0 - 1. Both work; writing the subtraction out is the habit that goes with the parenthesising rule from Step 3, and it is what the rest of this chapter does.

Step 8: Move the paddle

Goal: the paddle moves while the countdown lasts, and stops at both edges.

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 countdown is the whole trick. A single tap sets PDEC# to 12 and the paddle coasts for twelve frames; holding the key down refills it faster than it drains, so the paddle keeps going. Held reads as held and tapped reads as a nudge, with no held-key state anywhere.

The clamp is written as a jump to PADCLMP rather than wrapped around the movement, because the paddle has to be pushed back inside the window whether it moved this frame or not.

M# = SCW# - PW# is computed into a variable rather than written inline. Any expression you are about to compare or pass is clearer that way, and it keeps lines short — a line here holds at most 32 tokens, so long conditions want breaking up into named pieces anyway.

Going over 32 currently stops the interpreter rather than raising a BASIC error you could TRAP, which makes a too-long line harder to diagnose than it should be. That is recorded in TODO.md §8, and the fix is to report it as the parse error it is; the limit itself stays either way, so short lines are the habit regardless.

Step 9: Move the ball

Goal: a ball that moves, bounces off three walls, and is lost off the bottom.

The ball has a position (BX#, BY#) and a velocity (BVX#, BVY#) in pixels per frame. Moving it is adding one to the other — but do the two axes separately, and test each move on its own:

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

Doing X first and then Y means a ball arriving at the corner of a brick reverses one axis rather than both, which is what looks right. OX# and OY# remember where the ball came from, so a collision has somewhere to put it back to.

A wall bounce puts the ball on the wall and flips the sign of that axis:

LABEL WALLL
BX# = 0
BVX# = 0 - BVX#
RETURN

LABEL WALLR
BX# = MAXX#
BVX# = 0 - BVX#
RETURN

LABEL WALLT
BY# = TOPY#
BVY# = 0 - BVY#
RETURN

There is no wall at the bottom. Below LOSEY# the ball is gone, and the loop's dispatch in Step 6 picks that up.

Draw a visible ceiling once, at startup, so the ball turns against something the player can see:

FRAME$ = "=" * COLS#
CHAR 1, 0, 1, FRAME$

Step 10: Break bricks

Goal: work out which brick a point is inside, and take it out of the wall.

This is the one routine that converts pixels into cells. Give it a point in TX# and TY# and it sets HIT# to 1 or 0, and when it is 1 it also sets BI# — the index into the sixty-element grid — and BR2#, the row:

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

Read it as four questions in a row, each of which can answer "no": is the point below the top of the wall, above the bottom of it, right of the left edge, left of the right edge. Only if all four pass is there a cell to look at. Dividing a pixel by CH# gives a text row; dividing by CW# gives a text column; dividing the column by BRW# gives a brick column, because a brick is four cells wide.

HIT#, BI# and BR2# work as answers only because Step 3 created all three outside this routine.

Test the ball's leading edge rather than its centre:

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

LABEL YBRICK
TX# = BX# + 4
TY# = BY#
IF BVY# > 0 THEN TY# = BY# + 7
GOSUB HITTEST
IF HIT# = 0 THEN RETURN
BY# = OY#
BVY# = 0 - BVY#
GOSUB KILLBR
RETURN

Moving right, the leading edge is BX# + 7; moving left it is BX#. 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 testing the 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 of the two trades, and it is worth knowing which one you took.

Breaking a brick clears its cell, scores it, and redraws just that row:

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

The top row is worth 60 and the bottom row 10. LEFTN# reaching zero sets the state that Step 6's dispatch turns into a level change.

Step 11: Bounce off the paddle

Goal: a bounce whose angle is decided by where on the paddle the ball landed.

This is the part that makes it a game rather than a demonstration. Divide the paddle into five zones and let the zone choose the horizontal speed:

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
RETURN

The first five lines are the overlap test, each written so it can bail out. Z# comes out 0 to 4, so (Z# - 2) * 3 gives horizontal speeds of -6, -3, 0, +3 and +6. Catch the ball on the left of the paddle and it goes left; catch it in the middle and it goes straight up.

Straight up is a problem, so the middle zone does not give it. A ball with no sideways speed rises and falls down the same column for ever and never reaches a brick it has not already broken. The last three lines before PADAIM catch that case and give the ball a shallow angle in the direction it arrived from instead.

That handles the obvious case. The subtle one is a ball that has found some other orbit missing the wall, and the cure for that is a watchdog. Count frames since the last brick, and after ten seconds' worth mark the ball for a new angle. Add these two lines to the end of MOVEBAL from Step 9, just before its RETURN:

STALL# = STALL# + 1
IF STALL# > 500 THEN NUDGE# = 1

and add one line to KILLBR from Step 10, so that hitting a brick resets the count:

STALL# = 0

Then apply the nudge at the next paddle bounce, never in mid-air — a ball that changes direction in open space looks exactly like it hit something invisible. Add this line to PADHIT above, immediately before LABEL PADAIM:

IF NUDGE# = 1 THEN GOSUB UNSTICK
LABEL UNSTICK
NUDGE# = 0
STALL# = 0
RMAX# = 4
GOSUB RANDOM
BVX# = (RND# * 3) - 6
IF BVX# = 0 THEN BVX# = 3
RETURN

You have to write your own random numbers

There is no RND in this dialect, and no INT, SQR, ASC or TIMER either. A linear congruential generator is nine tokens and does the job. Put the number of possible answers in RMAX# and read the result from RND#:

SEED# = 12345
RMAX# = 6
RND# = 0
I# = 0
FOR I# = 1 TO 5
  GOSUB RANDOM
  PRINT "ROLL " + (RND# + 1)
NEXT I#
END

LABEL RANDOM
SEED# = MOD(((SEED# * 1103515245) + 12345), 2147483648)
RND# = MOD((SEED# / 65536), RMAX#)
RETURN
ROLL 1
ROLL 5
ROLL 2
ROLL 1
ROLL 2

The multiplication stays inside a 64-bit integer for any seed below 2147483648, which is why the modulus is that number. The answer is taken from the middle bits — SEED# / 65536 — because the low bits of a power-of-two modulus barely change from one call to the next. Integer division truncating for free is the INT you do not have.

Seed it from the clock at startup. TI# is the host's uptime in sixtieths of a second, which is different every time the game is run:

SEED# = TI#

Use RANDOM for the serve, too, so the ball does not always leave in the same direction:

LABEL SERVE
PX# = (SCW# - PW#) / 2
HELD# = 1
BX# = PX# + ((PW# / 2) - 4)
BY# = PY# - 10
RMAX# = 2
GOSUB RANDOM
BVX# = BSPD#
IF RND# = 0 THEN BVX# = 0 - BSPD#
BVY# = 0 - BSPD#
PDEC# = 0
GOSUB SHOWSPR
RETURN

HELD# is the flag Step 6's loop tests: while it is 1 the ball sits on the paddle, and HOLDBAL keeps it there:

LABEL HOLDBAL
BX# = PX# + ((PW# / 2) - 4)
BY# = PY# - 10
RETURN

Space clears HELD# and the ball launches. That is the KEYSPC that Step 7's HANDKEY calls:

LABEL KEYSPC
IF HELD# = 0 THEN RETURN
HELD# = 0
GOSUB CLRMSG
RETURN

Step 15 adds two lines to the top of it.

Step 12: Draw the HUD and the messages

Goal: a status line and centred messages, both drawn the way CHAR wants.

Same rule as the brick rows: build the whole line as one string, then write it once.

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
  SCORE 001250   LIVES 3   LEVEL 2   HIGH 009900

In the game that becomes a routine called DRAWHUD, whose last line is CHAR 1, 0, 0, H$ instead of PRINT H$; everything else is the same, which means you can develop the HUD on either build. Step 16 has it written out.

"" + V# turns a number into a string, and PAD6 pads it to six digits so the line does not change width as the score grows. Every + after the first is parenthesised, for the reason in Step 3.

A centred message needs no padding on the right, because CHAR terminating the row is what erases whatever was there before:

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

Set MSG$ and MROW#, then GOSUB SHOWAT. 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.

Two small wrappers save repeating the row number:

LABEL SHOWMSG
MROW# = MSGROW#
GOSUB SHOWAT
RETURN

LABEL CLRMSG
MROW# = MSGROW#
GOSUB CLRAT
RETURN

Step 13: Lives, levels and game over

Goal: the states from Step 6, written out.

Each of these is a branch target, not a subroutine. It does its work and jumps back to TICK.

Losing a ball:

LABEL LOSTLIF
LIVES# = LIVES# - 1
GOSUB DRAWHUD
IF LIVES# < 1 THEN GOTO GAMEOVR
GOSUB SERVE
MSG$ = "BALL LOST -- PRESS SPACE"
GOSUB SHOWMSG
STATE# = 0
GOTO TICK

Clearing a level. The ball speeds up each time, to a ceiling:

LABEL LEVELUP
MSG$ = "LEVEL CLEARED"
GOSUB SHOWMSG
SLEEP 1.5
LEVEL# = LEVEL# + 1
IF BSPD# < 7 THEN BSPD# = BSPD# + 1
GOSUB NEWLEV
STATE# = 0
GOTO TICK

LABEL NEWLEV
GOSUB LOADLAY
GOSUB BUILDW
GOSUB DRAWW
GOSUB DRAWHUD
GOSUB SERVE
MSG$ = "PRESS SPACE TO LAUNCH"
GOSUB SHOWMSG
RETURN

Game over waits for a key in a small loop of its own:

LABEL GAMEOVR
MSG$ = "GAME OVER -- SPACE PLAYS AGAIN, Q QUITS"
GOSUB SHOWMSG
LABEL GOWAIT
GET K#
IF K# = 32 THEN GOTO NEWGAME
IF K# = 113 THEN GOTO BYE
IF K# = 27 THEN GOTO BYE
SLEEP 0.05
GOTO GOWAIT

And quitting turns the sprites off, so they are not left on the screen behind the final message:

LABEL BYE
SPRITE 1, 0
SPRITE 2, 0
SPRITE 3, 0
SPRITE 4, 0
SCNCLR
PRINT "BREAKOUT"
PRINT "FINAL SCORE " + SCORE#
PRINT "HIGH SCORE  " + HIGH#
SLEEP 2
QUIT

The high score is one comparison, called whenever the score changes:

LABEL HISCORE
IF SCORE# > HIGH# THEN HIGH# = SCORE#
RETURN

There is nowhere to save it. This interpreter has no disk — see Chapter 9 — so the high score lasts as long as the process does.

Step 14: Add sound

Goal: sound where there is a sound device, silence where there is not, and no crash either way.

SOUND refuses on a machine with no audio device, and an untrapped refusal ends the program. So ask once at startup, remember the answer, and never ask again. Here is the idea on its own, as a program you can run to see which kind of machine you are on:

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
NO AUDIO DEVICE, PLAYING SILENTLY

That output is from the plain build, which has no devices at all; on the SDL build the same program prints the other line.

TRAP NOAUDIO arms a handler. RESUME NEXT returns to the statement after the one that failed. TRAP with no argument disarms it again — leave it armed and the next refusal anywhere in your game is silently swallowed. See Chapter 4.

In the game that becomes a routine and its handler, called once from the setup block before anything else makes a noise:

LABEL SNDPROBE
SND# = 1
TRAP NOAUDIO
SOUND 1, 2000, 1
TRAP
RETURN

LABEL NOAUDIO
SND# = 0
RESUME NEXT

NOAUDIO has no RETURN of its own, and must not have one: RESUME NEXT is what leaves a handler, and it goes back into SNDPROBE at the TRAP after the SOUND — so the disarm still happens and the RETURN after it is the one that runs.

After that, every sound is one line guarded by the flag:

LABEL BEEPB
IF SND# = 1 THEN SOUND 1, 12000, 2
RETURN

LABEL BEEPP
IF SND# = 1 THEN SOUND 2, 6000, 3
RETURN

LABEL BEEPW
IF SND# = 1 THEN SOUND 3, 9000, 1
RETURN

LABEL BEEPL
IF SND# = 1 THEN SOUND 1, 1500, 20
RETURN

Then call them. Each is one GOSUB added to a routine you already have:

Add To From
GOSUB BEEPB KILLBR, before its RETURN Step 10
GOSUB BEEPP PADHIT, at LABEL PADAIM Step 11
GOSUB BEEPW WALLL, WALLR and WALLT, in all three Step 9
GOSUB BEEPL LOSTLIF, as its first line Step 13

So WALLL from Step 9 becomes:

LABEL WALLL
BX# = 0
BVX# = 0 - BVX#
GOSUB BEEPW
RETURN

and WALLR and WALLT take the same line in the same place.

SOUND's frequency argument is a SID register value, not hertz. The pitch you get is register * 1022730 / 16777216, so 12000 is about 732 Hz. Work the notes you want out once and write the numbers down in a comment beside them; Chapter 7 has the arithmetic.

Step 15: Add an attract mode

Goal: a title screen that plays the game by itself.

This is not decoration. A demo that plays for two minutes exercises the ball, the bricks, the level change and the speed-up together, for thousands of frames, which is the only way some defects show up at all.

One flag is the whole difference between the demo and a real game:

LABEL TITLE
DEMO# = 1
SCORE# = 0
LIVES# = 3
LEVEL# = 1
BSPD# = 4
GOSUB LOADLAY
GOSUB BUILDW
GOSUB DRAWW
GOSUB DRAWHUD
GOSUB TITTEXT
GOSUB SERVE
STATE# = 0
GOTO TICK

Everything else is four small edits to routines you already have. Each one says which.

In MOVEPAD from Step 8, add these two lines at the top, immediately after the LABEL, so that a demo paddle is driven by the machine instead of by PDEC#:

IF DEMO# = 1 THEN GOSUB DEMOPAD
IF DEMO# = 1 THEN GOTO PADCLMP

The demo paddle tracks the ball — but aims off-centre by a random amount, chosen afresh at every bounce:

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

DOFF# is between -40 and +40 pixels of deliberate error, so the machine plays like something with a hand on the paddle and occasionally misses. A demo that tracks perfectly never loses a ball and therefore never tests losing one.

DEMOAIM has to be called from somewhere, and the somewhere is the bounce. In PADHIT from Step 11, beside the NUDGE# line and immediately before LABEL PADAIM:

IF DEMO# = 1 THEN GOSUB DEMOAIM

In KEYSPC from Step 11, add two lines at the top, so that space starts a real game rather than launching the demo's ball:

LABEL KEYSPC
IF DEMO# = 1 THEN STATE# = 5
IF DEMO# = 1 THEN RETURN
IF HELD# = 0 THEN RETURN
HELD# = 0
GOSUB CLRMSG
RETURN

In LOSTLIF from Step 13, add one line at the top, so a lost ball re-serves rather than costing a life:

LABEL LOSTLIF
IF DEMO# = 1 THEN GOTO DEMOSRV
LIVES# = LIVES# - 1

with the branch target it needs:

LABEL DEMOSRV
GOSUB SERVE
HELD# = 0
STATE# = 0
GOTO TICK

In SERVE from Step 11, add one line after HELD# = 1, so the demo's ball launches by itself:

IF DEMO# = 1 THEN HELD# = 0

In KILLBR from Step 10, guard the scoreboard, because the machine does not get on it:

IF DEMO# = 0 THEN GOSUB HISCORE

Leave it running and watch the score climb. That is your test suite for everything the unit tests cannot reach.

Step 16: Put the program together

Goal: one file, in an order that runs.

A program runs from the top, so the order of the file matters in three places and nowhere else: the setup has to come first, DATA has to be in the order it will be read, and every LABEL has to exist somewhere. Subroutines can go in any order you like.

This is the shape of the whole file:

LABEL SETUP           the geometry from Step 2
                      the declaration block from Step 3
                      the brick faces from Step 5
                      SEED# = TI#
                      the ceiling from Step 9
                      GOSUB MKSPR       Step 4
                      GOSUB SNDPROBE    Step 14
                      GOTO TITLE

  the sprite routine, the sound routines, the title screen,
  the game and level setup, the main loop, the input routines,
  the paddle, the ball, the drawing routines, the state changes
  --- in any order ---

  the sprite patterns as DATA        Step 4
  the level layouts as DATA          Step 5

The two DATA blocks come last because nothing else in the program uses DATA, and the sprite patterns are read before the layouts because MKSPR runs before LOADLAY does. Change that order and each loader gets the other one's numbers.

Declare all of it

Step 3 showed the shape of the declaration block. Here it is in full — every name the finished game uses, and nothing gets to be created later:

DIM BR#(60)
DIM SB#(63)
DIM SP#(63)
DIM LAY$(6)
DIM BSG$(6)

SCORE# = 0
HIGH# = 0
LIVES# = 3
LEVEL# = 1
LEFTN# = 0
BSPD# = 4
STATE# = 0
DEMO# = 0
PAUSED# = 0
HELD# = 0
PDIR# = 0
PDEC# = 0
PX# = 0
BX# = 0
BY# = 0
BVX# = 0
BVY# = 0
OX# = 0
OY# = 0
K# = 0
HIT# = 0
PVX# = 0
DOFF# = 0
STALL# = 0
NUDGE# = 0
BI# = 0
BR2# = 0
RB# = 0
CB# = 0
RC# = 0
CC2# = 0
TX# = 0
TY# = 0
PTS# = 0
V# = 0
L# = 0
C2# = 0
D# = 0
M# = 0
X2# = 0
X3# = 0
Z# = 0
BB# = 0
RX# = 0
N# = 0
MROW# = 0
RMAX# = 2
RND# = 0
SND# = 0
SEED# = 0
P$ = ""
H$ = ""
S$ = ""
T$ = ""
MSG$ = ""
BSEG$ = ""
FRAME$ = ""
I# = 0
R# = 0
R2# = 0
C# = 0
CC# = 0
KI# = 0
T# = 0

That is 69 names out of the 128 the interpreter has. Loop counters are in there too — I#, R#, C#, CC#, KI# — because a FOR creates its counter the same way a GOSUB creates a local, and declaring them costs nothing.

The routines the earlier steps referred to but did not show

Four small ones, for completeness.

Loading the sprite patterns at startup, which is Step 4 turned into a routine:

LABEL MKSPR
FOR I# = 0 TO 62
  READ SB#(I#)
NEXT I#
FOR I# = 0 TO 62
  READ SP#(I#)
NEXT I#
SPRSAV SB#, 1
SPRITE 1, 1, 2
SPRSAV SP#, 2
SPRSAV SP#, 3
SPRSAV SP#, 4
SPRITE 2, 1, 15, 0, 1, 0
SPRITE 3, 1, 15, 0, 1, 0
SPRITE 4, 1, 15, 0, 1, 0
RETURN

Note this reads all 63 numbers for each pattern, so the DATA at the foot of the file is 21 lines of three numbers per sprite rather than the shortened form Step 4's figure used. Here are both, and the ball's is worth reading against the bit diagram in Step 4:

DATA 60, 0, 0
DATA 126, 0, 0
DATA 255, 0, 0
DATA 255, 0, 0
DATA 255, 0, 0
DATA 255, 0, 0
DATA 126, 0, 0
DATA 60, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0

DATA 255, 255, 255
DATA 255, 255, 255
DATA 255, 255, 255
DATA 255, 255, 255
DATA 255, 255, 255
DATA 255, 255, 255
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0

Eight rows of ball in the top-left corner and thirteen empty ones; six solid rows of paddle and fifteen empty ones. The zeros are not optional — MKSPR reads 63 numbers per pattern and will take them from the level layouts if they are not there.

The HUD, which is Step 12's block with CHAR instead of PRINT:

LABEL DRAWHUD
V# = SCORE#
GOSUB PAD6
H$ = "  SCORE " + P$
H$ = (H$ + "   LIVES ") + LIVES#
H$ = (H$ + "   LEVEL ") + LEVEL#
V# = HIGH#
GOSUB PAD6
H$ = (H$ + "   HIGH ") + P$
CHAR 1, 0, 0, H$
RETURN

Pause, which is the P key from Step 7, and which the demo ignores:

LABEL KEYPAU
IF DEMO# = 1 THEN RETURN
PAUSED# = 1 - PAUSED#
IF PAUSED# = 1 THEN MSG$ = "PAUSED"
IF PAUSED# = 1 THEN GOSUB SHOWMSG
IF PAUSED# = 0 THEN GOSUB CLRMSG
RETURN

Starting a real game, which is what STATE# = 5 dispatches to, and the title text and its eraser:

LABEL NEWGAME
DEMO# = 0
SCORE# = 0
LIVES# = 3
LEVEL# = 1
BSPD# = 4
PAUSED# = 0
GOSUB CLRTIT
GOSUB NEWLEV
STATE# = 0
GOTO TICK

LABEL TITTEXT
MROW# = TITROW#
MSG$ = "B R E A K O U T"
GOSUB SHOWAT
MROW# = TITROW# + 2
MSG$ = "PRESS SPACE TO PLAY"
GOSUB SHOWAT
MROW# = TITROW# + 4
MSG$ = "ATTRACT MODE"
GOSUB SHOWAT
RETURN

LABEL CLRTIT
MROW# = TITROW#
GOSUB CLRAT
MROW# = TITROW# + 2
GOSUB CLRAT
MROW# = TITROW# + 4
GOSUB CLRAT
RETURN

Check it before you have a window

Run the assembled file through the plain build first:

$ ./build/basic mybreakout.bas

It will stop at the first RGR with a message about a missing graphics device — which means everything above that point parsed, and that is the whole of what this check is for. A parse error, a bad line number or an OUT OF DATA here is a real problem, and it is far easier to find without a window in the way. Then run it properly:

$ ./build-akgl/basic mybreakout.bas

The rules this listing never breaks

A checklist to write your own game against.

Rule Because Where
DIM at the top, never inside a loop or a GOSUB An array created inside a scope takes pool storage that is never returned Step 3
Declare any name a subroutine has to answer through A name first seen inside a GOSUB dies at RETURN, silently Step 3
Parenthesise every mixed + and - a - b + c is currently evaluated as a - (b + c) Step 3
Parenthesise a second AND or OR Only one unparenthesised AND or OR is matched per expression Step 3
Compute a MOVSPR coordinate into a variable first A leading sign means move by, not move to Step 4
Build a text row whole and write it from column 0 CHAR terminates the row where it stops Step 5
Tell rows apart by shape, not colour CHAR ignores its colour argument Step 5
Loop the game with GOTO, and never jump out of a DO A GOTO out of a loop does not release the loop's scope, and there are 32 Step 6
Name things with letters and digits only There is no underscore in an identifier, and the error names the character rather than the name Step 6
Keep a line under 32 tokens The 33rd stops the interpreter rather than raising an error you can TRAP Step 8
Span a loop across lines, and never write FOR I# = 1 TO 1 A whole loop on one line does not loop; equal bounds run the body zero times Chapter 13
Probe for a device once and remember the answer An untrapped refusal ends the program Step 14

The picture at the top of this chapter

Every figure in this guide is generated by running the listing beside it, and the one at the top of this chapter is no exception. It is the game's own drawing code — the row builder from Step 5, the sprite patterns from Step 4, the HUD from Step 12 — with the numbers filled in by hand and no main loop, so it draws one frame and stops.

Type it in and you have a still of the game before you have written any of the game:

DIM SB#(63)
DIM SP#(63)
DIM BSG$(6)
DIM LAY$(6)
I# = 0
R# = 0
C# = 0
S$ = ""
T$ = ""
SCNCLR
COLS# = RWINDOW(1)
BLEFT# = (COLS# - 40) / 2
BSG$(0) = "[##]"
BSG$(1) = "[##]"
BSG$(2) = "[==]"
BSG$(3) = "[==]"
BSG$(4) = "[--]"
BSG$(5) = "[--]"
LAY$(0) = "1111111111"
LAY$(1) = "1111111111"
LAY$(2) = "1111111111"
LAY$(3) = "0111111110"
LAY$(4) = "0110110110"
LAY$(5) = "0010110100"
CHAR 1, 0, 0, "  SCORE 001250   LIVES 3   LEVEL 2   HIGH 009900"
CHAR 1, 0, 1, "=" * COLS#
FOR R# = 0 TO 5
  S$ = " " * BLEFT#
  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#
  I# = 4 + R#
  CHAR 1, 0, I#, S$
NEXT R#
CHAR 1, 0, 35, "          PRESS SPACE TO LAUNCH"
FOR I# = 0 TO 62
  SB#(I#) = 0
  SP#(I#) = 0
NEXT I#
FOR I# = 0 TO 7
  READ SB#(I# * 3)
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, 396, 518
MOVSPR 2, 328, 528
MOVSPR 3, 376, 528
MOVSPR 4, 424, 528

DATA 60, 126, 255, 255, 255, 255, 126, 60

Where to go next

  • Chapter 18 builds Breakout again out of loaded artwork, with powerups and a coloured HUD. It is a bigger program and it teaches the drawing verbs, which this chapter never uses.
  • Chapter 13 is the full list of what this dialect does differently from BASIC 7.0.
  • Chapter 8 is the sprite reference, and Chapter 4 is the whole of GOSUB, TRAP and the loop forms.
  • Chapter 14 is the interpreter itself: the step loop, the pools and how to debug a program that stops for no visible reason.