Teach chapter 17 the collision verbs its listing now uses
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 3m42s
akbasic CI Build / akgl_build (push) Failing after 22s
akbasic CI Build / mutation_test (push) Failing after 3m29s

The previous commit converted `examples/breakout/characters/breakout.bas` and
left the chapter teaching three routines the listing no longer has. This closes
that.

Step 5 gains the `SOLID` registration beside the `BR#()` array, and says why
there are two records of the same thing: one is the program's, one is the
interpreter's, and the id is the array index plus one so neither has to be looked
up from the other. It is also now the only place cells and pixels meet, which
Step 2 used to promise about Step 10.

Step 10 is rewritten. It was `HITTEST` -- fourteen lines dividing pixels by cell
sizes to recover a grid index -- plus `XBRICK` and `YBRICK` testing the ball's
leading edge, with a paragraph explaining that the cost of testing an edge rather
than a box is missing a brick clipped at the corner by up to seven pixels. All of
that is gone. What replaces it asks four questions: which rectangle, is it still
there, which way out and how far, and which axis to reverse.

Three things in it are worth the reader's attention and get it: `BUMP(2)` is a
separate accumulator from `BUMP(1)` so the handler never sees the paddle; the
guard against being told about an already-broken brick, because a handler runs a
line or two after the overlap; and `SOLID BI# + 1` in `KILLBR`, without which the
ball goes on bouncing off a brick that is no longer drawn. That last one is now a
row in the rules table.

The float note is kept and sharpened rather than dropped. `D%` must be a float
because field 4 is one, and the products land in integer `BX#`/`BY#` -- which is
safe here because a box against a box gives a normal that is exactly -1, 0 or 1,
and would *not* be safe against a circle. Stated as a reason rather than left as
something that happens to work.

Step 9 loses its two brick calls and its `OX#`/`OY#` backup, with a sentence
saying why there is nothing to restore to. Step 16's declaration block and name
count follow, and the skeleton arms the handler.

Every fenced example still runs; both suites green; the game runs clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL
This commit is contained in:
2026-08-02 11:21:32 -04:00
parent 9e43acc0b0
commit 8da8d717c8

View File

