Teach the artwork tutorial the converted listing

Chapter 18 still taught the machinery the listing lost when it moved onto
SOLID and the persistent drawing layer: the frame-boundary PACE routine, the
flattened live list, the SSHAPE pool accounting, and the sixty lines of
BALLBRICKS/TESTCELL that computed a minimum translation axis by hand.

Step 8 now registers each brick with SOLID, arms COLLISION 2 and reads the
contact back with RCOLLISION, including the point that costs an evening if it
is missed: the BUMP mask is by sprite, and the balls are sprites 5, 6 and 7,
so their bits are 16, 32 and 64. The FOR-with-equal-bounds rule that every
one-item case in the chapter depends on is now stated where it is first used
rather than referred to from a step that no longer exists.

The listing loses six declarations orphaned by the conversion and the comment
blocks that still described the captures.

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 13:13:51 -04:00
parent 39d1d0c80c
commit a01554d304
3 changed files with 248 additions and 369 deletions

View File

@@ -54,26 +54,25 @@ tells you which it is.
- **[Step 1](#step-1-put-artwork-on-the-screen)** — load PNG artwork into sprites
- **[Step 2](#step-2-budget-the-eight-sprite-slots)** — decide what each of the eight
sprites is, before writing anything else
- **[Step 3](#step-3-turn-a-drawing-into-a-sprite)** — draw something and turn it into a
sprite so it stays on the screen
- **[Step 3](#step-3-make-room-for-what-you-draw)** — take the text layer out of the way,
so what you draw can be seen and stays seen
- **[Step 4](#step-4-make-the-brick-stamps)** — make one brick per colour and stamp the
field out of them
- **[Step 5](#step-5-find-the-frame-boundary)** — find the host's frame boundary, so a
drawing survives being captured
- **[Step 6](#step-6-draw-at-most-one-thing-per-frame)** — draw at most one thing per
- **[Step 5](#step-5-draw-at-most-one-thing-per-frame)** — draw at most one thing per
frame, chosen by dirty flags
- **[Step 7](#step-7-keep-a-flattened-list-of-live-bricks)** — keep a flattened list of the
bricks still standing, so the draw routine decides nothing
- **[Step 8](#step-8-draw-lettering-with-a-stroke-font)** — draw lettering with a stroke
- **[Step 6](#step-6-erase-one-brick-not-the-whole-field)** — erase a broken brick in
place, so the field is drawn once and not rebuilt
- **[Step 7](#step-7-draw-lettering-with-a-stroke-font)** — draw lettering with a stroke
font, because text has no colour
- **[Step 9](#step-9-move-and-bounce-the-ball)** — move the ball and bounce it off bars and
bricks without a square root
- **[Step 10](#step-10-gems-and-powerups)** — drop gems and apply what catching one does
- **[Step 11](#step-11-three-voices-and-a-mute)** — three voices of sound and a mute that
- **[Step 8](#step-8-move-and-bounce-the-ball)** — move the ball, bounce it off the bars
without a square root, and register the bricks as collision geometry so the interpreter
finds the hits
- **[Step 9](#step-9-gems-and-powerups)** — drop gems and apply what catching one does
- **[Step 10](#step-10-three-voices-and-a-mute)** — three voices of sound and a mute that
costs nothing
- **[Step 12](#step-12-the-states)** — the frame loop's states: title, serve, play, lost,
- **[Step 11](#step-11-the-states)** — the frame loop's states: title, serve, play, lost,
cleared
- **[Step 13](#step-13-put-the-program-together)** — put the pieces in one file, in the
- **[Step 12](#step-12-put-the-program-together)** — put the pieces in one file, in the
right order
---
@@ -143,15 +142,15 @@ around later, so spend them on paper first. This game spends them like this:
| Slot | Is |
|---|---|
| 1 | the HUD strip — a captured drawing |
| 2 | the playing field — a captured drawing |
| 1 | free |
| 2 | free |
| 3 | the paddle |
| 4 | the catcher bar (the purple gem) |
| 5, 6, 7 | up to three balls |
| 8 | the falling gem |
Two of the eight are **the screen itself**, and Step 3 explains why. That leaves six for
everything that moves, and two consequences fall straight out of it:
Six are spent and **two are left over**, which is more room than the program needs. Two
consequences fall straight out of the six that are spent:
- **The bricks are drawn rather than made of artwork.** Sixty bricks will not fit in six
slots, and a sprite is the only way to get an image file onto the screen — `GSHAPE`
@@ -161,44 +160,41 @@ everything that moves, and two consequences fall straight out of it:
Deciding this first is what stops a feature costing an afternoon before it is abandoned.
## Step 3: Turn a drawing into a sprite
## Step 3: Make room for what you draw
**Goal: something you drew, still on the screen on the next frame.**
Two layers are redrawn for you every frame from state the interpreter keeps: the text grid
and the sprites. **The drawing verbs are not one of them.** `DRAW`, `BOX`, `CIRCLE` and
`PAINT` write straight into the renderer's buffer, and by default the text layer owns
every row of the window and repaints over them before the frame is shown.
Two things have to be true before a drawing is any use, and only one of them is automatic.
So a drawing has to be turned into something that persists. The sequence is: draw it,
capture it with `SSHAPE`, install the capture into a sprite with `SPRSAV`.
**A drawing persists.** `DRAW`, `BOX`, `CIRCLE` and `PAINT` render into a layer the frame
composites underneath the text and the sprites, so a picture you draw once is there on
every frame after. You do not redraw it and you do not have to keep it anywhere.
**But the text layer covers it.** It repaints every row it owns, opaque, every frame — and
by default it owns the whole window. So the first executable line of this program is:
```basic norun
SSHAPE Z$, 0, 60, 800, 600
SPRSAV Z$, 2
SPRITE 2, 1, 2
MOVSPR 2, 0, 60
WINDOW 0, 35, 49, 36
```
Four lines, and they are the last four lines of every draw routine in this game. `SSHAPE`
captures the rectangle from (0, 60) to (800, 600) into `Z$`; `SPRSAV` installs it into
slot 2; `SPRITE` turns the slot on; `MOVSPR` puts it back where it was drawn.
Two rows at the bottom, which is enough for the final score, and the other thirty-five
belong to the drawing verbs. `WINDOW l, t, r, b` takes character cells, and
`RWINDOW(0)` and `RWINDOW(1)` report what you ended up with.
`Z$` holds a **handle**, not pixels — see
[Chapter 6](06-graphics.md#saving-and-stamping-regions). You can pass it to `GSHAPE` and
`SPRSAV`. You cannot print it, save it or take its `LEN`.
That is the whole of it. Draw your field once and it stays; draw your HUD once and it
stays.
Once it is a sprite it stays on the screen for nothing, and that is the point: a sprite is
drawn from state the interpreter keeps, so it costs the program no work per frame at all.
**Two consequences shape the rest of this chapter, and both are things you no longer have
to do.**
`WINDOW 0, 0, 49, 1` would shrink the text area and hand the rest of the window to the
drawing verbs, which is the other way to make a drawing visible. This program does not use
it, because a drawing kept that way still has to be re-issued every frame *and* fit inside
one batch (Step 5), and a sprite has neither constraint. Making the drawing verbs visible
without a `WINDOW` call is [`TODO.md` §9 item 3](../TODO.md).
There is no drawing deadline. The host runs a fixed number of source lines and then
presents the frame, and a drawing longer than one batch used to be a problem — it does not
matter here, because a drawing that spans two batches simply arrives over two frames and
the layer keeps both halves.
The figures in this chapter are rendered by a tool that draws no text layer, which is why
they can show a bare drawing at all.
And there is nothing to redraw. If you change one brick, erase that brick; the other
fifty-nine are still on the screen. Which is why this chapter has no routine that walks a
list of everything still standing.
## Step 4: Make the brick stamps
@@ -242,7 +238,7 @@ x2, y2` draws a line using source 1.
Each brick is 68 by 16 and is filled with horizontal lines. The inner loop draws **two
scan lines per pass**, `T1# + K#` and `T2# + K#`, so eight passes fill sixteen rows. That
costs about a hundred and thirty lines for the whole set instead of the two hundred and
seventy a line-at-a-time loop would take, and Step 5 explains why that number matters.
seventy a line-at-a-time loop would take.
Capturing the six is six `SSHAPE`s:
@@ -255,19 +251,18 @@ SSHAPE Z$, 0, 80, 68, 96 : S4$ = Z$
SSHAPE Z$, 0, 100, 68, 116 : S5$ = Z$
```
**`SSHAPE` has sixteen slots and nothing gives one back.** `GRAPHIC 5` gives back all of
them at once, so count what you have spent and rebuild the stamps when the pool runs dry:
**`SSHAPE` has sixteen slots and nothing gives one back**, and this program spends eight of
them: six brick colours and the two erasers in Step 6. Spent once, at startup, and never
again — nothing here captures per frame, so there is no pool to run dry and no rebuild:
```basic norun
LABEL DRAWPROTOS
GRAPHIC 5
GRAPHIC 1, 1
WIDTH 1
SHN# = 99
```
with the six `DRAW` loops and six `SSHAPE`s after it, and `SHN# = 6` at the end. Every
routine that captures adds one to `SHN#`, and the frame loop rebuilds when it reaches 14.
with the six `DRAW` loops, Step 6's two erasers, and eight `SSHAPE`s after it. Called once,
from the setup block.
**`PAINT` would be one statement instead of sixteen, and is not usable here.** It costs
nearly four milliseconds a call; sixty of those is seven frames' worth of time.
@@ -342,55 +337,12 @@ and 9.
The finished game keeps its six stamps in six separate scalars — `S0$` to `S5$` — rather
than in an array. An array works too.
## Step 5: Find the frame boundary
**Goal: a drawing that is not cut in half by the host presenting the frame.**
The host runs **256 source lines and then presents the frame**, and presenting throws the
drawing buffer away. So all the drawing between a `GRAPHIC 1, 1` and its `SSHAPE` has to
happen inside one of those batches. Draw more than fits and the capture comes back holding
only what was issued since the present, over whatever the frame before it left behind —
which looks exactly like a ghost.
`TI#` is the host's clock in sixtieths of a second, and it is refreshed **once per batch**.
So the step on which `TI#` changes is the first step of a batch, and spinning until it
changes is how a program finds a frame boundary:
```basic norun
LABEL PACE
LASTT# = TI#
LABEL PACEEDGE
IF TI# - LASTT# < 2 THEN GOTO PACEEDGE
RETURN
```
Two jiffies is thirty frames a second.
**`LASTT#` is sampled on entry rather than carried over from the last frame, and that is
the whole correctness of the routine.** Carried over, a frame whose work ran long would
find the time already spent, return immediately from somewhere in the middle of a batch,
and ruin the capture that followed. Sampling here means the loop always sees `TI#` change
under it, and a change is only ever seen on the first step of a batch.
Measured on one machine: after a jiffy edge, 220 lines of drawing survive the capture
intact and 250 do not. Write every draw routine to stay near 200 and you have margin.
**Arithmetic is free.** A routine that computes for two thousand steps costs frame rate and
nothing else — only *drawing* has a deadline. That single fact is what Steps 7 and 8 are
built on: move everything that is not a drawing verb out of the routine that draws.
This is the host's budget rather than the interpreter's, so the pacing routine is the
answer rather than a workaround for something being fixed;
[`TODO.md` §9 item 5](../TODO.md) records the measurements and why no fix belongs in the
library.
## Step 6: Draw at most one thing per frame
## Step 5: Draw at most one thing per frame
**Goal: a frame loop that paces, draws one thing, and then plays the game.**
```basic norun
LABEL FRAME
GOSUB PACE
GOSUB DRAWJOB
GOSUB READKEYS
IF STATE# = 0 THEN GOSUB TITLETICK
@@ -402,19 +354,16 @@ IF RUNNING# = 0 THEN GOTO SHUTDOWN
GOTO FRAME
```
Pace first, because that is what puts the drawing at the top of a batch. Then at most one
capture, because a capture is the only thing with a deadline. Then the game, which may
take as long as it likes.
At most one drawing job a frame, then the game. Nothing here has a deadline any more — a
drawing that spans two batches arrives over two frames — but a queue of one still keeps the
work even, and it keeps the *decision* about what needs redrawing in one place instead of
scattered through the game.
`DRAWJOB` is a queue of one, chosen by dirty flags — stamps first, because everything else
draws with them, then the field, then the HUD:
```basic norun
LABEL DRAWJOB
IF SHN# < 14 THEN GOTO DRAWJOB2
GOSUB DRAWPROTOS
RETURN
LABEL DRAWJOB2
IF DPLAY# = 0 THEN GOTO DRAWJOB3
GOSUB DRAWPLAY
DPLAY# = 0
@@ -434,105 +383,43 @@ nobody can see it.
Those are `LABEL`s and `GOTO`s rather than `BEGIN` blocks. Either works; the labels keep
each arm to one `RETURN` and read the same.
## Step 7: Keep a flattened list of live bricks
## Step 6: Erase one brick, not the whole field
**Goal: a draw routine that decides nothing.**
**Goal: take a broken brick off the screen without redrawing the other fifty-nine.**
The draw routine has a deadline and the rest of the program does not, so any decision that
can be made earlier should be. When a brick breaks, walk the grid once and write out a
**list of the bricks still standing**, row by row — so a row becomes a contiguous run of
that list, and drawing it is a stamp and an advance:
Because a drawing stays, removing something means covering it up. There is no
filled-rectangle verb — `BOX` outlines and `PAINT` costs about four milliseconds a call —
so the cheapest way to blank a region is a stamp of something blank:
```basic norun
LABEL BUILDLIVE
LN# = 0
FOR R# = 0 TO 5
RS#(R#) = LN#
RC#(R#) = 0
GOSUB BUILDROW
NEXT R#
RETURN
COLOR 1, 1
FOR K# = 0 TO 15
DRAW 1, 0, 130 + K# TO 67, 130 + K#
NEXT K#
SSHAPE Z$, 0, 130, 68, 146 : BL$ = Z$
```
LABEL BUILDROW
FOR C# = 0 TO 9
IF BRK#(R# * 10 + C#) > 0 THEN BEGIN
LX#(LN#) = BRKX# + C# * 72
LY#(LN#) = BRKY# + R# * 24
LN# = LN# + 1
RC#(R#) = RC#(R#) + 1
BEND
NEXT C#
The same sixteen lines the brick stamps are made of, in the background colour. Erasing is
then one statement:
```basic norun
LABEL ERASEBRICK
Z$ = BL$
GSHAPE Z$, BRKX# + C# * 72, BRKY# + R# * 24
RETURN
```
`LX#()` and `LY#()` are the pixel positions of the bricks still alive. `RS#(R#)` is where
row `R#`'s run starts in that list and `RC#(R#)` is how long it is.
Make a second one the width of the HUD strip while you are here, for the same reason: the
strip is rewritten whenever a number in it changes, and the old digits have to go
somewhere before the new ones are drawn.
Drawing a row is then one loop with no tests in it:
**This is what makes the whole field a draw-once job.** Draw the walls and all sixty bricks
when a level is laid out, and after that touch only what changes. A program that had to
redraw the field to remove one brick would need a list of what is still standing, kept in
step with the array, walked in runs per row — and none of that is here, because none of it
is needed.
```basic norun
LABEL STAMPROW
IF T2# < 1 THEN RETURN
IF T2# = 1 THEN GSHAPE Z$, LX#(T1#), LY#(T1#)
IF T2# < 2 THEN RETURN
FOR I# = T1# TO T1# + T2# - 1
GSHAPE Z$, LX#(I#), LY#(I#)
NEXT I#
RETURN
```
and `DRAWPLAY` calls it six times, once per stamp:
```basic norun
Z$ = S0$ : T1# = RS#(0) : T2# = RC#(0) : GOSUB STAMPROW
Z$ = S1$ : T1# = RS#(1) : T2# = RC#(1) : GOSUB STAMPROW
Z$ = S2$ : T1# = RS#(2) : T2# = RC#(2) : GOSUB STAMPROW
Z$ = S3$ : T1# = RS#(3) : T2# = RC#(3) : GOSUB STAMPROW
Z$ = S4$ : T1# = RS#(4) : T2# = RC#(4) : GOSUB STAMPROW
Z$ = S5$ : T1# = RS#(5) : T2# = RC#(5) : GOSUB STAMPROW
```
Building the list costs about four lines a brick and has all the time in the world.
Drawing costs two and has a deadline. **That trade is the spine of this program**, and it
comes back in Step 8 for the lettering.
### Write the one-item case out beside every loop
Notice the two lines before the `FOR` in `STAMPROW`. **A `FOR` whose bounds are equal does
not run its body**, and the last brick left in a row is exactly that case:
```basic
FOR I# = 0 TO 0
PRINT "THE BODY RAN"
NEXT I#
PRINT "AFTER THE LOOP"
```
```output
AFTER THE LOOP
```
So a loop over a list of unknown length needs its count of one written out beside it, as
`STAMPROW` does. The alternative is a `DO ... LOOP UNTIL`, which tests at the bottom and
therefore always runs once:
```basic
I# = 0
DO
PRINT "PASS " + I#
I# = I# + 1
LOOP UNTIL I# >= 1
```
```output
PASS 0
```
Use whichever reads better. This is [`TODO.md` §6 item 19](../TODO.md), which is waiting on
a decision about a checked-in acceptance file rather than on the work; when it lands,
`FOR I# = 0 TO 0` will run once and both shapes above will still be correct.
## Step 8: Draw lettering with a stroke font
## Step 7: Draw lettering with a stroke font
**Goal: a HUD in more than one colour.**
@@ -640,11 +527,6 @@ FOR I# = 0 TO HN# - 1
DRAW HC#(I#), HX1#(I#), HY1#(I#) TO HX2#(I#), HY2#(I#)
NEXT I#
LABEL HUDONE
SSHAPE Z$, 0, 0, 800, 60
SPRSAV Z$, 1
SPRITE 1, 1, 2
MOVSPR 1, 0, 0
SHN# = SHN# + 1
RETURN
```
@@ -654,11 +536,14 @@ score yellow, the lives green, the level red. **Seven colour sources is the ceil
which is why the purple gem's banner is the closest purple the palette has rather than the
gem's own.
Note the one-stroke case written out beside the loop, for the reason in Step 7. And note
`SHN# = SHN# + 1`: every routine that captures counts the slot it spent, which is what
Step 4's rebuild-when-dry test reads.
Note the one-stroke case written out beside the loop. **A `FOR` with equal bounds does not
run its body at all in this dialect**, so `FOR I# = 0 TO HN# - 1` with one stroke in the
list draws nothing. Write the one-item case out beside the loop with a `GOTO` past the
loop, and do it wherever a list can hold exactly one thing — a one-character word, a
one-stroke glyph, a field with one brick left. [Chapter 13](13-differences.md) records the
rule; `TODO.md` §6 item 19 records why it stands.
## Step 9: Move and bounce the ball
## Step 8: Move and bounce the ball
**Goal: bounces at sensible angles, computed without a square root.**
@@ -692,12 +577,12 @@ RETURN
```
`T1#` is the bar's left edge, `T2#` its top and `T3#` its width, so the same routine serves
the paddle and the catcher bar — `BALLPADDLE` in Step 13 calls it twice with different
the paddle and the catcher bar — `BALLPADDLE` in Step 12 calls it twice with different
values and reads `HIT#` to find out whether the first call already caught the ball. The
four tests at the top are the overlap; each can bail out, and reaching the fifth line means
the ball is on the bar.
Step 10 adds two more lines before that `RETURN`, for the sticky gem.
Step 9 adds two more lines before that `RETURN`, for the sticky gem.
### Which type a variable is decides the arithmetic
@@ -768,92 +653,97 @@ scaling that up to the new speed multiplies everything by 1/0.53, and the ball e
nearly twice as fast as intended. Every zone inflates; only the middle two are close enough
to vertical for it not to show.
A ball can only be over four cells at once, so work out **which** cells from its bounding
box rather than walking all sixty bricks:
### Bricks are collision geometry, not arithmetic
A brick is a rectangle the interpreter knows about. Register one per brick as the level is
laid out:
```basic norun
LABEL BALLBRICKS
IF BRN# = 0 THEN RETURN
LFT# = BLX%(B#)
TOP# = BLY%(B#)
RGT# = LFT# + 21
BOT# = TOP# + 21
IF BOT# < BRKY# THEN RETURN
IF TOP# > BRKY# + 6 * 24 THEN RETURN
CC1# = (LFT# - BRKX#) / 72
CC2# = (RGT# - BRKX#) / 72
RR1# = (TOP# - BRKY#) / 24
RR2# = (BOT# - BRKY#) / 24
IF CC1# < 0 THEN CC1# = 0
IF CC2# > 9 THEN CC2# = 9
IF RR1# < 0 THEN RR1# = 0
IF RR2# > 5 THEN RR2# = 5
IF CC2# < CC1# THEN RETURN
IF RR2# < RR1# THEN RETURN
HIT# = 0
R# = RR1#
DO
C# = CC1#
DO
IF HIT# = 0 THEN GOSUB TESTCELL
C# = C# + 1
LOOP UNTIL C# > CC2# OR HIT# = 1
R# = R# + 1
LOOP UNTIL R# > RR2# OR HIT# = 1
LABEL FILLROW
FOR C# = 0 TO 9
BRK#(R# * 10 + C#) = 1
BRN# = BRN# + 1
SLX# = BRKX# + C# * 72
SLY# = BRKY# + R# * 24
SLI# = (R# * 10 + C#) + 1
SOLID SLI#, SLX#, SLY#, SLX# + 68, SLY# + 16
NEXT C#
RETURN
```
`LFT#`, `TOP#`, `RGT#` and `BOT#` are the ball's own box — the artwork is 22 pixels across,
so the ball's right edge is its left plus 21. `CC1#` to `RR2#` are the range of cells that
box can touch, clamped to the field. Both loops are `DO ... LOOP UNTIL` rather than `FOR`,
because a ball entirely inside one column gives `CC1#` and `CC2#` the same value and a
`FOR` would not run at all.
The id is the array index plus one, so what comes back out indexes `BRK#()` with no lookup.
Compute it into `SLI#` first rather than writing the expression inline — a parenthesised
first argument to a verb is worth avoiding, and it reads better anyway.
One brick at a time, then. A collision reflects off whichever face the ball has less of
itself past. `T1#` to `T4#` are the **overlapping** rectangle, and its width against its
height is the whole test:
**Retire the previous level's rectangles before laying the next one out**, not after. That
is one line in `SETUPLEVEL`, and putting it in the wrong place is silent: registering all
sixty and then clearing them leaves a wall the ball passes straight through, with no error
and nothing to see.
Then arm a handler and let it do the work:
```basic norun
LABEL TESTCELL
N# = R# * 10 + C#
COLLISION 2, BRICKHIT
```
```basic norun
LABEL BRICKHIT
MB# = BUMP(2)
IF MB# = 0 THEN RETURN
B# = 0
IF (MB# AND 16) <> 0 THEN GOSUB ONEBRICK
B# = 1
IF (MB# AND 32) <> 0 THEN GOSUB ONEBRICK
B# = 2
IF (MB# AND 64) <> 0 THEN GOSUB ONEBRICK
RETURN
```
**The mask is by sprite, and the balls are sprites 5, 6 and 7** — so their bits are 16, 32
and 64, not 1, 2 and 4. That is the same `5 + B#` arithmetic `MOVSPR` uses for them
everywhere else, and getting it wrong costs nothing visible: the handler runs, tests bits
nothing sets, and returns.
```basic norun
LABEL ONEBRICK
IF BLON#(B#) = 0 THEN RETURN
N# = RCOLLISION(5 + B#, 1)
IF N# < 1 THEN RETURN
N# = N# - 1
IF BRK#(N#) = 0 THEN RETURN
BX1# = BRKX# + C# * 72
BY1# = BRKY# + R# * 24
IF RGT# < BX1# THEN RETURN
IF LFT# > BX1# + 67 THEN RETURN
IF BOT# < BY1# THEN RETURN
IF TOP# > BY1# + 15 THEN RETURN
T1# = RGT# : IF BX1# + 67 < T1# THEN T1# = BX1# + 67
T2# = LFT# : IF BX1# > T2# THEN T2# = BX1#
T3# = BOT# : IF BY1# + 15 < T3# THEN T3# = BY1# + 15
T4# = TOP# : IF BY1# > T4# THEN T4# = BY1#
IF T1# - T2# < T3# - T4# THEN BLVX%(B#) = 0.0 - BLVX%(B#)
IF T1# - T2# >= T3# - T4# THEN BLVY%(B#) = 0.0 - BLVY%(B#)
DP% = RCOLLISION(5 + B#, 4)
BLX%(B#) = BLX%(B#) + (RCOLLISION(5 + B#, 2) * DP%)
BLY%(B#) = BLY%(B#) + (RCOLLISION(5 + B#, 3) * DP%)
AX# = RCOLLISION(5 + B#, 7)
IF AX# = 1 THEN BLVX%(B#) = 0.0 - BLVX%(B#)
IF AX# = 2 THEN BLVY%(B#) = 0.0 - BLVY%(B#)
R# = N# / 10
C# = MOD(N#, 10)
BRK#(N#) = 0
SOLID N# + 1
BRN# = BRN# - 1
SCORE# = SCORE# + BRV#(R#)
SOUND 1, 17175 - R# * 2100, 4
HIT# = 1
GOSUB BUILDLIVE
GOSUB ERASEBRICK
GOSUB HUDTEXT
DPLAY# = 1
IF GMON# = 1 THEN RETURN
RNMAX# = 7
GOSUB NEXTRAND
IF RNVAL# = 0 THEN GOSUB SPAWNGEM
RETURN
```
That is the standard box resolution, and it is the reason a ball clipping the end of a row
goes sideways instead of straight back down.
Fields 2, 3 and 4 push the ball exactly clear along the contact normal. Field 7 is which
axis to reverse — **the minimum translation axis**, which is why a ball clipping the end of
a row goes sideways rather than straight back down. Working that out by hand means building
an overlap rectangle and comparing its width to its height; it is one field here.
Everything after `BRK#(N#) = 0` is the consequences of the hit, in one place: score it,
sound it, rebuild the live list from Step 7, rebuild the HUD text, mark the field dirty so
Step 6's queue redraws it, and roll one chance in seven for a gem — but only if there is not
one already falling, because Step 2 left exactly one slot for it. `BX1#` and `BY1#` are the
broken brick's own corner, which is where the gem will appear.
`SOLID N# + 1` retires the rectangle in the same statement that clears the array, so the
next frame cannot hit a brick that is no longer drawn. `GOSUB ERASEBRICK` takes it off the
screen with Step 6's blank stamp.
## Step 10: Gems and powerups
**Both records exist on purpose.** `BRK#()` is the program's — it is what "is the level
clear" counts and what the field redraw reads. The rectangles are the interpreter's. Keeping
them in step is two statements and the ids line up, which is the whole reason the id is the
index plus one.
## Step 9: Gems and powerups
**Goal: one falling gem that can be any of five things.**
@@ -948,7 +838,7 @@ NEXT B#
RETURN
```
STICKY is three lines at the end of `HITBAR` in Step 9, replacing its final `RETURN` — the
STICKY is three lines at the end of `HITBAR` in Step 8, replacing its final `RETURN` — the
ball stops where it landed and remembers its offset along the bar:
```basic norun
@@ -973,7 +863,7 @@ RETURN
```
The CATCH bar **mirrors** the paddle rather than following it — the line is in `MOVEPADDLE`
in Step 13:
in Step 12:
```basic norun
CTX% = 0.0 - PDX% + WALLL# + WALLR# - 104
@@ -981,7 +871,7 @@ CTX% = 0.0 - PDX% + WALLL# + WALLR# - 104
A second bar directly above the first is worth nothing; a mirrored one turns a dive across
the field into a save at both ends. Note the leading `0.0` rather than `0`, for the reason
in Step 9.
in Step 8.
### Every timer in one place
@@ -1032,7 +922,7 @@ Each arm is the same shape: count down, and on the frame it reaches zero, put ba
gem changed. SLOW expiring calls `LEVELSPEED` rather than assigning a number, so the speed
it returns to is the speed the current level should be running at.
## Step 11: Three voices and a mute
## Step 10: Three voices and a mute
**Goal: sound that does not cut itself off, and a mute that costs one line.**
@@ -1078,9 +968,9 @@ TEMPO 12
PLAY "V3 T2 U8 O4 QC QE QG O5 HC"
```
## Step 12: The states
## Step 11: The states
**Goal: the five states the frame loop in Step 6 dispatches to.**
**Goal: the five states the frame loop in Step 5 dispatches to.**
Each is a subroutine called once a frame while that state is current. This is a different
shape from Chapter 17's branch targets, and it works here because nothing jumps out of a
@@ -1139,10 +1029,10 @@ BRN# = 0
FOR I# = 0 TO 59
BRK#(I#) = 0
NEXT I#
SOLID
FOR R# = 0 TO 5
IF R# < T3# THEN GOSUB FILLROW
NEXT R#
GOSUB BUILDLIVE
GOSUB RESETBALL
GOSUB HUDTEXT
STATE# = 1
@@ -1159,7 +1049,7 @@ RETURN
called from level setup, from a re-serve and from the title screen — so no state can leak
from one game into the next.
## Step 13: Put the program together
## Step 12: Put the program together
**Goal: one file, in an order that runs.**
@@ -1167,18 +1057,21 @@ The file runs from the top, so three things about the order matter and nothing e
setup first, `DATA` in the order it will be read, and every `LABEL` present somewhere.
```text
WINDOW 0, 35, 49, 36 the text layer, out of the way -- Step 3
the declaration block, below
GOSUB LOADTABLES the row colours, values and bounce zones
GOSUB LOADFONT the stroke font
SEED# = MOD(1 + TI# * 7919, 2147483648)
the artwork from Step 1
VOL 8 / ENVELOPE / TEMPO from Step 11
VOL 8 / ENVELOPE / TEMPO from Step 10
COLLISION 2, BRICKHIT the brick handler -- Step 8
GOSUB DRAWPROTOS the stamps, once -- Steps 4 and 6
GOSUB TITLESCREEN
LABEL FRAME the frame loop from Step 6
LABEL FRAME the frame loop from Step 5
the pacing, the draw jobs, the input, the states, the ball,
the gems, the text builders, the generator
the draw jobs, the input, the states, the ball, the gems,
the text builders, the generator
--- in any order ---
the tables as DATA read by LOADTABLES
@@ -1241,19 +1134,14 @@ GMX% = 0
GMY% = 0
DIM BRK#(60)
DIM LX#(60)
DIM LY#(60)
DIM RS#(6)
DIM RC#(6)
DIM BRC#(6)
DIM BRV#(6)
LN# = 0
BRN# = 0
S0$ = "" : S1$ = "" : S2$ = ""
S3$ = "" : S4$ = "" : S5$ = ""
BL$ = "" : HBL$ = ""
Z$ = ""
SHN# = 99
DHUD# = 1
DPLAY# = 1
BAN$ = ""
@@ -1294,10 +1182,18 @@ LFT# = 0 : TOP# = 0 : RGT# = 0 : BOT# = 0
CC1# = 0 : CC2# = 0 : RR1# = 0 : RR2# = 0
HIT# = 0
SEED# = 1 : RNMAX# = 2 : RNVAL# = 0
MB# = 0 : AX# = 0 : DP% = 0.0
SLX# = 0 : SLY# = 0 : SLI# = 0
```
`SHN# = 99` means "the brick stamps do not exist", so the frame loop's first job is to
build them. `DPLAY#` and `DHUD#` start at 1 so the first frame draws both.
**`BL$` and `HBL$` are in that list for a reason worth learning from.** They are the two
eraser stamps, and they are built inside `DRAWPROTOS` — so left undeclared they were
created *in that routine's scope*, held a perfectly good handle while it ran, and were
empty everywhere else. Nothing failed; the first `GSHAPE` that used one simply refused with
"was given a string that did not come from SSHAPE", several routines away from the cause.
Chapter 17 Step 3 is about exactly this.
`DPLAY#` and `DHUD#` start at 1 so the first frame draws both.
That block is 121 of the interpreter's 128 variables, which is why several constants in the
geometry are spelled out where they are used rather than given names: a name costs a slot
@@ -1334,7 +1230,7 @@ DATA -0.85, -0.62, -0.40, -0.18, 0.18, 0.40, 0.62, 0.85
DATA -0.53, -0.78, -0.92, -0.98, -0.98, -0.92, -0.78, -0.53
```
Row colours, what a brick in each row is worth, and the eight bounce zones from Step 9.
Row colours, what a brick in each row is worth, and the eight bounce zones from Step 8.
`NEXTRAND` is a `GOSUB` over globals rather than a `DEF` function. Set `RNMAX#` to the
number of answers you want and read `RNVAL#`.
@@ -1527,7 +1423,6 @@ IF BLST#(B#) = 0 THEN BEGIN
BEND
IF BLON#(B#) = 0 THEN RETURN
IF BLST#(B#) = 0 THEN BEGIN
GOSUB BALLBRICKS
GOSUB BALLPADDLE
BEND
MOVSPR 5 + B#, BLX%(B#), BLY%(B#)
@@ -1570,9 +1465,9 @@ RETURN
Balls 0, 1 and 2 live in sprites 5, 6 and 7, which is why `MOVSPR 5 + B#` works.
`BLST#(B#)` is 1 while a ball is stuck to the paddle. Note the `0.0 -` on every sign flip,
for the reason in Step 9.
for the reason in Step 8.
`BALLBRICKS` and `TESTCELL` are in Step 9.
Brick collision is in Step 8.
### The gem's fall
@@ -1597,7 +1492,7 @@ HIT# = 1
RETURN
```
`TAKEGEM` is in Step 10. `HIT#` is doing double duty here as "this gem is finished with" —
`TAKEGEM` is in Step 9. `HIT#` is doing double duty here as "this gem is finished with" —
it is set both by the gem going off the bottom and by it being caught.
### Turning text into strokes
@@ -1771,7 +1666,7 @@ RETURN
colour rather than the last gem's.
`TITLESCREEN` and `GAMEISOVER` are longer only because both draw a screenful of lettering.
Both clear the brick array and call `BUILDLIVE` so the field draws empty, turn the paddle
Both clear the brick array and retire every rectangle with a bare `SOLID`, turn the paddle
and ball sprites off, reset `PN#`, and then build their text at a large `TXS#`:
```basic norun
@@ -1784,7 +1679,7 @@ BRN# = 0
FOR I# = 0 TO 59
BRK#(I#) = 0
NEXT I#
GOSUB BUILDLIVE
SOLID
SPRITE 3, 0
SPRITE 5, 0
SPRITE 6, 0
@@ -1886,10 +1781,8 @@ actually calls, with those pieces in place.
```basic norun
LABEL DRAWPROTOS
GRAPHIC 5
GRAPHIC 1, 1
WIDTH 1
SHN# = 99
FOR R# = 0 TO 5
COLOR 1, BRC#(R#)
T1# = R# * 20
@@ -1904,15 +1797,11 @@ SSHAPE Z$, 0, 40, 68, 56 : S2$ = Z$
SSHAPE Z$, 0, 60, 68, 76 : S3$ = Z$
SSHAPE Z$, 0, 80, 68, 96 : S4$ = Z$
SSHAPE Z$, 0, 100, 68, 116 : S5$ = Z$
SHN# = 6
DPLAY# = 1
DHUD# = 1
RETURN
```
`GRAPHIC 5` hands all sixteen `SSHAPE` slots back, which is the only way to get any of them
back — so `SHN#` goes to 99 first and to 6 at the end, and both flags are set because the
field and the HUD were drawn with stamps that no longer exist.
plus Step 6's two erasers, and then `DPLAY# = 1` and `DHUD# = 1` so the first frame draws
both. Called once from the setup block: nothing captures per frame, so eight of the sixteen
`SSHAPE` slots are spent here and never again, and `GRAPHIC 5` is never called at all.
```basic norun
LABEL DRAWPLAY
@@ -1921,29 +1810,29 @@ WIDTH 2
COLOR 0, 1 : COLOR 1, 4 : COLOR 2, 8 : COLOR 3, 5
COLOR 4, 11 : COLOR 5, 16 : COLOR 6, 6
BOX 5, 2, 62, 797, 597 : BOX 1, 6, 66, 793, 593
Z$ = S0$ : T1# = RS#(0) : T2# = RC#(0) : GOSUB STAMPROW
Z$ = S1$ : T1# = RS#(1) : T2# = RC#(1) : GOSUB STAMPROW
Z$ = S2$ : T1# = RS#(2) : T2# = RC#(2) : GOSUB STAMPROW
Z$ = S3$ : T1# = RS#(3) : T2# = RC#(3) : GOSUB STAMPROW
Z$ = S4$ : T1# = RS#(4) : T2# = RC#(4) : GOSUB STAMPROW
Z$ = S5$ : T1# = RS#(5) : T2# = RC#(5) : GOSUB STAMPROW
FOR R# = 0 TO 5
IF R# = 0 THEN Z$ = S0$
IF R# = 1 THEN Z$ = S1$
IF R# = 2 THEN Z$ = S2$
IF R# = 3 THEN Z$ = S3$
IF R# = 4 THEN Z$ = S4$
IF R# = 5 THEN Z$ = S5$
FOR C# = 0 TO 9
IF BRK#(R# * 10 + C#) > 0 THEN GSHAPE Z$, BRKX# + C# * 72, BRKY# + R# * 24
NEXT C#
NEXT R#
IF PN# = 1 THEN DRAW PC#(0), PX1#(0), PY1#(0) TO PX2#(0), PY2#(0)
IF PN# < 2 THEN GOTO PLDONE
FOR I# = 0 TO PN# - 1
DRAW PC#(I#), PX1#(I#), PY1#(I#) TO PX2#(I#), PY2#(I#)
NEXT I#
LABEL PLDONE
SSHAPE Z$, 0, 60, 800, 600
SPRSAV Z$, 2
SPRITE 2, 1, 2
MOVSPR 2, 0, 60
SHN# = SHN# + 1
RETURN
```
Same shape as `DRAWHUD` in Step 8: the walls, then the bricks, then the field's own stroke
list with its one-item case beside the loop, then Step 3's four lines that turn all of it
into sprite 2.
Same shape as `DRAWHUD` in Step 7: the walls, then the bricks, then the field's own stroke
list with its one-item case beside the loop. Nothing at the end — what is drawn is on the
screen.
**A label is one bare word.** `PLDONE` and `HUDONE` have no underscore in them, and cannot:
underscores are not part of an identifier here.
@@ -1963,7 +1852,7 @@ BRN# = 0
FOR I# = 0 TO 59
BRK#(I#) = 0
NEXT I#
GOSUB BUILDLIVE
SOLID
SPRITE 3, 0
SPRITE 5, 0
PN# = 0
@@ -2021,10 +1910,10 @@ a feature costing an afternoon before it is abandoned.
| Resource | There are | This game uses |
|---|---|---|
| Sprites | 8 | 8 |
| Variables | 128 | 121, plus 4 the interpreter makes |
| Labels | 64 | 61 |
| `SSHAPE` slots | 16, none reclaimed except by `GRAPHIC 5` | 6 stamps plus 1 capture a frame |
| Sprites | 8 | 6 |
| Variables | 128 | 123, plus 4 the interpreter makes |
| Labels | 64 | 57 |
| `SSHAPE` slots | 16, none reclaimed except by `GRAPHIC 5` | 8, spent once at startup |
| Colour sources | 7 | 7 |
| Scopes | 32 | 6 deep at most |
| Tokens on a line | 32, and the 33rd stops the interpreter rather than raising | short lines, temporaries instead of long conditions |
@@ -2043,7 +1932,7 @@ Step 1's artwork, with the numbers filled in by hand and no frame loop, so it dr
frame and stops.
The font here carries only the fourteen glyphs the HUD needs, in the order they appear in
`ALPHA$` — which is the ordering rule from Step 8, shown small enough to check by eye:
`ALPHA$` — which is the ordering rule from Step 7, shown small enough to check by eye:
```basic requires=akgl setup=breakout_art screenshot=breakout-game-artwork size=800x600
DIM BRC#(6)

View File

@@ -48,10 +48,10 @@ dive across the field into a save at both ends.
## How it works, and how to write your own
**[Chapter 18 of the guide](../../../docs/18-tutorial-breakout-artwork.md)** builds this
program in twelve steps: loading artwork, budgeting the eight sprite slots, capturing a
drawing into one, stamping the brick field, finding the host's frame boundary with `TI#`,
moving everything that is not drawing out of the deadline, the stroke font, the bounce
without a square root, and the gems.
program step by step: loading artwork, budgeting the eight sprite slots, taking the text
layer out of the way so a drawing can be seen, stamping the brick field, registering the
bricks as collision geometry, the stroke font, the bounce without a square root, and the
gems.
[Chapter 17](../../../docs/17-tutorial-breakout.md) does the same for
[`../characters`](../characters), which builds the same game out of the text grid and two
@@ -71,18 +71,16 @@ Crediting Kenney is not required by CC0. It is here because it should be.
Named honestly rather than left to be discovered:
- **No music under the game**, only event sounds and two four-note stings. There is room in
the third voice for it; there was not much room anywhere else when it was written. 121 of
the interpreter's 128 variables are spoken for and the label table is at 61 of 64 —
several of them spent working around defects that have since been fixed.
the third voice for it; there was not much room anywhere else when it was written. The
conversion gave some of that room back — the variable table is under the 128 ceiling with
a handful to spare and the label table is at 57 of 64, where it was at 61.
- **One gem at a time.** There is one sprite slot for it. A brick broken while a gem is
falling drops nothing.
- **Sticky and multiball share one offset.** Two balls stuck to the paddle at once sit on
top of each other. It is rare enough that fixing it would cost a variable I do not have.
- **The score lags a frame behind the bricks.** Only one capture happens per frame and the
field goes first, so a brick disappears one frame before the score that counts it. At
thirty frames a second nobody can see it, and it is a deliberate trade rather than an
oversight.
- **No high score on disk.** There is no disk.
- **No attract mode**, unlike `../characters`. It sits on the title screen until somebody
presses space, which also means it cannot test itself unattended the way that one can.
The eight interpreter defects this game turned up are filed in `TODO.md` §9, each with a
reduction that fits on a screen and the file and line of the cause. **Six are now fixed**
@@ -92,8 +90,14 @@ drawing. The two that stand are the left-operand arithmetic rule, which turned o
decision rather than a defect and is now documented as one, and the 256-line batch tear,
which belongs to the host.
**The listing's code is unchanged**, and deliberately: it is what a program written
against those constraints looks like, and none of the workarounds costs anything now that
they are not needed. Six separate scalars for six brick stamps, and loops guarded by
`GOTO` rather than wrapped in a block, are both still what the file does. Its comments say
which of the five traps stand and which are marked FIXED.
**The listing has since been converted**, and its header records what went. It used to
spend two of its eight sprites on the screen itself — an `SSHAPE` of the HUD strip and one
of the whole play field, because a drawing lasted a single frame and a sprite was the only
thing redrawn for nothing. A drawing persists now, so both slots are free, the `PACE`
routine that hunted for a frame boundary is gone with the captures it protected, and the
flattened live list that made a full-field redraw cheap is gone with the redraw itself: a
broken brick is erased in place.
What has *not* changed is the shape of the workarounds that no longer cost anything — six
separate scalars for six brick stamps, loops guarded by `GOTO` rather than wrapped in a
block. Its comments say which of the five traps still stand and which are marked FIXED.

View File

@@ -145,9 +145,9 @@ DIM BRV#(6)
BRN# = 0
REM ------------------------------------------------------- shapes and dirt
REM SHN# counts the SSHAPE slots spent. There are sixteen, nothing gives
REM one back, and GRAPHIC 5 gives back all of them at once -- so the brick
REM stamps are thrown away and rebuilt every time the pool runs dry.
REM Eight of the sixteen SSHAPE slots, spent once in DRAWPROTOS and never
REM again: six brick stamps and the two erasers. Nothing captures per frame
REM any more, so the pool never runs dry and GRAPHIC 5 is never called.
REM One scalar per row colour, and scalars rather than an array because
REM SSHAPE and GSHAPE used to read the leaf they were handed without
REM applying its subscript -- a string array element always resolved to
@@ -160,8 +160,6 @@ S3$ = ""
S4$ = ""
S5$ = ""
Z$ = ""
REM 99 means the stamps are gone and have to be rebuilt before anything
REM can be drawn with them.
DHUD# = 1
DPLAY# = 1
BAN$ = ""
@@ -228,12 +226,6 @@ T3# = 0
T4# = 0
BX1# = 0
BY1# = 0
LFT# = 0
TOP# = 0
RGT# = 0
BOT# = 0
CC1# = 0
CC2# = 0
RR1# = 0
RR2# = 0
HIT# = 0
@@ -310,14 +302,10 @@ GOSUB TITLESCREEN
REM =====================================================================
REM The frame loop
REM
REM PACE first, because it is what puts the drawing at the top of a host
REM batch; then at most one capture, because a capture is the only thing
REM with a deadline; then the game, which may take as long as it likes.
REM At most one drawing job, then the game, which may take as long as it
REM likes. Nothing here has a deadline: a drawing that spans two host
REM batches simply arrives over two frames and the layer keeps both halves.
REM =====================================================================
REM No pacing. PACE spun on TI# until a batch boundary so that an SSHAPE
REM capture would not be cut in half by a present; nothing is captured now, so
REM a drawing that spans two batches simply arrives over two frames and the
REM layer keeps both halves.
LABEL FRAME
GOSUB DRAWJOB
GOSUB READKEYS
@@ -338,18 +326,17 @@ PRINT "FINAL SCORE " + SCORE# + " BEST " + HISCORE#
END
REM =====================================================================
REM One capture per frame, and only one
REM One drawing job per frame, and only one
REM
REM Rebuilding the brick stamps comes first, because everything else
REM draws with them and GRAPHIC 5 has just thrown them away.
REM The field is the expensive one -- sixty stamps and a border -- so it
REM never shares a frame with the HUD. Both are flagged rather than drawn
REM where the change happens, so a routine that changes three things costs
REM one drawing.
REM =====================================================================
REM Labels rather than BEGIN blocks here. When this was written it was not
REM a style choice -- a loop inside a block that was skipped left the
REM interpreter with no GOSUB to return from and stopped the program. That
REM is fixed; the shape is kept because it reads the same either way.
REM The stamps are built once now rather than counted and rebuilt. Nothing
REM captures per frame any more, so the sixteen SSHAPE slots are spent once and
REM stay spent -- eight of them -- and GRAPHIC 5 is never called.
LABEL DRAWJOB
LABEL DRAWJOB2
IF DPLAY# = 0 THEN GOTO DRAWJOB3
@@ -1002,10 +989,9 @@ FOR I# = 0 TO 59
BRK#(I#) = 0
NEXT I#
REM Retire the previous level's rectangles *before* laying this one out, not
REM after. This is where GOSUB BUILDLIVE used to sit, and it meant something
REM different: rebuild the list of what is standing. Left in place, the clear
REM ran after FILLROW had registered all sixty and threw them all away again --
REM which cost nothing visible and simply stopped the ball hitting anything.
REM after. Put after FILLROW it throws away all sixty rectangles that were
REM just registered, which costs nothing visible and simply stops the ball
REM hitting anything.
SOLID
FOR R# = 0 TO 5
IF R# < T3# THEN GOSUB FILLROW