Eleven of the thirteen defects these two games found are fixed, and the chapters that taught around them said things that are no longer true. Chapter 17: the drawing verbs are no longer invisible, they are covered by a text layer that `WINDOW` can now shrink; the cell size is `RGR(3)` and the grid is `RWINDOW`, not a hand-measured constant; a character written past a short row's end lands. Chapter 18: traps 3 and 4 -- the skipped block that broke its caller's `RETURN`, and `SSHAPE` ignoring a subscript -- become history rather than warnings, and Step 3 no longer claims a sprite is the *only* way to put a picture up, only the one that costs nothing. **Neither `.bas` listing changes, and both READMEs now say why.** The character game still writes `CW# = 16` and the artwork game still keeps six brick stamps in six scalars. What those listings are worth is being what a program written against those constraints looks like, with comments explaining what each one was avoiding -- rewriting them to pretend the problems never existed would throw that away. So Chapter 17 Step 2 shows the `RGR(3)`/`RWINDOW` form and says plainly that the listing beside it does not use it. That is the one place in either chapter where a quoted fragment is not verbatim from the game. Also: the closing pointers now say which entries are struck and which stand, and the budgets table in Chapter 18 notes that several of those budgets were tighter when the game was written. TODO.md section 9 item 3 is updated rather than struck: the `WINDOW` half is fixed, the default text area owning the whole window is not, and whether that default is right is a question rather than a defect. Verified against the fixed interpreter: both games run 45 seconds on the SDL frontend with no error line and the score climbing; both suites are green in both configurations; `docs_examples` passes and `docs_screenshots --check` re-renders all thirteen figures byte-identically; coverage is 95.0% of lines against the 90 gate, with src/variable.c at 100%; and every other quoted fragment still appears verbatim in the listing it came from. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
758 lines
23 KiB
Markdown
758 lines
23 KiB
Markdown
# 17. Tutorial: Breakout
|
|
|
|
This chapter builds a complete game — three lives, six rows of bricks, three level
|
|
layouts, a high score and an attract mode that plays itself — out of nothing but the
|
|
verbs in the earlier chapters. The finished listing is
|
|
[`examples/breakout/characters/breakout.bas`](../examples/breakout/characters/breakout.bas),
|
|
and it is worth having open beside this.
|
|
|
|
**The ball and the paddle are sprites; the bricks, the HUD and the messages are
|
|
characters in the text grid.** That split is the first decision and everything else
|
|
follows from it, so it is Step 1.
|
|
|
|
Run the finished game with the SDL build:
|
|
|
|
```sh norun
|
|
$ ./build-akgl/basic examples/breakout/characters/breakout.bas
|
|
```
|
|
|
|
| Key | Does |
|
|
|---|---|
|
|
| left / right | move the paddle |
|
|
| space | start a game, then launch the ball |
|
|
| P | pause |
|
|
| Q or escape | quit |
|
|
|
|
Chapter 18 builds the same game again out of sprite artwork, and takes a completely
|
|
different shape. Read this one first: it is the smaller of the two and it is where the
|
|
rules that constrain both are explained.
|
|
|
|
---
|
|
|
|
## Step 1: Decide what is a sprite and what is a character
|
|
|
|
Two layers of this interpreter are redrawn from state it keeps for you, every frame,
|
|
without your program doing anything:
|
|
|
|
- the **text grid**, repainted row by row by the akgl sink, and
|
|
- the **sprites**, walked slot by slot after it.
|
|
|
|
The drawing verbs are not one of them. `DRAW`, `BOX`, `CIRCLE` and `PAINT` go straight
|
|
into the renderer's back buffer, and **the text layer owns every row of the window by
|
|
default** — so it paints over anything they drew on its way past. `WINDOW` shrinks the
|
|
text area and gives the rest of the window to the drawing verbs, but a drawing still has
|
|
to be re-issued every frame and has to fit in one batch to survive the capture; see
|
|
[Chapter 13](13-differences.md#graphics).
|
|
|
|
So this game uses no drawing verb at all. Anything that has to move smoothly is a sprite;
|
|
anything that can live on a 16-pixel grid is text. Ball and paddle move; bricks, score
|
|
and messages do not.
|
|
|
|
**If you want artwork on the screen, that rule flips** and the whole architecture changes
|
|
with it. That is Chapter 18.
|
|
|
|
## Step 2: Measure the screen once, at the top
|
|
|
|
```basic norun
|
|
CW# = RGR(3)
|
|
CH# = RGR(4)
|
|
SCW# = RGR(1)
|
|
SCH# = RGR(2)
|
|
COLS# = RWINDOW(1)
|
|
ROWS# = RWINDOW(0)
|
|
```
|
|
|
|
Six numbers, none of them assumed. `RGR(1)` and `RGR(2)` are the window in pixels,
|
|
`RGR(3)` and `RGR(4)` are one character cell, and `RWINDOW` gives the text grid in
|
|
columns and rows — see [Chapter 6](06-graphics.md#rgr). Everything below is derived from
|
|
these, including the ball's speed relative to the wall, so the game fits whatever window
|
|
the host opened and whatever font it loaded.
|
|
|
|
**The listing in `examples/` still has `CW# = 16` written out**, measured by hand against
|
|
the bundled C64_Pro_Mono at 16 points, because when it was written a program had no way
|
|
to ask — and it was the one thing in it that would break on a different font. That is
|
|
what `RGR(3)` and `RWINDOW` are for; `TODO.md` §6 item 31 is the history.
|
|
|
|
Lay the rest of the geometry out in the same block — the wall in cells, the play area in
|
|
pixels:
|
|
|
|
```basic norun
|
|
BRW# = 4
|
|
BCOLS# = 10
|
|
BROWS# = 6
|
|
BTOP# = 4
|
|
BLEFT# = (COLS# - (BRW# * BCOLS#)) / 2
|
|
TOPY# = 2 * CH#
|
|
MAXX# = SCW# - 8
|
|
PW# = 144
|
|
PY# = 528
|
|
```
|
|
|
|
A brick is four cells wide, so ten of them are forty cells, and `BLEFT#` centres that in
|
|
whatever `COLS#` turned out to be. Integer division truncates here, which is exactly what
|
|
a cell index wants.
|
|
|
|
## Step 3: Declare the names a subroutine has to answer through
|
|
|
|
A `GOSUB` gets its own scope. Assignment walks up the chain and finds an outer name, but a
|
|
name **first seen** inside a subroutine is created in that subroutine and dies at
|
|
`RETURN` — so a routine cannot answer its caller through a name it invented itself.
|
|
|
|
That is why the game declares this block before anything calls anything:
|
|
|
|
```basic norun
|
|
DIM BR#(60)
|
|
DIM LAY$(6)
|
|
DIM BSG$(6)
|
|
SCORE# = 0
|
|
LIVES# = 3
|
|
BX# = 0
|
|
BY# = 0
|
|
HIT# = 0
|
|
BI# = 0
|
|
```
|
|
|
|
`HIT#` and `BI#` are how `HITTEST` (Step 8) answers the routine that called it. Declared
|
|
out here they are one variable the whole program shares; left to `HITTEST`, they would be
|
|
two that vanish at `RETURN` and the caller would read zeros for ever, with no diagnostic
|
|
of any kind. [Chapter 4](04-control-flow.md#goto-and-gosub) has the scope rules.
|
|
|
|
**Creating a scalar inside a scope is free**, so nothing else has to be declared up front:
|
|
|
|
```basic
|
|
X# = 0
|
|
FOR T# = 1 TO 20000
|
|
GOSUB SUBA
|
|
NEXT T#
|
|
PRINT "OK " + X#
|
|
END
|
|
|
|
LABEL SUBA
|
|
LOC# = 1
|
|
X# = X# + LOC#
|
|
RETURN
|
|
```
|
|
|
|
```output
|
|
OK 20000
|
|
```
|
|
|
|
Twenty thousand calls, each creating a local. **This did not use to work.** A scalar drew
|
|
storage from the same 4096-slot pool an array does, scope exit gave back the variable but
|
|
not its storage, and a program creating one name per tick died in about half a minute —
|
|
naming whichever line was unlucky rather than the line that was wrong. It is what killed
|
|
the first version of this game after twenty-five seconds. `TODO.md` §6 item 30 has the
|
|
history; `tests/value_pool.c` is what keeps it fixed.
|
|
|
|
**An array is different, and still costs slots for good.** `DIM` inside a subroutine takes
|
|
pool storage that never comes back, because a pointer into a record can outlive the scope
|
|
that declared it — see [Chapter 16](16-structures.md#limits):
|
|
|
|
```basic
|
|
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
|
|
```
|
|
|
|
```output
|
|
? 8 : RUNTIME ERROR Array of 4 elements does not fit in the 0 remaining value slots
|
|
|
|
```
|
|
|
|
So the rule that survives is narrow: **`DIM` at the top, not in a loop.** Which is where
|
|
you would have put it anyway.
|
|
|
|
## Step 4: Build the wall, and write a row whole
|
|
|
|
Levels are `DATA`, one string per row, `1` for a brick and `0` for a hole:
|
|
|
|
```basic norun
|
|
LABEL LAY1
|
|
DATA "1111111111"
|
|
DATA "1111111111"
|
|
DATA "1111111111"
|
|
DATA "1111111111"
|
|
DATA "1111111111"
|
|
DATA "1111111111"
|
|
|
|
LABEL LAY2
|
|
DATA "0011111100"
|
|
DATA "0111111110"
|
|
DATA "1111111111"
|
|
DATA "1111111111"
|
|
DATA "0111111110"
|
|
DATA "0011111100"
|
|
```
|
|
|
|
`RESTORE` takes a label — it wants a line number and a label evaluates to one — so a
|
|
layout is just a named place in the `DATA` stream, and adding a fourth level is a block
|
|
and one `IF`:
|
|
|
|
```basic norun
|
|
LABEL LOADLAY
|
|
N# = MOD((LEVEL# - 1), 3)
|
|
IF N# = 0 THEN RESTORE LAY1
|
|
IF N# = 1 THEN RESTORE LAY2
|
|
IF N# = 2 THEN RESTORE LAY3
|
|
FOR R# = 0 TO 5
|
|
READ LAY$(R#)
|
|
NEXT R#
|
|
RETURN
|
|
```
|
|
|
|
Now draw it — and here is the second rule that shapes this listing:
|
|
|
|
**`CHAR` terminates the row where it stops.** A row of the grid is a C string, so writing
|
|
at column N erases everything from N+1 on. Writing *past* the end of a short row is fine
|
|
— the gap fills with spaces — but there is still no way to poke one character into the
|
|
middle of a row and leave the rest of it alone.
|
|
|
|
So a row is rebuilt whole and written from column 0, in one `CHAR`:
|
|
|
|
```basic
|
|
DIM LAY$(6)
|
|
DIM BSG$(6)
|
|
BSG$(0) = "[##]"
|
|
BSG$(1) = "[##]"
|
|
BSG$(2) = "[==]"
|
|
BSG$(3) = "[==]"
|
|
BSG$(4) = "[--]"
|
|
BSG$(5) = "[--]"
|
|
LAY$(0) = "1111111111"
|
|
LAY$(1) = "1111111111"
|
|
LAY$(2) = "1101111011"
|
|
LAY$(3) = "1111111111"
|
|
LAY$(4) = "1011110111"
|
|
LAY$(5) = "0110110111"
|
|
S$ = ""
|
|
T$ = ""
|
|
R# = 0
|
|
C# = 0
|
|
FOR R# = 0 TO 5
|
|
GOSUB BUILDROW
|
|
PRINT S$
|
|
NEXT R#
|
|
END
|
|
|
|
LABEL BUILDROW
|
|
S$ = " "
|
|
FOR C# = 0 TO 9
|
|
T$ = MID(LAY$(R#), C#, 1)
|
|
IF T$ = "1" THEN S$ = S$ + BSG$(R#)
|
|
IF T$ = "0" THEN S$ = S$ + " "
|
|
NEXT C#
|
|
RETURN
|
|
```
|
|
|
|
```output
|
|
[##][##][##][##][##][##][##][##][##][##]
|
|
[##][##][##][##][##][##][##][##][##][##]
|
|
[==][==] [==][==][==][==] [==][==]
|
|
[==][==][==][==][==][==][==][==][==][==]
|
|
[--] [--][--][--][--] [--][--][--]
|
|
[--][--] [--][--] [--][--][--]
|
|
```
|
|
|
|
That block runs anywhere, with or without a graphics device, because `PRINT` and `CHAR`
|
|
are building the same string. In the game the last line is `CHAR 1, 0, R2#, S$` instead,
|
|
and the leading spaces are `" " * BLEFT#`.
|
|
|
|
**The rows are told apart by shape, not by colour.** `CHAR` parses a colour argument and
|
|
ignores it; the sink draws in one colour. `[##]`, `[==]` and `[--]` are what makes a
|
|
60-point row look different from a 10-point one.
|
|
|
|
Killing a brick clears its cell and redraws that one row:
|
|
|
|
```basic norun
|
|
LABEL KILLBR
|
|
BR#(BI#) = 0
|
|
LEFTN# = LEFTN# - 1
|
|
PTS# = (BROWS# - BR2#) * 10
|
|
SCORE# = SCORE# + PTS#
|
|
GOSUB DRAWBR
|
|
GOSUB DRAWHUD
|
|
IF LEFTN# < 1 THEN STATE# = 2
|
|
RETURN
|
|
```
|
|
|
|
`LEFTN#` is counted up when the wall is built and down when a brick goes, so "is the
|
|
level clear" is a comparison rather than a scan of sixty cells.
|
|
|
|
## Step 5: Make the ball and the paddle
|
|
|
|
`SPRSAV` takes an **integer array** of 63 bytes: three per row, twenty-one rows, most
|
|
significant bit leftmost. A clear bit is transparent — see
|
|
[Chapter 8](08-sprites.md#from-data).
|
|
|
|
Two patterns are all this game needs. The ball is an 8 by 8 disc in the *top-left corner*
|
|
of the pattern, so the sprite's position and its pixel rectangle are the same thing and
|
|
no offset arithmetic is needed anywhere. The paddle is a solid 24 by 6 bar with `SPRITE`'s
|
|
x-expand bit set, which makes it 48 wide; three of them laid end to end are one 144-pixel
|
|
paddle.
|
|
|
|
```basic requires=akgl screenshot=breakout-paddle size=240x120
|
|
DIM SB#(63)
|
|
DIM SP#(63)
|
|
I# = 0
|
|
D# = 0
|
|
FOR I# = 0 TO 62
|
|
SB#(I#) = 0
|
|
SP#(I#) = 0
|
|
NEXT I#
|
|
FOR I# = 0 TO 7
|
|
READ D#
|
|
SB#(I# * 3) = D#
|
|
NEXT I#
|
|
FOR I# = 0 TO 17
|
|
SP#(I#) = 255
|
|
NEXT I#
|
|
SPRSAV SB#, 1
|
|
SPRSAV SP#, 2
|
|
SPRSAV SP#, 3
|
|
SPRSAV SP#, 4
|
|
SPRITE 1, 1, 2
|
|
SPRITE 2, 1, 15, 0, 1, 0
|
|
SPRITE 3, 1, 15, 0, 1, 0
|
|
SPRITE 4, 1, 15, 0, 1, 0
|
|
MOVSPR 1, 108, 40
|
|
MOVSPR 2, 48, 80
|
|
MOVSPR 3, 96, 80
|
|
MOVSPR 4, 144, 80
|
|
|
|
DATA 60, 126, 255, 255, 255, 255, 126, 60
|
|
```
|
|
|
|

|
|
|
|
The three segments meet with no seam, which is the whole point of the arrangement. The
|
|
game writes the same 63 bytes out as `DATA` a row at a time, with the bit pattern drawn
|
|
in a comment beside it; the loops above are the same two patterns spelled shorter so the
|
|
figure fits on a page.
|
|
|
|
Moving them is four `MOVSPR`s, and each coordinate is worked out into a variable first:
|
|
|
|
```basic norun
|
|
LABEL SHOWSPR
|
|
MOVSPR 1, BX#, BY#
|
|
MOVSPR 2, PX#, PY#
|
|
X2# = PX# + 48
|
|
MOVSPR 3, X2#, PY#
|
|
X3# = PX# + 96
|
|
MOVSPR 4, X3#, PY#
|
|
RETURN
|
|
```
|
|
|
|
**Do not write `MOVSPR 3, PX# + 48, PY#`.** `MOVSPR` reads a leading sign as *move by this
|
|
much* rather than *move to here* — see [Chapter 8](08-sprites.md#showing-and-moving) — and
|
|
an expression that starts with one is the relative form. Computing into `X2#` first is
|
|
unambiguous.
|
|
|
|
Sprites are positioned by their top-left corner in **device pixels**, so ball, paddle and
|
|
walls are all plain pixel arithmetic. Only the bricks are in cells, and exactly one
|
|
routine converts between the two (Step 8).
|
|
|
|
## Step 6: Write the main loop as a `GOTO` loop
|
|
|
|
```basic norun
|
|
LABEL TICK
|
|
GOSUB READKEY
|
|
IF STATE# <> 0 THEN GOTO DISPATCH
|
|
IF PAUSED# = 1 THEN GOTO TICKEND
|
|
GOSUB MOVEPAD
|
|
IF HELD# = 1 THEN GOSUB HOLDBAL
|
|
IF HELD# = 0 THEN GOSUB MOVEBAL
|
|
GOSUB SHOWSPR
|
|
LABEL TICKEND
|
|
SLEEP 0.02
|
|
GOTO TICK
|
|
|
|
LABEL DISPATCH
|
|
IF STATE# = 1 THEN GOTO LOSTLIF
|
|
IF STATE# = 2 THEN GOTO LEVELUP
|
|
IF STATE# = 3 THEN GOTO BYE
|
|
IF STATE# = 5 THEN GOTO NEWGAME
|
|
STATE# = 0
|
|
GOTO TICK
|
|
```
|
|
|
|
**`DO ... LOOP` would be the natural way to write this and it is the wrong one.** Every
|
|
loop and every `GOSUB` pushes one of 32 scopes, and a state change that jumped out of a
|
|
`DO` — losing a life, clearing a level — would leak its scope every single time. Thirty-two
|
|
lives later the program stops. A `GOTO` loop pushes nothing at all, so the deepest chain
|
|
in play is the six `GOSUB`s of a brick hit.
|
|
|
|
Nothing here `GOSUB`s into the state changes either. They are branch targets that run and
|
|
then `GOTO TICK`, so the stack is empty again by the time the next frame starts.
|
|
|
|
`SLEEP 0.02` is the frame pacing, and **it does not block**: it records a deadline and
|
|
the step loop declines to advance the program until the clock reaches it, so the host
|
|
goes on drawing frames and pumping the keyboard the whole time. Fifty ticks a second is
|
|
what that asks for. Chapter 18 needs a much sharper instrument than this one and builds
|
|
it out of `TI#`.
|
|
|
|
## Step 7: Read the keyboard through a countdown
|
|
|
|
`GET` is the only non-blocking read there is. There is **no key-up event and no held-key
|
|
state** — what you get instead is the host's own key repeat arriving as ordinary key-down
|
|
events, about one a tick while a key is held down.
|
|
|
|
So drain the queue every tick, and let a keystroke refill a countdown:
|
|
|
|
```basic norun
|
|
LABEL READKEY
|
|
FOR KI# = 1 TO 8
|
|
GET K#
|
|
IF K# <> 0 THEN GOSUB HANDKEY
|
|
NEXT KI#
|
|
RETURN
|
|
|
|
LABEL HANDKEY
|
|
IF K# = 1073741904 THEN PDIR# = 0 - 1
|
|
IF K# = 1073741904 THEN PDEC# = 12
|
|
IF K# = 1073741903 THEN PDIR# = 1
|
|
IF K# = 1073741903 THEN PDEC# = 12
|
|
IF K# = 32 THEN GOSUB KEYSPC
|
|
IF K# = 112 THEN GOSUB KEYPAU
|
|
IF K# = 113 THEN STATE# = 3
|
|
IF K# = 27 THEN STATE# = 3
|
|
RETURN
|
|
```
|
|
|
|
```basic norun
|
|
LABEL MOVEPAD
|
|
IF PDEC# < 1 THEN GOTO PADCLMP
|
|
PDEC# = PDEC# - 1
|
|
PX# = PX# + (PDIR# * PSPD#)
|
|
LABEL PADCLMP
|
|
IF PX# < 0 THEN PX# = 0
|
|
M# = SCW# - PW#
|
|
IF PX# > M# THEN PX# = M#
|
|
RETURN
|
|
```
|
|
|
|
The paddle coasts to a stop over twelve ticks when the repeats stop, so **held reads as
|
|
held and tapped reads as a nudge** — with no held-key state anywhere. 1073741903 and
|
|
1073741904 are the right and left arrows; the codes are the host's, and Chapter 12's
|
|
`GET` entry says where they come from.
|
|
|
|
Note `0 - 1` rather than `-1`. Unary minus on a literal is fine, but the habit of writing
|
|
the subtraction out is worth forming here: this parser reads `a - b + c` as `a - (b + c)`,
|
|
so every mixed `+` and `-` in the game is parenthesised as a rule rather than where it
|
|
happens to matter. It also matches only **one** unparenthesised `AND` or `OR` per
|
|
expression. Both are in [Chapter 13](13-differences.md).
|
|
|
|
## Step 8: Move the ball one axis at a time
|
|
|
|
```basic norun
|
|
LABEL MOVEBAL
|
|
OX# = BX#
|
|
BX# = BX# + BVX#
|
|
IF BX# < 0 THEN GOSUB WALLL
|
|
IF BX# > MAXX# THEN GOSUB WALLR
|
|
GOSUB XBRICK
|
|
OY# = BY#
|
|
BY# = BY# + BVY#
|
|
IF BY# < TOPY# THEN GOSUB WALLT
|
|
GOSUB YBRICK
|
|
GOSUB PADHIT
|
|
IF BY# > LOSEY# THEN STATE# = 1
|
|
RETURN
|
|
```
|
|
|
|
X first and then Y, **each move tested on its own**, so a ball arriving at the corner of
|
|
a brick reverses one axis rather than both. `OX#` and `OY#` remember where it came from,
|
|
which is what a collision backs it out to.
|
|
|
|
The brick test converts a pixel to a cell, and it is the only place in the program that
|
|
does:
|
|
|
|
```basic norun
|
|
LABEL HITTEST
|
|
HIT# = 0
|
|
RC# = TY# / CH#
|
|
IF RC# < BTOP# THEN RETURN
|
|
RB# = RC# - BTOP#
|
|
IF RB# > (BROWS# - 1) THEN RETURN
|
|
CC2# = TX# / CW#
|
|
IF CC2# < BLEFT# THEN RETURN
|
|
CB# = (CC2# - BLEFT#) / BRW#
|
|
IF CB# > (BCOLS# - 1) THEN RETURN
|
|
BI# = (RB# * BCOLS#) + CB#
|
|
BR2# = RB#
|
|
IF BR#(BI#) = 1 THEN HIT# = 1
|
|
RETURN
|
|
```
|
|
|
|
Callers hand it a point in `TX#`/`TY#` and read `HIT#`, `BI#` and `BR2#` back out — which
|
|
works only because Step 3 created all three outside the routine.
|
|
|
|
**Test the ball's leading edge, not its centre:**
|
|
|
|
```basic norun
|
|
LABEL XBRICK
|
|
TX# = BX#
|
|
IF BVX# > 0 THEN TX# = BX# + 7
|
|
TY# = BY# + 4
|
|
GOSUB HITTEST
|
|
IF HIT# = 0 THEN RETURN
|
|
BX# = OX#
|
|
BVX# = 0 - BVX#
|
|
GOSUB KILLBR
|
|
RETURN
|
|
```
|
|
|
|
A ball tested by its centre sinks four pixels into a brick before it turns, and it looks
|
|
like the brick was hit late. The cost of the leading edge is that a brick clipped at the
|
|
very corner can be missed by up to seven pixels' worth of ball; that is the better trade
|
|
of the two, and it is worth knowing which one you took.
|
|
|
|
## Step 9: Make the paddle bounce mean something
|
|
|
|
```basic norun
|
|
LABEL PADHIT
|
|
IF BVY# < 1 THEN RETURN
|
|
BB# = BY# + 8
|
|
IF BB# < PY# THEN RETURN
|
|
IF BY# > (PY# + 10) THEN RETURN
|
|
RX# = PX# + PW#
|
|
IF (BX# + 8) < PX# THEN RETURN
|
|
IF BX# > RX# THEN RETURN
|
|
BY# = PY# - 8
|
|
BVY# = 0 - BSPD#
|
|
Z# = ((BX# + 4) - PX#) / (PW# / 5)
|
|
IF Z# < 0 THEN Z# = 0
|
|
IF Z# > 4 THEN Z# = 4
|
|
PVX# = BVX#
|
|
BVX# = (Z# - 2) * 3
|
|
IF BVX# <> 0 THEN GOTO PADAIM
|
|
BVX# = 2
|
|
IF PVX# < 0 THEN BVX# = 0 - 2
|
|
LABEL PADAIM
|
|
GOSUB BEEPP
|
|
RETURN
|
|
```
|
|
|
|
Five zones across the face and the zone decides the angle: `Z#` is 0 to 4, so the new
|
|
horizontal speed is -6, -3, 0, +3, +6. Where you catch it decides where it goes, which is
|
|
the entire game.
|
|
|
|
The dead-centre case needs its own handling. A ball with **no** sideways speed rises and
|
|
falls down one column for ever, which is how attract mode used to hang itself — so the
|
|
middle zone keeps the direction the ball came in on and gives it a shallow angle instead
|
|
of zero.
|
|
|
|
There is a watchdog for the same failure in a subtler form. Ten seconds without touching
|
|
a brick means the ball has found an orbit that misses the wall, so it gets a new angle —
|
|
**at the next paddle bounce, never in mid-air.** Changing it in flight looks exactly like
|
|
the ball hitting something that is not there:
|
|
|
|
```basic norun
|
|
LABEL UNSTICK
|
|
NUDGE# = 0
|
|
STALL# = 0
|
|
RMAX# = 4
|
|
GOSUB RANDOM
|
|
BVX# = (RND# * 3) - 6
|
|
IF BVX# = 0 THEN BVX# = 3
|
|
RETURN
|
|
```
|
|
|
|
`RANDOM` is a linear congruential generator, because **this dialect has no `RND`** — and
|
|
no `INT`, `SQR`, `ASC` or `TIMER` either. `TI#` is jiffies off the host's clock and is
|
|
uptime rather than zero-based, which makes it a fine seed:
|
|
|
|
```basic norun
|
|
LABEL RANDOM
|
|
SEED# = MOD(((SEED# * 1103515245) + 12345), 2147483648)
|
|
RND# = MOD((SEED# / 65536), RMAX#)
|
|
RETURN
|
|
```
|
|
|
|
The multiply stays inside a 64-bit integer for any seed under 2^31, and the answer comes
|
|
from the middle bits because the low bits of a power-of-two modulus barely move.
|
|
Integer division truncates for free, which is the `INT` you do not have.
|
|
|
|
## Step 10: The HUD and the messages
|
|
|
|
Same rule as the wall: build the whole line, write it once.
|
|
|
|
```basic
|
|
V# = 0
|
|
P$ = ""
|
|
H$ = ""
|
|
SCORE# = 1250
|
|
LIVES# = 3
|
|
LEVEL# = 2
|
|
HIGH# = 9900
|
|
V# = SCORE#
|
|
GOSUB PAD6
|
|
H$ = " SCORE " + P$
|
|
H$ = (H$ + " LIVES ") + LIVES#
|
|
H$ = (H$ + " LEVEL ") + LEVEL#
|
|
V# = HIGH#
|
|
GOSUB PAD6
|
|
H$ = (H$ + " HIGH ") + P$
|
|
PRINT H$
|
|
END
|
|
|
|
LABEL PAD6
|
|
P$ = "" + V#
|
|
DO WHILE LEN(P$) < 6
|
|
P$ = "0" + P$
|
|
LOOP
|
|
RETURN
|
|
```
|
|
|
|
```output
|
|
SCORE 001250 LIVES 3 LEVEL 2 HIGH 009900
|
|
```
|
|
|
|
`"" + V#` is how a number becomes a string: `+` concatenates a string with a number, and
|
|
that is the conversion — see [Chapter 5](05-strings-and-formatting.md). Every `+` after
|
|
the first is parenthesised, for the reason in Step 7.
|
|
|
|
A centred message is the same idea, and needs no padding on the right because `CHAR`
|
|
terminating the row is what erases the rest of it:
|
|
|
|
```basic norun
|
|
LABEL SHOWAT
|
|
L# = LEN(MSG$)
|
|
C2# = (COLS# - L#) / 2
|
|
S$ = MSG$
|
|
IF C2# > 0 THEN S$ = (" " * C2#) + MSG$
|
|
CHAR 1, 0, MROW#, S$
|
|
RETURN
|
|
|
|
LABEL CLRAT
|
|
CHAR 1, 0, MROW#, " "
|
|
RETURN
|
|
```
|
|
|
|
Clearing a message is a single space written at column 0 — one character, and the
|
|
terminator behind it takes the rest of the row with it.
|
|
|
|
## Step 11: Ask for sound once, then believe the answer
|
|
|
|
`SOUND` refuses on a machine with no audio device, and an untrapped refusal ends the
|
|
game. Ask once at startup, record the answer, and never ask again:
|
|
|
|
```basic requires=noakgl
|
|
SND# = 1
|
|
TRAP NOAUDIO
|
|
SOUND 1, 2000, 1
|
|
TRAP
|
|
IF SND# = 1 THEN PRINT "SOUND IS AVAILABLE"
|
|
IF SND# = 0 THEN PRINT "NO AUDIO DEVICE, PLAYING SILENTLY"
|
|
END
|
|
|
|
LABEL NOAUDIO
|
|
SND# = 0
|
|
RESUME NEXT
|
|
```
|
|
|
|
```output
|
|
NO AUDIO DEVICE, PLAYING SILENTLY
|
|
```
|
|
|
|
That output is the stdio build, which has no devices at all. On the SDL build the same
|
|
program prints the other line. `TRAP` with no argument disarms the handler again — leave
|
|
it armed and the next refusal anywhere in the game is silently swallowed. See
|
|
[Chapter 4](04-control-flow.md#trapping-errors).
|
|
|
|
After that every sound is guarded by the flag it recorded:
|
|
|
|
```basic norun
|
|
LABEL BEEPB
|
|
IF SND# = 1 THEN SOUND 1, 12000, 2
|
|
RETURN
|
|
```
|
|
|
|
**`SOUND`'s frequency argument is a SID register value, not hertz** — the pitch is
|
|
`register * 1022730 / 16777216`, and [Chapter 7](07-sound.md#sound) has the arithmetic.
|
|
Work the notes out once and write them down in a comment beside the numbers.
|
|
|
|
## Step 12: Give it an attract mode, and test with it
|
|
|
|
The title screen plays itself, and this is not decoration — it is how the game gets
|
|
tested without a hand on the keyboard. `DEMO#` is the only difference between the demo
|
|
and a real game:
|
|
|
|
```basic norun
|
|
LABEL DEMOPAD
|
|
TX# = (BX# + 4) - (PW# / 2)
|
|
TX# = TX# + DOFF#
|
|
D# = TX# - PX#
|
|
IF D# > PSPD# THEN D# = PSPD#
|
|
M# = 0 - PSPD#
|
|
IF D# < M# THEN D# = M#
|
|
PX# = PX# + D#
|
|
RETURN
|
|
|
|
LABEL DEMOAIM
|
|
RMAX# = 81
|
|
GOSUB RANDOM
|
|
DOFF# = RND# - 40
|
|
RETURN
|
|
```
|
|
|
|
The demo paddle tracks the ball but **aims off-centre by a random amount**, refreshed at
|
|
every bounce, so it plays like something with a hand on it and occasionally misses. A
|
|
demo that tracks perfectly never loses a ball and therefore never tests losing one.
|
|
|
|
Leave it running for two minutes and watch the score climb: that exercises the ball, the
|
|
bricks, the level change and the speed-up — and it is the only thing that exercises them
|
|
*together*, for long enough. Every defect this game found took thousands of ticks to show
|
|
itself, and a green test suite proved nothing about any of them.
|
|
|
|
The machine does not get on the scoreboard. Attract mode keeps score so the wall behaves,
|
|
but only a player's score is ever the high one:
|
|
|
|
```basic norun
|
|
LABEL HISCORE
|
|
IF SCORE# > HIGH# THEN HIGH# = SCORE#
|
|
RETURN
|
|
```
|
|
|
|
There is nowhere to save it. There is no disk in this interpreter — see
|
|
[Chapter 9](09-files-and-disk.md) — so the high score lasts as long as the process does.
|
|
|
|
## The rules this listing never breaks
|
|
|
|
Every one of these was learned by breaking it. They are the checklist to write your own
|
|
game against:
|
|
|
|
| Rule | Because | Where |
|
|
|---|---|---|
|
|
| `DIM` at the top, never in a loop | An array created inside a scope costs pool slots for good; 4096 ends the run. A scalar is free | Step 3 |
|
|
| Build a text row whole and write it from column 0 | `CHAR` terminates the row where it stops, so a second write erases the tail of the first | Step 4 |
|
|
| Tell rows apart by shape, not colour | `CHAR` ignores its colour argument | Step 4 |
|
|
| Compute a `MOVSPR` coordinate into a variable first | A leading sign means *move by*, not *move to* | Step 5 |
|
|
| Loop the game with `GOTO`, not `DO` | 32 scopes, and jumping out of a loop leaks one | Step 6 |
|
|
| Parenthesise every mixed `+` and `-` | `a - b + c` parses as `a - (b + c)` | Step 7 |
|
|
| One `AND` or `OR` per expression unless parenthesised | The parser matches one | Step 7 |
|
|
| Keep a line under 32 tokens | The 33rd kills the interpreter with a stack trace, not a BASIC error you can `TRAP` | — |
|
|
| Span a loop across lines, and never `FOR I = 1 TO 1` | A whole loop on one line does not loop; equal bounds run zero times | [Ch 13](13-differences.md) |
|
|
| Probe for a device once and remember | An untrapped refusal ends the program | Step 11 |
|
|
|
|
## Where to go next
|
|
|
|
- **[Chapter 18](18-tutorial-breakout-artwork.md)** builds Breakout again with sprite
|
|
artwork, powerups and a coloured HUD. Every architectural decision comes out different,
|
|
and the chapter is about why.
|
|
- **[Chapter 13](13-differences.md)** is the full list of what this dialect does
|
|
differently from BASIC 7.0.
|
|
- **[Chapter 14](14-architecture.md)** is the interpreter itself — the step loop, the
|
|
pools and how to debug a program that stops for no visible reason.
|
|
- `TODO.md` §6 and §9 carry the thirteen defects these two games found, with the
|
|
reduction for each. Eleven have since been fixed and are struck; the entries are kept
|
|
because the reduction and the cause are worth more than the tidiness of deleting them.
|