@@ -43,8 +43,7 @@ $ ./build-akgl/basic examples/breakout/characters/breakout.bas
- **[Step 7](#step-7-read-the-keyboard)** — read the keyboard without blocking - **[Step 7](#step-7-read-the-keyboard)** — read the keyboard without blocking
- **[Step 8](#step-8-move-the-paddle)** — move the paddle and keep it on the screen - **[Step 8](#step-8-move-the-paddle)** — move the paddle and keep it on the screen
- **[Step 9](#step-9-move-the-ball)** — move the ball and bounce it off the walls - **[Step 9](#step-9-move-the-ball)** — move the ball and bounce it off the walls
- **[Step 10](#step-10-break-bricks)** — work out which brick the ball is touching, and - **[Step 10](#step-10-break-bricks)** — find out which brick the ball hit, and break it
break it
- **[Step 11](#step-11-bounce-off-the-paddle)** — bounce off the paddle at an angle the - **[Step 11](#step-11-bounce-off-the-paddle)** — bounce off the paddle at an angle the
player chooses player chooses
- **[Step 12](#step-12-draw-the-hud-and-the-messages)** — draw the HUD and the messages - **[Step 12](#step-12-draw-the-hud-and-the-messages)** — draw the HUD and the messages
@@ -190,9 +189,9 @@ HUD line. `MAXX#` is as far right as the ball can go — the window less the bal
eight pixels. `PW#` is the paddle's width and `PY#` the row of pixels it sits on. 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. `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 **Cells and pixels are two different coordinate systems, and they meet in exactly one
game converts between them.** That routine is in Step 10. Everywhere else, bricks are in place** — Step 5, where each brick's cell is turned into the rectangle `SOLID` registers.
cells and everything that moves is in pixels. Everywhere else, bricks are in cells and everything that moves is in pixels.
## Step 3: Declare every name first ## Step 3: Declare every name first
@@ -521,10 +520,25 @@ FOR C# = 0 TO 9
BR#(I#) = 0 BR#(I#) = 0
IF T$ = "1" THEN BR#(I#) = 1 IF T$ = "1" THEN BR#(I#) = 1
IF T$ = "1" THEN LEFTN# = LEFTN# + 1 IF T$ = "1" THEN LEFTN# = LEFTN# + 1
SX# = (BLEFT# + (C# * BRW#)) * CW#
SY# = (BTOP# + R#) * CH#
IF T$ = "1" THEN SOLID I# + 1, SX#, SY#, SX# + (BRW# * CW#), SY# + CH#
IF T$ = "0" THEN SOLID I# + 1
NEXT C# NEXT C#
RETURN RETURN
``` ```
Two things happen per cell, and the second is what Step 10 is built on. `BR#()` is the
program's own record of which bricks are left. **`SOLID` tells the interpreter**, so it can
answer "did the ball hit a brick, and which one" instead of the program working it out.
The id is the array index plus one — `SOLID` numbers from 1 and the array from 0 — so a
rectangle and its array element are the same brick with no lookup between them. A hole
retires whatever was there, which matters because a level change reuses the ids.
The rectangle is in pixels, and this is the only place the cell grid and the pixel grid meet:
a brick at column `C#` starts at `(BLEFT# + C# * BRW#) * CW#` and is `BRW# * CW#` wide.
`MID` counts from **zero** here, unlike Commodore BASIC. `BR#(row * 10 + col)` is the `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 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. comparison rather than a scan of sixty cells.
@@ -792,23 +806,19 @@ each move on its own:
```basic norun ```basic norun
LABEL MOVEBAL LABEL MOVEBAL
OX# = BX#
BX# = BX# + BVX# BX# = BX# + BVX#
IF BX# < 0 THEN GOSUB WALLL IF BX# < 0 THEN GOSUB WALLL
IF BX# > MAXX# THEN GOSUB WALLR IF BX# > MAXX# THEN GOSUB WALLR
GOSUB XBRICK
OY# = BY#
BY# = BY# + BVY# BY# = BY# + BVY#
IF BY# < TOPY# THEN GOSUB WALLT IF BY# < TOPY# THEN GOSUB WALLT
GOSUB YBRICK
GOSUB PADHIT GOSUB PADHIT
IF BY# > LOSEY# THEN STATE# = 1 IF BY# > LOSEY# THEN STATE# = 1
RETURN RETURN
``` ```
Doing X first and then Y means a ball arriving at the corner of a brick reverses one axis **Nothing here tests a brick.** Step 10 arms a handler that fires when the ball meets one,
rather than both, which is what looks right. `OX#` and `OY#` remember where the ball came and it pushes the ball out along the contact rather than restoring a remembered position —
from, so a collision has somewhere to put it back to. so there is no "where was it before" to keep.
A wall bounce puts the ball on the wall and flips the sign of that axis: A wall bounce puts the ball on the wall and flips the sign of that axis:
@@ -842,75 +852,72 @@ CHAR 1, 0, 1, FRAME$
## Step 10: Break bricks ## Step 10: Break bricks
**Goal: work out which brick a point is inside, and take it out of the wall.** **Goal: find out which brick the ball hit, and take it out of the wall.**
This is the one routine that converts pixels into cells. Give it a point in `TX#` and Step 5 registered every brick with `SOLID`, so this is not arithmetic — it is a question.
`TY#` and it sets `HIT#` to 1 or 0, and when it is 1 it also sets `BI#` — the index into Arm a handler:
the sixty-element grid — and `BR2#`, the row:
```basic norun ```basic norun
LABEL HITTEST COLLISION 2, BRICKHIT
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 `COLLISION 2` fires when a sprite overlaps one of those rectangles. The handler runs
top of the wall, above the bottom of it, right of the left edge, left of the right edge. **between source lines**, exactly like a `GOSUB` the program did not write, and must end in
Only if all four pass is there a cell to look at. Dividing a pixel by `CH#` gives a text `RETURN`.
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:**
```basic norun ```basic norun
LABEL XBRICK LABEL BRICKHIT
TX# = BX# M# = BUMP(2)
IF BVX# > 0 THEN TX# = BX# + 7 IF (M# AND 1) = 0 THEN RETURN
TY# = BY# + 4 T# = RCOLLISION(1, 1)
GOSUB HITTEST IF T# < 1 THEN RETURN
IF HIT# = 0 THEN RETURN IF BR#(T# - 1) = 0 THEN RETURN
BX# = OX# D% = RCOLLISION(1, 4)
BVX# = 0 - BVX# BX# = BX# + (RCOLLISION(1, 2) * D%)
GOSUB KILLBR BY# = BY# + (RCOLLISION(1, 3) * D%)
RETURN A# = RCOLLISION(1, 7)
IF A# = 1 THEN BVX# = 0 - BVX#
LABEL YBRICK IF A# = 2 THEN BVY# = 0 - BVY#
TX# = BX# + 4 BI# = T# - 1
TY# = BY# BR2# = BI# / BCOLS#
IF BVY# > 0 THEN TY# = BY# + 7
GOSUB HITTEST
IF HIT# = 0 THEN RETURN
BY# = OY#
BVY# = 0 - BVY#
GOSUB KILLBR GOSUB KILLBR
RETURN RETURN
``` ```
Moving right, the leading edge is `BX# + 7`; moving left it is `BX#`. A ball tested by its Read it as four questions and an answer.
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**: **`BUMP(2)` is the mask of which sprites met static geometry**, and bit 0 is sprite 1 — the
ball. Reading it clears it, so the next hit is news again. `BUMP(1)` is a *separate*
accumulator for sprite-against-sprite, so this handler never sees the paddle.
**`RCOLLISION(1, 1)` is which rectangle.** Because Step 5 registered brick `I#` as id
`I# + 1`, that number is the array index plus one and nothing has to be looked up. The two
guards after it are worth keeping: a program can be told about a brick it has already
broken, because the handler runs a line or two after the overlap happened.
**Fields 2, 3 and 4 push the ball out.** Field 4 is how deep the overlap is and fields 2 and
3 are the direction out of the brick, so adding one times the other puts the ball exactly
clear. That is why `MOVEBAL` in Step 9 keeps no `OX#`/`OY#` backup — there is nothing to
restore to.
`D%` is a float variable, and it has to be: field 4 is a float and a `#` would throw the
fraction away. The products land in `BX#` and `BY#`, which *are* integers — and that is
safe here for a reason worth knowing rather than assuming. A box against a box gives a
normal that is exactly -1, 0 or 1, so the product is a whole number before it is stored.
Against a circle it would not be, and the push-out would land a pixel short.
**Field 7 is which axis to reverse.** It is the one the ball is least far through, which is
what makes a ball clipping the end of a row go sideways rather than straight back down.
Working it out yourself means comparing two floats, which is exactly where
[Chapter 13](13-differences.md)'s left-operand rule catches people, so it is computed for
you.
Breaking the brick clears its cell, scores it, and redraws **just that row**:
```basic norun ```basic norun
LABEL KILLBR LABEL KILLBR
BR#(BI#) = 0 BR#(BI#) = 0
SOLID BI# + 1
LEFTN# = LEFTN# - 1 LEFTN# = LEFTN# - 1
PTS# = (BROWS# - BR2#) * 10 PTS# = (BROWS# - BR2#) * 10
SCORE# = SCORE# + PTS# SCORE# = SCORE# + PTS#
@@ -920,6 +927,10 @@ IF LEFTN# < 1 THEN STATE# = 2
RETURN RETURN
``` ```
**`SOLID BI# + 1` retires the rectangle in the same breath as clearing the array.** Miss it
and the ball goes on bouncing off a brick that is no longer drawn, which is a bewildering
thing to debug and an easy thing to forget.
The top row is worth 60 and the bottom row 10. `LEFTN#` reaching zero sets the state that 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 6's dispatch turns into a level change.
@@ -1487,6 +1498,7 @@ LABEL SETUP the geometry from Step 2
the ceiling from Step 9 the ceiling from Step 9
GOSUB MKSPR Step 4 GOSUB MKSPR Step 4
GOSUB SNDPROBE Step 14 GOSUB SNDPROBE Step 14
COLLISION 2, BRICKHIT Step 10
GOTO TITLE GOTO TITLE
the sprite routine, the sound routines, the title screen, the sprite routine, the sound routines, the title screen,
@@ -1531,8 +1543,6 @@ BX# = 0
BY# = 0 BY# = 0
BVX# = 0 BVX# = 0
BVY# = 0 BVY# = 0
OX# = 0
OY# = 0
K# = 0 K# = 0
HIT# = 0 HIT# = 0
PVX# = 0 PVX# = 0
@@ -1541,13 +1551,12 @@ STALL# = 0
NUDGE# = 0 NUDGE# = 0
BI# = 0 BI# = 0
BR2# = 0 BR2# = 0
RB# = 0
CB# = 0
RC# = 0
CC2# = 0
TX# = 0
TY# = 0
PTS# = 0 PTS# = 0
SX# = 0
SY# = 0
A# = 0
M# = 0
D% = 0.0
V# = 0 V# = 0
L# = 0 L# = 0
C2# = 0 C2# = 0
@@ -1580,7 +1589,7 @@ KI# = 0
T# = 0 T# = 0
``` ```
That is 69 names out of the 128 the interpreter has. Loop counters are in there too — That is 65 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 `I#`, `R#`, `C#`, `CC#`, `KI#` — because a `FOR` creates its counter the same way a
`GOSUB` creates a local, and declaring them costs nothing. `GOSUB` creates a local, and declaring them costs nothing.
@@ -1757,6 +1766,7 @@ A checklist to write your own game against.
| Parenthesise every mixed `+` and `-` | `a - b + c` is currently evaluated as `a - (b + c)` | [Step 3](#step-3-declare-every-name-first) | | Parenthesise every mixed `+` and `-` | `a - b + c` is currently evaluated as `a - (b + c)` | [Step 3](#step-3-declare-every-name-first) |
| Parenthesise a second `AND` or `OR` | Only one unparenthesised `AND` or `OR` is matched per expression | [Step 3](#step-3-declare-every-name-first) | | Parenthesise a second `AND` or `OR` | Only one unparenthesised `AND` or `OR` is matched per expression | [Step 3](#step-3-declare-every-name-first) |
| Compute a `MOVSPR` coordinate into a variable first | A leading sign means *move by*, not *move to* | [Step 4](#step-4-make-the-ball-and-the-paddle) | | Compute a `MOVSPR` coordinate into a variable first | A leading sign means *move by*, not *move to* | [Step 4](#step-4-make-the-ball-and-the-paddle) |
| Retire a `SOLID` when the thing it stood for is gone | The ball goes on bouncing off a brick that is no longer drawn | [Step 10](#step-10-break-bricks) |
| Build a text row whole and write it from column 0 | `CHAR` terminates the row where it stops | [Step 5](#step-5-build-the-brick-wall) | | Build a text row whole and write it from column 0 | `CHAR` terminates the row where it stops | [Step 5](#step-5-build-the-brick-wall) |
| Tell rows apart by shape, not colour | `CHAR` ignores its colour argument | [Step 5](#step-5-build-the-brick-wall) | | Tell rows apart by shape, not colour | `CHAR` ignores its colour argument | [Step 5](#step-5-build-the-brick-wall) |
| 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](#step-6-write-the-main-loop) | | 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](#step-6-write-the-main-loop) |