`RGR(1)` and `RGR(2)` gave the window in pixels and nothing gave columns, rows or the cell size -- so anything placing a character *and* a sprite at the same spot had to hardcode a number measured by hand against whatever font the host loaded. The Breakout in examples/ does exactly that, `CW# = 16`, and it is the one thing in that listing that breaks on a different font or window. **`RWINDOW` is BASIC 7.0's own answer and had never been implemented here.** `RWINDOW(0)` is the current text window's rows and `RWINDOW(1)` its columns. `RWINDOW(2)` reports a C128's 40 or 80 column screen mode, and this interpreter has neither -- refused by name, because answering 0 would be a plausible lie, which is worse than a refusal that says why. The cell size in pixels is `RGR(3)` and `RGR(4)`, beside the surface's own dimensions rather than on `RWINDOW`. Two reasons: a cell size is a fact about the surface, and `RWINDOW` reports the *window*, so dividing `RGR(1)` by a column count stops being right the moment a program calls `WINDOW`. Both read a new optional `grid` entry point on `akbasic_TextSink` -- columns, rows, cell width, cell height -- implemented by the akgl sink and forwarded by the tee, in the shape `moveto` and `window` already had. NULL everywhere else, so both verbs refuse by name against a sink with no grid. `akbasic_sink_init_ stdio()` clears it for the same reason it now clears the other two. Measured on the standalone build: `RGR(3)` answers 16 and `RWINDOW` answers 50 columns by 37 rows -- the three numbers the Breakout listing had written out as constants -- and `RWINDOW` follows a `WINDOW` call while `RGR(3)` does not. tests/console_verbs.c drives the answers through a stand-in sink with a grid, since the harness sink is stdio and has none; tests/graphics_verbs.c covers the new `RGR` fields, their refusal, and the moved range bound. The `c excerpt=` block in docs/10-embedding.md moves with the header, which is `docs_examples` doing its job. TODO.md section 6 item 31's second half, struck. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
756 lines
23 KiB
Markdown
756 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 paints over the whole window on its
|
|
way past — so in the standalone SDL build **nothing you draw with them is ever visible**.
|
|
That is recorded as a defect (`TODO.md` §9 item 3) rather than a design, but it is the
|
|
interpreter you have.
|
|
|
|
So this game uses no drawing verb at all. Anything that has to move smoothly is a sprite;
|
|
anything that can live on a 16-pixel grid is text. Ball and paddle move; bricks, score
|
|
and messages do not.
|
|
|
|
**If you want artwork on the screen, that rule flips** and the whole architecture changes
|
|
with it. That is Chapter 18.
|
|
|
|
## Step 2: Measure the screen once, at the top
|
|
|
|
```basic norun
|
|
CW# = 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, and writing *past* the terminator of a short
|
|
row draws nothing at all. There is no way to poke one character into the middle of a row
|
|
and leave the rest alone.
|
|
|
|
So a row is rebuilt whole and written from column 0, in one `CHAR`:
|
|
|
|
```basic
|
|
DIM LAY$(6)
|
|
DIM BSG$(6)
|
|
BSG$(0) = "[##]"
|
|
BSG$(1) = "[##]"
|
|
BSG$(2) = "[==]"
|
|
BSG$(3) = "[==]"
|
|
BSG$(4) = "[--]"
|
|
BSG$(5) = "[--]"
|
|
LAY$(0) = "1111111111"
|
|
LAY$(1) = "1111111111"
|
|
LAY$(2) = "1101111011"
|
|
LAY$(3) = "1111111111"
|
|
LAY$(4) = "1011110111"
|
|
LAY$(5) = "0110110111"
|
|
S$ = ""
|
|
T$ = ""
|
|
R# = 0
|
|
C# = 0
|
|
FOR R# = 0 TO 5
|
|
GOSUB BUILDROW
|
|
PRINT S$
|
|
NEXT R#
|
|
END
|
|
|
|
LABEL BUILDROW
|
|
S$ = " "
|
|
FOR C# = 0 TO 9
|
|
T$ = MID(LAY$(R#), C#, 1)
|
|
IF T$ = "1" THEN S$ = S$ + BSG$(R#)
|
|
IF T$ = "0" THEN S$ = S$ + " "
|
|
NEXT C#
|
|
RETURN
|
|
```
|
|
|
|
```output
|
|
[##][##][##][##][##][##][##][##][##][##]
|
|
[##][##][##][##][##][##][##][##][##][##]
|
|
[==][==] [==][==][==][==] [==][==]
|
|
[==][==][==][==][==][==][==][==][==][==]
|
|
[--] [--][--][--][--] [--][--][--]
|
|
[--][--] [--][--] [--][--][--]
|
|
```
|
|
|
|
That block runs anywhere, with or without a graphics device, because `PRINT` and `CHAR`
|
|
are building the same string. In the game the last line is `CHAR 1, 0, R2#, S$` instead,
|
|
and the leading spaces are `" " * BLEFT#`.
|
|
|
|
**The rows are told apart by shape, not by colour.** `CHAR` parses a colour argument and
|
|
ignores it; the sink draws in one colour. `[##]`, `[==]` and `[--]` are what makes a
|
|
60-point row look different from a 10-point one.
|
|
|
|
Killing a brick clears its cell and redraws that one row:
|
|
|
|
```basic norun
|
|
LABEL KILLBR
|
|
BR#(BI#) = 0
|
|
LEFTN# = LEFTN# - 1
|
|
PTS# = (BROWS# - BR2#) * 10
|
|
SCORE# = SCORE# + PTS#
|
|
GOSUB DRAWBR
|
|
GOSUB DRAWHUD
|
|
IF LEFTN# < 1 THEN STATE# = 2
|
|
RETURN
|
|
```
|
|
|
|
`LEFTN#` is counted up when the wall is built and down when a brick goes, so "is the
|
|
level clear" is a comparison rather than a scan of sixty cells.
|
|
|
|
## Step 5: Make the ball and the paddle
|
|
|
|
`SPRSAV` takes an **integer array** of 63 bytes: three per row, twenty-one rows, most
|
|
significant bit leftmost. A clear bit is transparent — see
|
|
[Chapter 8](08-sprites.md#from-data).
|
|
|
|
Two patterns are all this game needs. The ball is an 8 by 8 disc in the *top-left corner*
|
|
of the pattern, so the sprite's position and its pixel rectangle are the same thing and
|
|
no offset arithmetic is needed anywhere. The paddle is a solid 24 by 6 bar with `SPRITE`'s
|
|
x-expand bit set, which makes it 48 wide; three of them laid end to end are one 144-pixel
|
|
paddle.
|
|
|
|
```basic requires=akgl screenshot=breakout-paddle size=240x120
|
|
DIM SB#(63)
|
|
DIM SP#(63)
|
|
I# = 0
|
|
D# = 0
|
|
FOR I# = 0 TO 62
|
|
SB#(I#) = 0
|
|
SP#(I#) = 0
|
|
NEXT I#
|
|
FOR I# = 0 TO 7
|
|
READ D#
|
|
SB#(I# * 3) = D#
|
|
NEXT I#
|
|
FOR I# = 0 TO 17
|
|
SP#(I#) = 255
|
|
NEXT I#
|
|
SPRSAV SB#, 1
|
|
SPRSAV SP#, 2
|
|
SPRSAV SP#, 3
|
|
SPRSAV SP#, 4
|
|
SPRITE 1, 1, 2
|
|
SPRITE 2, 1, 15, 0, 1, 0
|
|
SPRITE 3, 1, 15, 0, 1, 0
|
|
SPRITE 4, 1, 15, 0, 1, 0
|
|
MOVSPR 1, 108, 40
|
|
MOVSPR 2, 48, 80
|
|
MOVSPR 3, 96, 80
|
|
MOVSPR 4, 144, 80
|
|
|
|
DATA 60, 126, 255, 255, 255, 255, 126, 60
|
|
```
|
|
|
|

|
|
|
|
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 | Step 4 |
|
|
| Tell rows apart by shape, not colour | `CHAR` ignores its colour argument | Step 4 |
|
|
| Compute a `MOVSPR` coordinate into a variable first | A leading sign means *move by*, not *move to* | Step 5 |
|
|
| Loop the game with `GOTO`, not `DO` | 32 scopes, and jumping out of a loop leaks one | Step 6 |
|
|
| Parenthesise every mixed `+` and `-` | `a - b + c` parses as `a - (b + c)` | Step 7 |
|
|
| One `AND` or `OR` per expression unless parenthesised | The parser matches one | Step 7 |
|
|
| Keep a line under 32 tokens | The 33rd kills the interpreter with a stack trace, not a BASIC error you can `TRAP` | — |
|
|
| Span a loop across lines, and never `FOR I = 1 TO 1` | A whole loop on one line does not loop; equal bounds run zero times | [Ch 13](13-differences.md) |
|
|
| Probe for a device once and remember | An untrapped refusal ends the program | Step 11 |
|
|
|
|
## Where to go next
|
|
|
|
- **[Chapter 18](18-tutorial-breakout-artwork.md)** builds Breakout again with sprite
|
|
artwork, powerups and a coloured HUD. Every architectural decision comes out different,
|
|
and the chapter is about why.
|
|
- **[Chapter 13](13-differences.md)** is the full list of what this dialect does
|
|
differently from BASIC 7.0.
|
|
- **[Chapter 14](14-architecture.md)** is the interpreter itself — the step loop, the
|
|
pools and how to debug a program that stops for no visible reason.
|
|
- `TODO.md` §6 and §9 carry the defects this game found, each with a reduction that fits
|
|
on a screen and what a fix would touch.
|