Stop the listings and their chapters stating fixed defects as fact
Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m21s
akbasic CI Build / sanitizers (push) Failing after 4m30s
akbasic CI Build / coverage (push) Failing after 3m40s
akbasic CI Build / akgl_build (push) Failing after 21s
akbasic CI Build / mutation_test (push) Failing after 3m27s

You were right that this was still everywhere. The two `.bas` files carried
their workarounds as present-tense statements about the interpreter -- "a name
first created inside a GOSUB costs a value slot that is never handed back",
"writing PAST the terminator of a short row draws nothing at all", "SSHAPE and
GSHAPE ignore the subscript on a string array", "a FOR inside a BEGIN block does
not survive the RETURN" -- and every one of those is now false. A comment that
lies is worse than no comment, and these were the first thing anybody opening
the files would read.

**The characters game takes the geometry fix.** `CW# = 16` becomes `CW# =
RGR(3)`, and the grid comes from `RWINDOW`, so the game fits whatever window and
font the host gives it. That was the one thing in the listing that would break on
a different font, and `RGR(3)` and `RWINDOW` were added to the interpreter
because of it -- leaving the constant in place while Chapter 17 teaches the
function was the inconsistency worth closing. `RGR(1)` still comes first so a
build with no graphics device refuses by naming the device that is missing.

Everything else in both listings keeps its shape and says why. Declaring every
name up front, guarding loops with `GOTO`, six scalars for six brick stamps: none
is forced any more and none costs anything, so they stay, with the comments
marking which rules stand and which are history. The sprites header's five traps
are marked FIXED where they are fixed.

The chapters follow: Chapter 17 Step 2 no longer says "the listing still has
`CW# = 16`", and Chapter 18's trap section is "five things that did not do what
they looked like" with the two that still stand named up front rather than left
to be counted.

Verified: Chapter 17 Step 2's block is once again verbatim from the listing, so
every quoted fragment in both chapters matches its source with no exceptions;
both games run 40 seconds on the SDL frontend with no error line; both suites
green in both configurations; `docs_examples` and `docs_screenshots --check`
pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-02 06:32:48 -04:00
parent 89fca8007b
commit 0679c3042b
6 changed files with 120 additions and 78 deletions

View File

@@ -12,16 +12,23 @@ REM text grid and the sprites are both redrawn from persistent state every
REM frame. See README.md for the whole list of things that shaped this.
REM
REM Three rules this listing never breaks, each one earned the hard way:
REM * Every name is created once, in the pool block below -- variables,
REM loop counters, all of it. A name first created inside a GOSUB or a
REM FOR costs a value slot that is never handed back, and there are 4096
REM of them for the whole run. A game that creates one name per tick is
REM dead in half a minute. This one creates none after startup.
REM * A row of text is written whole, from column 0. CHAR terminates the
REM row where it stops, so writing at column N erases N+1 onwards -- and
REM writing PAST the terminator of a short row draws nothing at all.
REM row where it stops, so writing at column N erases N+1 onwards.
REM * Mixed + and - are parenthesised. This parser reads a - b + c as
REM a - (b + c), and matches only ONE unparenthesised AND or OR.
REM * The main loop is a GOTO loop. Every loop and every GOSUB takes one
REM of 32 scopes, and a state change jumping out of a DO would leak one
REM every time; thirty-two lives later the program stops.
REM
REM Two more rules were true when this was written and are not any more.
REM They are recorded because the shape of the listing is still theirs:
REM * Every name is created once, in the pool block below. A name first
REM created inside a GOSUB or a FOR used to cost a value slot that was
REM never handed back, out of 4096 for the whole run, and this game died
REM after twenty-five seconds because of it. A scalar is free now; only
REM the DIMs below still have to be up here. TODO.md section 6 item 30.
REM * Writing PAST the terminator of a short row used to draw nothing at
REM all. The gap is padded with spaces now. TODO.md section 6 item 32.
REM
REM Written by Claude Opus (1M)
REM ####################################################################
@@ -30,17 +37,23 @@ LABEL SETUP
SCNCLR
REM --- geometry -------------------------------------------------------
REM The cell is 16x16 and the grid is 50x37, measured with the bundled
REM C64_Pro_Mono at 16 points in the 800x600 window. BASIC cannot ask for
REM the cell size -- WINDOW would tell us, but the standalone frontend's
REM tee sink never offers it -- so these two are constants. Change them
REM together with the font or the window and everything else follows.
CW# = 16
CH# = 16
REM Nothing here is assumed. RGR(1) and RGR(2) are the window in pixels,
REM RGR(3) and RGR(4) are one character cell, and RWINDOW gives the text
REM grid in columns and rows -- so the game fits whatever window the host
REM opened and whatever font it loaded. Everything below is derived from
REM these six, including the ball's speed relative to the wall.
REM
REM The first version of this file had CW# = 16 and CH# = 16 written out,
REM measured by hand against the bundled C64_Pro_Mono at 16 points, because
REM there was no way to ask -- and that was the one thing in the listing
REM that would break on a different font. RGR(3) and RWINDOW were added to
REM the interpreter because of it.
SCW# = RGR(1)
SCH# = RGR(2)
COLS# = SCW# / CW#
ROWS# = SCH# / CH#
CW# = RGR(3)
CH# = RGR(4)
COLS# = RWINDOW(1)
ROWS# = RWINDOW(0)
REM --- the brick wall, in cells ---------------------------------------
BRW# = 4
@@ -60,18 +73,20 @@ TITROW# = 20
PSPD# = 10
REM --- pools ----------------------------------------------------------
REM Every name in the program is created HERE, and this is load-bearing
REM twice over.
REM Every name in the program is created HERE.
REM
REM A GOSUB gets its own scope. Assignment walks up the chain and finds an
REM outer variable, but a name first seen inside a subroutine is created in
REM the subroutine and dies at RETURN -- so HIT# and BI# would mean nothing
REM to the caller if they were not already out here.
REM One reason still stands. A GOSUB gets its own scope: assignment walks up
REM the chain and finds an outer variable, but a name first seen inside a
REM subroutine is created in the subroutine and dies at RETURN -- so HIT#
REM and BI# would mean nothing to the caller if they were not already out
REM here, and HITTEST would answer into two variables nobody can read.
REM
REM And a name that is created costs a value slot for good. The pool is a
REM bump allocator with no free (../akbasic/src/value.c:142) and recycling a
REM variable takes fresh slots, so 4096 creations ends the run whenever they
REM happen. Declared once, the whole game then runs on nothing but stores.
REM The other has been fixed. A scalar used to cost a value slot for good --
REM the pool is a bump allocator with no free, and recycling a variable took
REM fresh slots, so 4096 creations ended the run whenever they happened. A
REM scalar now lives in the variable itself and costs the pool nothing; only
REM the DIMs still spend, which is why they are the part that has to be up
REM here. The rest of this block is habit, and harmless.
DIM BR#(60)
DIM SB#(63)
DIM SP#(63)