Add two Breakout examples and the tutorials that build them
Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m10s
akbasic CI Build / sanitizers (push) Failing after 4m5s
akbasic CI Build / coverage (push) Failing after 3m29s
akbasic CI Build / akgl_build (push) Failing after 21s
akbasic CI Build / mutation_test (push) Failing after 3m19s
Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m10s
akbasic CI Build / sanitizers (push) Failing after 4m5s
akbasic CI Build / coverage (push) Failing after 3m29s
akbasic CI Build / akgl_build (push) Failing after 21s
akbasic CI Build / mutation_test (push) Failing after 3m19s
Two complete games in `examples/breakout/`, both 100% BASIC: `characters/` draws its wall in the text grid with two `DATA` sprites for the ball and paddle, and `sprites/` loads CC0 artwork and captures its whole screen with `SSHAPE`/`SPRSAV`. They take opposite shapes for reasons that are entirely this interpreter's, which is what the chapters are for. `docs/17-tutorial-breakout.md` and `docs/18-tutorial-breakout-artwork.md` build each one a step at a time, and end in a checklist of the rules a real program runs into: create every name before the loop starts, write a text row whole, loop with `GOTO` rather than `DO`, put the float on the left. Every trap is a runnable block with its own output rather than a claim -- the value pool dying at four thousand names, the skipped `BEGIN` block that breaks its caller's `RETURN`, `SSHAPE` ignoring a subscript, `READ`'s single cursor. Five figures, generated from the listings beside them by `docs_screenshots`, and a `breakout_art` setup so the ones that load artwork load the example's own. The character game's wall cannot be photographed -- the screenshot host omits the text layer on purpose -- so it is shown as compared output instead. `docs/07-sound.md` never said `SOUND`'s frequency is a SID register value rather than hertz, which both games depend on. It says so now, with the conversion from `src/audio_tables.c:84`. `TODO.md` gains the thirteen defects the two games turned up -- §6 items 30 to 33 and all of §9 -- each with a reduction that fits on a screen, the file and line of the cause, and what a fix would touch. Verified: `docs_examples` passes in both build configurations, `docs_screenshots --check` re-renders all thirteen figures and byte-compares them, the full 109-test suite passes in both builds, every quoted fragment was checked to appear verbatim in the listing it came from, and every relative link and anchor resolves. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
763
docs/17-tutorial-breakout.md
Normal file
763
docs/17-tutorial-breakout.md
Normal file
@@ -0,0 +1,763 @@
|
||||
# 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# = 16
|
||||
CH# = 16
|
||||
SCW# = RGR(1)
|
||||
SCH# = RGR(2)
|
||||
COLS# = SCW# / CW#
|
||||
ROWS# = SCH# / CH#
|
||||
```
|
||||
|
||||
`RGR(1)` and `RGR(2)` are the window in pixels — see
|
||||
[Chapter 6](06-graphics.md#rgr) — and the game asks for them rather than assuming 800 by
|
||||
600, so it survives a host that opens a different window.
|
||||
|
||||
**The cell size is a constant and has to be**, which is the one thing here you cannot
|
||||
derive. `WINDOW` knows the grid, but the standalone frontend's tee sink never offers it
|
||||
(`TODO.md` §9 item 3 again), so a program has no way to ask. 16 by 16 is the bundled
|
||||
C64_Pro_Mono at 16 points. Change the font or the point size and change these two with
|
||||
it; everything below is derived from them, including the ball's speed relative to the
|
||||
wall.
|
||||
|
||||
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: Create every name before the game starts
|
||||
|
||||
This is the rule that decides whether your game runs for four minutes or for ever, and it
|
||||
is worth getting straight before you write a single subroutine.
|
||||
|
||||
**A name that is created costs value-pool slots that are never handed back.** The pool is
|
||||
a bump allocator with 4096 slots for the life of the run. Scope exit *does* destroy
|
||||
variables — a `GOSUB`'s locals go when it returns — but their slots are not reclaimed, so
|
||||
recreating the same local on the next call takes fresh ones.
|
||||
|
||||
Here is the whole defect on one screen:
|
||||
|
||||
```basic
|
||||
X# = 0
|
||||
FOR T# = 1 TO 6000
|
||||
GOSUB SUBA
|
||||
NEXT T#
|
||||
PRINT "OK " + X#
|
||||
END
|
||||
|
||||
LABEL SUBA
|
||||
LOC# = 1
|
||||
X# = X# + LOC#
|
||||
RETURN
|
||||
```
|
||||
|
||||
```output
|
||||
? 8 : RUNTIME ERROR Array of 1 elements does not fit in the 0 remaining value slots
|
||||
|
||||
```
|
||||
|
||||
Six thousand calls, one new name each, and it dies on the four-thousand-and-somethingth
|
||||
— naming the line that was unlucky rather than the line that was wrong. A game loop that
|
||||
creates one name per tick is dead in half a minute. This one was, and it took twenty-five
|
||||
seconds to do it.
|
||||
|
||||
The fix is one line, moved:
|
||||
|
||||
```basic
|
||||
X# = 0
|
||||
LOC# = 0
|
||||
FOR T# = 1 TO 6000
|
||||
GOSUB SUBA
|
||||
NEXT T#
|
||||
PRINT "OK " + X#
|
||||
END
|
||||
|
||||
LABEL SUBA
|
||||
LOC# = 1
|
||||
X# = X# + LOC#
|
||||
RETURN
|
||||
```
|
||||
|
||||
```output
|
||||
OK 6000
|
||||
```
|
||||
|
||||
So **declare everything at the top** — variables, scratch temporaries, loop counters, all
|
||||
of it — and after that the program only ever stores into names that already exist:
|
||||
|
||||
```basic norun
|
||||
DIM BR#(60)
|
||||
DIM LAY$(6)
|
||||
DIM BSG$(6)
|
||||
SCORE# = 0
|
||||
LIVES# = 3
|
||||
BX# = 0
|
||||
BY# = 0
|
||||
HIT# = 0
|
||||
BI# = 0
|
||||
I# = 0
|
||||
R# = 0
|
||||
C# = 0
|
||||
KI# = 0
|
||||
```
|
||||
|
||||
`I#`, `R#`, `C#` and `KI#` are in that list because a `FOR` counter is a name like any
|
||||
other: name one that does not exist yet and every entry to the loop spends slots.
|
||||
|
||||
The same block does a second job. A `GOSUB` gets its own scope, and assignment walks up
|
||||
the chain to find an outer name — but a name *first seen* inside a subroutine is created
|
||||
in that subroutine and dies at `RETURN`. `HIT#` and `BI#` are how `HITTEST` answers its
|
||||
caller, so they have to exist before anybody calls it. See
|
||||
[Chapter 4](04-control-flow.md#goto-and-gosub) for the scope rules.
|
||||
|
||||
This is filed as `TODO.md` §6 item 30. Until it is fixed, the declaration block is not a
|
||||
style preference — it is the price of a program that runs.
|
||||
|
||||
## 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, the speed-up and — most usefully — the value pool from Step 3,
|
||||
which is the failure that only shows up after thousands of ticks.
|
||||
|
||||
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 |
|
||||
|---|---|---|
|
||||
| Create every name at the top, loop counters included | A name created inside a scope costs pool slots for good; 4096 ends the run | 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.
|
||||
Reference in New Issue
Block a user