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

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:
2026-08-01 22:54:42 -04:00
parent 8a50d07eef
commit cb0e2d0800
28 changed files with 4424 additions and 2 deletions

View File

@@ -25,7 +25,7 @@ scripting engine for game authors.
| [`MAINTENANCE.md`](MAINTENANCE.md) | Everything above. Start here | | [`MAINTENANCE.md`](MAINTENANCE.md) | Everything above. Start here |
| [`TODO.md`](TODO.md) | Outstanding defects, with file, line and consequence. §0.1 first — it retires the byte-for-byte fidelity constraint several later sections were written on | | [`TODO.md`](TODO.md) | Outstanding defects, with file, line and consequence. §0.1 first — it retires the byte-for-byte fidelity constraint several later sections were written on |
| [`README.md`](README.md) | What the project is and why, for somebody who has not seen it | | [`README.md`](README.md) | What the project is and why, for somebody who has not seen it |
| [`docs/`](docs/README.md) | The language itself: fourteen chapters, verb and function reference. [Chapter 14](docs/14-architecture.md) is the interpreter's architecture — the step loop, the pools, the two kinds of error, and how to debug it. [Chapter 15](docs/15-error-codes.md) is the error-code appendix | | [`docs/`](docs/README.md) | The language itself: eighteen chapters, verb and function reference. [Chapter 14](docs/14-architecture.md) is the interpreter's architecture — the step loop, the pools, the two kinds of error, and how to debug it. [Chapter 15](docs/15-error-codes.md) is the error-code appendix. [Chapters 17](docs/17-tutorial-breakout.md) and [18](docs/18-tutorial-breakout-artwork.md) are tutorials that build the games in `examples/breakout/` |
| `deps/libakerror/AGENTS.md` | The `ATTEMPT`/`CLEANUP`/`PROCESS`/`HANDLE`/`FINISH` protocol, authoritatively | | `deps/libakerror/AGENTS.md` | The `ATTEMPT`/`CLEANUP`/`PROCESS`/`HANDLE`/`FINISH` protocol, authoritatively |
| `deps/libakerror/UPGRADING.md` | 1.0.0's status registry. Required before writing an error code | | `deps/libakerror/UPGRADING.md` | 1.0.0's status registry. Required before writing an error code |
| `deps/<library>/AGENTS.md` | Per-repo rules. Read the relevant one **before editing a submodule** | | `deps/<library>/AGENTS.md` | Per-repo rules. Read the relevant one **before editing a submodule** |

View File

@@ -126,7 +126,7 @@ version are catalogued in [`TODO.md`](TODO.md) and summarised for a BASIC progra
| | | | | |
|---|---| |---|---|
| [`docs/`](docs/README.md) | The guide: fourteen chapters, the language then each hardware area then a reference section for every verb and function, [Chapter 14](docs/14-architecture.md) on the interpreter's own architecture, and [Chapter 15](docs/15-error-codes.md) listing every error code | | [`docs/`](docs/README.md) | The guide: eighteen chapters, the language then each hardware area then a reference section for every verb and function, [Chapter 14](docs/14-architecture.md) on the interpreter's own architecture, [Chapter 15](docs/15-error-codes.md) listing every error code, and [Chapters 17](docs/17-tutorial-breakout.md) and [18](docs/18-tutorial-breakout-artwork.md) building a whole game twice |
| [`MAINTENANCE.md`](MAINTENANCE.md) | For contributors and maintainers: the documentation-example harness, the three test lists, mutation testing, error-code allocation, style | | [`MAINTENANCE.md`](MAINTENANCE.md) | For contributors and maintainers: the documentation-example harness, the three test lists, mutation testing, error-code allocation, style |
| [`TODO.md`](TODO.md) | Outstanding defects, with file, line and consequence | | [`TODO.md`](TODO.md) | Outstanding defects, with file, line and consequence |
| [`tests/reference/README.md`](tests/reference/README.md) | Where the golden corpus came from, and the rule for changing it | | [`tests/reference/README.md`](tests/reference/README.md) | Where the golden corpus came from, and the rule for changing it |

424
TODO.md
View File

@@ -1858,6 +1858,147 @@ the reference's semantics reproduced faithfully; the third is the port's own.
reentrant" and "do not grow the runtime by a third for a scratch buffer" are both right reentrant" and "do not grow the runtime by a third for a scratch buffer" are both right
and picking between them is a decision. and picking between them is a decision.
### Found while writing a game against the interpreter
A complete Breakout, sprites and text grid and keyboard, running for minutes at a time. Nothing
in either corpus runs for minutes, which is why the first of these had never been seen.
30. **A variable created inside a scope costs pool slots that never come back, so a long-running
program has a hard budget of 4096 name creations.** `akbasic_ValuePool` is a bump allocator
on purpose (`include/akbasic/value.h:30`), and the reason given is that "nothing in BASIC
destroys a variable". **Scope exit destroys variables.**
`akbasic_runtime_prev_environment()` (`src/runtime.c:121`) marks a popped environment's
variables `used = false`; `akbasic_runtime_new_variable()` (`src/runtime.c:31`) `memset`s
the slot it hands back, clearing `values` and `valuecount`; and `akbasic_variable_init()`
(`src/variable.c:98`) therefore takes *fresh* slots for a variable whose old ones are still
counted against `obj->next`. Nothing decrements it, so every `GOSUB` that creates a local
leaks the local's slots for the life of the run.
Reduced, and it fails on both builds:
```basic
X# = 0
FOR T# = 1 TO 6000
GOSUB SUBA
NEXT T#
PRINT "OK " + X#
END
LABEL SUBA
LOC# = 1
X# = X# + LOC#
RETURN
```
`? 110 : RUNTIME ERROR Array of 1 elements does not fit in the 0 remaining value slots`,
at `LOC# = 1`, on the 4091st call -- 4096 less the handful of names the program itself
declared. Both builds, same line, same count. Declare `LOC#` alongside `X#` and the same
6000 calls cost nothing at all. A `FOR` counter behaves the same way: name one that does
not already exist and every entry to the loop spends a slot.
**What it costs a program:** a game loop that creates one name per tick is dead in half a
minute, and the error names the line that happened to be unlucky rather than the line that
caused it. Breakout ran twenty-five seconds before this was understood. The workaround is
real and not onerous -- declare every name, loop counters included, at the top -- but it
has to be *known*, and right now the only way to learn it is to hit it.
Three ways out, in increasing order of work. Reclaim on scope exit: give the pool a mark
and release, take the mark when an environment is pushed and rewind to it when it pops,
which is exactly the discipline the environments themselves already have and is why they
are a pool in the first place. Or keep the slice on the recycled variable: stop the
`memset` in `new_variable()` from clearing `values`/`valuecount` and let
`variable_init()`'s existing "reuse the slice when it is already big enough" branch do the
work -- much smaller, and it fixes the common case of the same-sized scalar being recreated
over and over. Or, at minimum, say so in the manual and in `value.h`'s comment, which
currently states the opposite.
Whichever is chosen, the test is a CTest program that enters and leaves a scope creating a
variable tens of thousands of times and returns zero -- and it wants to exist even for the
documentation-only outcome, pinned at whatever the real budget turns out to be. Item 33
has to be settled with it: while the pool is dry the failure cannot be trapped, so a
program cannot even report this one against itself.
31. **`WINDOW` cannot be reached in the standalone AKGL build.** `akbasic_sink_init_tee()`
(`src/sink_tee.c:143`) wires `write`, `writeln`, `readline`, `clear` and -- conditionally
-- `moveto`, and never sets `window`. The standalone frontend runs the interpreter against
the tee (`src/frontend_akgl.c:215`), so the fully implemented `sink_window()` underneath it
(`src/sink_akgl.c:413`) is unreachable: `WINDOW 0, 0, 10, 10` in the windowed build answers
`? RUNTIME ERROR WINDOW needs a text device with a character grid, and this one has none`.
The verb is only usable by a host that hands the runtime the akgl sink directly.
It looks like an oversight rather than a decision, because `moveto` right above it *is*
forwarded, and with the same reasoning that would apply here. The fix is a `tee_window()`
that forwards to whichever half offers one, offered only when one does, and a case in
`tests/sink_tee.c` beside the `moveto` one. §3's "Still missing from the sink" closes by
calling `WINDOW` "ordinary work here rather than anything blocked", which is now half true:
the work was done and then not plumbed.
Worth deciding at the same time: **a program still has no way to ask how big the text grid
is.** `RGR(1)` and `RGR(2)` give the window in pixels, and nothing gives columns, rows or
the cell size, so anything that wants to place a character *and* a sprite at the same spot
has to hardcode a cell size measured against the bundled font -- which is what the Breakout
in `examples/breakout/characters` does, and it is the one thing in that listing that will break
on a different font or window. Two more `RGR()` indices would answer it.
32. **A character written past a short row's terminator is stored where nothing will render
it.** A grid row is a NUL-terminated string and `putchar_at()` (`src/sink_akgl.c:89`)
writes the character, advances, and terminates -- so `CHAR 1, 40, 1, "#"` on an empty row
stores `#` at column 40 with `text[1][0]` still `'\0'`, and `sink_akgl_render()` draws
nothing at all. The same call on a row already 50 characters long draws the `#` and erases
columns 41 onward, which is the documented truncation and is fine.
It is the *silent nothing* that is the trap: the write succeeds, the cursor moves, stdout's
mirror shows the character, and the window stays blank. Either pad the gap with spaces on
a `moveto` past the terminator, so a positioned write always lands, or say plainly beside
`CHAR` in `docs/11-verb-reference.md` and deviation 49 that a row is written from column 0
or not at all. Padding is the friendlier of the two and costs one loop in `sink_moveto()`;
the argument against it is that it makes `moveto` write to the grid, which it currently
does not.
33. **A `TRAP` handler cannot be entered once the value pool is dry, and the error is dropped
in silence.** `akbasic_trap_set_error_variables()` (`src/runtime_trap.c:36`) sets `ER#`
and `EL#` through `akbasic_runtime_global()` (`:51` and `:53`), which *creates* them if
the program never named them. Creating them needs pool slots. So the one error most
likely to leave the pool empty -- item 30's -- is the one error whose handler cannot be
reached, and because the dispatch fails inside the error path rather than raising, **the
program keeps running with the failed statement's effect quietly missing**.
It is easy to see, and it is much worse than an abort:
```basic
X# = 0
TRAP RANOUT
FOR T# = 1 TO 6000
GOSUB SUBA
NEXT T#
PRINT "OK " + X#
END
LABEL SUBA
LOC# = 1
X# = X# + LOC#
RETURN
LABEL RANOUT
PRINT "DIED AT X " + X#
QUIT
```
prints `OK 4092`. The handler never ran, nothing was reported, and `X#` is 1908 short of
the 6000 the loop counted -- a wrong answer delivered as a right one. Add `ER# = 0` and
`EL# = 0` at the top, so the dispatch has nothing left to create, and the same program
prints `DIED AT X 4090` from its handler as it should.
Two things want fixing and they are separable. The dispatch should not allocate: create
`ER#` and `EL#` at runtime init, where there is always room, rather than at the moment the
program is in trouble -- they are reserved names and §1's other reserved globals are
already there. And a failure *inside* the trap dispatch should not be swallowed; if the
handler cannot be entered, the original error is the thing to report, and reporting it is
what the untrapped path already does correctly.
`AKBASIC_ERR_BOUNDS` from the pool is documented as "bounded and diagnosable, which is the
point" (`include/akbasic/value.h:34`). With a `TRAP` armed it is currently neither.
## 7. Filing gaps against `libakgl` ## 7. Filing gaps against `libakgl`
When phase 7 or phase 8 needs something `libakgl` does not have, **stop and file it** in When phase 7 or phase 8 needs something `libakgl` does not have, **stop and file it** in
@@ -2285,3 +2426,286 @@ What remains, in priority order:
own body said the fill was filed rather than implemented. Drawing the figure and getting own body said the fill was filed rather than implemented. Drawing the figure and getting
an outline back is what caught it. `akbasic_GraphicsBackend::filled_rect` is implemented, an outline back is what caught it. `akbasic_GraphicsBackend::filled_rect` is implemented,
recorded by the mock, and reached by no verb at all as a result. recorded by the mock, and reached by no verb at all as a result.
---
## 9. Defects found by writing a game against it
`examples/breakout/sprites/breakout.bas` is a complete Breakout — sprite artwork,
powerups, a stroke-font HUD — written in this dialect, for this interpreter, by somebody who
had not written a line of it before. Eight things came out of that, and they are here for the
same reason §8's three are: **none of them was on any list, and all of them were found by
using the thing rather than by testing it.** The corpus reaches none of them.
Ordered by blast radius. Each has a minimal reproduction that fits on a screen; every one was
reduced against `build/basic`, the stdio build, unless it says otherwise.
1. **Every call to a user-defined function leaks value-pool slots, so a program may call one
about four thousand times and then die.**
```basic
DEF ADDIT(N#)
RETURN N# + 1
R# = 0
FOR I# = 1 TO 8000
R# = ADDIT(I#)
NEXT I#
```
```output
? 5 : RUNTIME ERROR Array of 1 elements does not fit in the 0 remaining value slots
```
It fails between 4000 and 5000 calls, which is `AKBASIC_MAX_ARRAY_VALUES` (4096,
`include/akbasic/types.h:29`) at one slot per call — the call scope's parameter. **Both
`DEF` forms leak**; the single-expression `DEF SQR1(X#) = X# * X#` fails at the same point.
A `GOSUB` doing the same work runs eight thousand times without complaint, which is the
comparison that isolates it.
`akbasic_valuepool_take()` (`src/value.c:142`) is a bump allocator — `obj->next += count` at
`:160` — and **nothing anywhere returns a slot**. The only reset is
`akbasic_valuepool_init()`, from `src/runtime.c:209` at startup and
`src/runtime_housekeeping.c:53` for `CLR`/`NEW`. Environments *are* released back to their
own pool (§1.4, and §6 item 11 says so), but the value slots the variables in them held are
not: `src/environment.c:321` takes them through `akbasic_variable_init()` and the release
path has no counterpart.
The consequence is a hard ceiling on a whole language feature. A game calling one function
per frame at 30fps gets a little over two minutes. It is why the game in question spells its
pseudo-random generator as a `GOSUB` over globals rather than the `DEF` it wants to be.
Fixing it means giving the pool a free list, or giving each environment a value arena
released with it. The second is closer to the house style and to what §1.4 already does for
environments; it touches `src/value.c`, `src/variable.c`, `src/environment.c` and
`include/akbasic/value.h`. A test belongs in `tests/user_functions.c`: call a `DEF` ten
thousand times and then `DIM` an array.
2. **A `BEGIN` block that is skipped leaks a scope for every loop inside it**, because `FOR`
and `DO` create their environment at *parse* time and the skip is decided at *evaluate*
time.
```basic
N# = 0
LABEL AGAIN
N# = N# + 1
IF 1 = 0 THEN BEGIN
FOR I# = 0 TO 2
N# = N# + 1000
NEXT I#
BEND
IF N# < 40 THEN GOTO AGAIN
```
```output
? 5 : PARSE ERROR Environment pool exhausted at line 5 (32 in use)
```
Thirty-two skips is `AKBASIC_MAX_ENVIRONMENTS` (`include/akbasic/types.h:31`) at one per
skip. Inside a subroutine it announces itself on the very first skip and much more
confusingly — the orphaned scope sits between the routine and its caller, so the `RETURN`
after the block reports from a routine that plainly *was* entered by a `GOSUB`:
```basic
T# = 0
GOSUB DOIT
PRINT "came back"
END
LABEL DOIT
IF 1 = 0 THEN BEGIN
FOR I# = 0 TO 2
T# = T# + 1
NEXT I#
BEND
RETURN
```
```output
? 11 : RUNTIME ERROR RETURN outside the context of GOSUB
```
That is the form it was found in, and it costs an evening because the error names the one
construct that is not at fault.
**The cause is an ordering mismatch and it is exact.** `akbasic_parse_do()`
(`src/parser_commands.c:266`) and `akbasic_parse_for()` (`:849`) both call
`akbasic_runtime_new_environment()` while parsing the line. The block skip is tested in
`akbasic_runtime_interpret_line()` (`src/runtime.c:767`), which runs *after* the line has
been parsed — so a skipped `FOR` pushes a scope, its execution is skipped, and the `NEXT`
that would pop it is skipped too. `BEGIN` itself pushes nothing, which is why a nested
`BEGIN`/`BEND` inside a skipped block is harmless, and a `GOSUB` inside one is harmless for
the same reason.
Confirmed for both loop forms: `DO`/`LOOP` in a skipped block fails identically. Confirmed
for both kinds of enclosing routine: a multi-line `DEF` reports the same thing.
A fix has to either push the loop's scope at execution rather than at parse, or have the
skip release what parsing pushed. Touches `src/parser_commands.c`, `src/runtime.c` and
`tests/structure_verbs.c`, which today only exercises blocks whose bodies are plain
statements.
3. **In the standalone SDL build, nothing the graphics verbs draw can ever be seen.** Not
"does not persist" — *never visible at all*, in any frame.
```basic
GRAPHIC 1, 1
COLOR 1, 3
BOX 1, 100, 300, 700, 500
PRINT "THE TEXT SHOWS AND THE BOX DOES NOT"
LABEL SPIN
GOTO SPIN
```
The text appears; the box never does. Two mechanisms combine, and each alone would be
survivable:
- `akbasic_sink_akgl_render()` fills **every row of the text area, opaque, every frame**
(`src/sink_akgl.c:564`), and the area is the whole window — `state->rows = h / cellh` at
`:505`. `akbasic_frontend_akgl_pump()` calls it after `akbasic_runtime_run()` and before
`SDL_RenderPresent`, so it paints over everything the program drew during those steps.
The comment at `:564` explains why it must repaint rather than track dirty rows, and that
reasoning is sound; what it does not account for is that the rows it owns are all of them.
- `WINDOW` would shrink the text area out of the way, and the akgl sink implements it
(`sink_window`, `src/sink_akgl.c:413`) — but **the tee sink in front of it never assigns
`obj->window`** (`src/sink_tee.c:143-151` assigns `clear` and conditionally `moveto`, and
stops). So `WINDOW` refuses with "needs a text device with a character grid"
(`src/runtime_console.c:179`) in the one build that has a character grid.
The result is that **chapter 6 cannot be run in the interpreter it documents**. Its figures
are right because `tools/screenshot.c` deliberately omits the text layer — the comment in
that file says so — so the harness that proves the chapter is exactly the harness that hides
this. `docs_screenshots` passes and always would.
Sprites are unaffected: `akbasic_sprite_akgl_render()` runs after the sink, which is why the
game draws its entire screen by `SSHAPE`-ing what it drew and installing it with `SPRSAV`.
That is a fine trick and it should not be the only way.
The tee half is three lines and is worth doing whatever is decided about the rest.
4. **Mixed-type arithmetic is decided by the left operand alone, and nothing says so.**
```basic
A# = 3
PRINT A# * 0.45
PRINT 0.45 * A#
```
```output
0
1.350000
```
`akbasic_value_math_plus()` and its siblings branch on `self->valuetype`
(`src/value.c:510`, `:512`, `:539`) and convert the right operand to match, so an integer on
the left silently truncates a float on the right. **This is inherited, not introduced** —
`deps/basicinterpret/basicvalue.go:190-193` has the same shape — and §6 item 5 already fixed
the other half of that expression without changing the branch.
It may well be intended; a dialect is allowed to say the left operand wins, and this one is
consistent about it. The defect is that **it is documented nowhere**. Chapter 3's "Numbers"
does not mention it, chapter 13 does not list it among the differences, and a C128 promotes
to float instead. Nothing fails when you get it wrong — the program computes something else
and carries on.
It cost the game two live bugs before either was noticed. `SPD% = 5.6 + LEVEL# * 0.45`
evaluated to a flat 5.6, so no level ever ran faster than level one; and `0 - BLVX%(B#)`,
the obvious way to reverse a ball, quantised its velocity to whole pixels on every bounce
and bled speed out of it. Both read correctly. Neither produced a diagnostic.
The cheap fix is documentation: a paragraph in chapter 3 and a row in chapter 13. The other
fix is promotion, which would be a real change of semantics and wants its own decision.
5. **A drawing that spans a 256-line batch boundary is torn, not merely transient.** §5 and
chapter 13 record that drawing does not persist across frames. What neither says is that a
single uninterrupted sequence of drawing verbs longer than one `akbasic_runtime_run()` budget
comes back **half drawn**, because the present in the middle of it discards the buffer — so
an `SSHAPE` at the end captures only the statements issued since that present, over whatever
the frame before left behind.
Measured against `AKBASIC_FRONTEND_STEPS_PER_FRAME` of 256, in the SDL build: after
synchronising to a jiffy edge, 110 `DRAW` statements in a `FOR` loop (220 executed lines)
are captured whole; 125 (250 lines) capture nothing at all; 140 (280 lines) capture the last
fifteen. A program cannot see the boundary except by watching `TI#` change, which is what
the game's `PACE` routine does and what makes the whole thing workable.
This is worth a paragraph in chapter 13 beside the existing note, because "redraw it every
frame" is the advice there and it is not sufficient advice — the redraw also has to fit.
6. **`SSHAPE` and `GSHAPE` ignore the subscript on a string array element.** Needs a graphics
device, so this one is reduced against `build-akgl/basic`.
```basic
DIM SH$(4)
SSHAPE SH$(2), 0, 0, 61, 4
PRINT "[" + SH$(0) + "] [" + SH$(2) + "]"
```
```output
[SHAPE:0] []
```
The handle lands in `SH$(0)`; `SH$(2)` stays empty. `GSHAPE SH$(2)` then stamps whatever is
in `SH$(0)`. Ordinary assignment and `PRINT` honour the subscript, so a program that keeps
several saved shapes in an array gets every one of them resolving to element zero, silently.
`shape_variable()` (`src/runtime_graphics.c:625`) takes `arg->identifier` and calls
`akbasic_environment_get()` — it never evaluates the subscript — and both verbs then use a
literal zero: `src/runtime_graphics.c:688` writes with `zerosubscript`, `:724` reads with it.
**`SPRSAV` is the counter-example and the model for the fix**: it calls
`akbasic_runtime_evaluate()` on its argument (`src/runtime_sprite.c:480`) and handles an
array element correctly.
The game keeps six brick stamps in six separate scalars because of this, and the workaround
is not obvious from the failure — every brick simply comes out the colour of the last one
captured. `tests/graphics_verbs.c` is where the regression goes.
7. **`GRAPHIC CLR` is documented and refused.** `docs/06-graphics.md:65` and
`docs/11-verb-reference.md:54` both give the form as `GRAPHIC mode | CLR`.
```basic
GRAPHIC CLR
```
```output
? 1 : PARSE ERROR 1 at 'CLR', Expected expression or literal
```
`GRAPHIC` parses with `akbasic_parse_arglist` (`src/verbs.c:86`), so `CLR` scans as the `CLR`
verb rather than as a keyword argument. `GRAPHIC 5` does the job and is what the game calls.
Either the parse handler learns the word or both documents lose it; the second is smaller and
the first is what a C128 programmer will type.
Worth noting because `docs_examples` cannot catch it: every fenced `GRAPHIC CLR` in the
documentation is prose, not a tagged runnable block.
8. **A `DATA` line holding a negative number cannot be executed.** §4 settled that "`DATA` at
run time is now a no-op: it is a declaration, and reaching the statement means walking past
it." Walking past a negative one raises instead:
```basic
READ A#
PRINT "READ GAVE " + A#
DATA -5
PRINT "REACHED THE LINE AFTER THE DATA"
```
```output
READ GAVE -5
? 3 : PARSE ERROR Expected literal
```
`READ` handles the value correctly — the negative comes back intact — so this is only the
statement's own argument parse, in `akbasic_parse_data()`
(`src/parser_commands.c:934`), refusing a leading `-` where the prescan accepted it. The
positive form falls through cleanly.
Reachable in ordinary code: a table of `DATA` at the foot of a program is walked into by any
routine above it that ends in a fall-through rather than an `END`, and a table of angles or
offsets is exactly where negative numbers live. `tests/read_data.c`.
**A note on what this list is.** Items 1, 2, 6, 7 and 8 are defects by any reading. Item 3 is a
design consequence nobody chose. Item 4 may be intended and is a documentation gap either way.
Item 5 sharpens something already recorded. None of them is a complaint about the interpreter,
which was a pleasure to write a real program against — they are the kind of thing one real
program finds and a test suite written alongside the implementation structurally cannot,
because the suite tests the constructs the implementer had in mind and a game reaches for
whatever it needs.

View File

@@ -13,6 +13,12 @@ name, and a machine with no sound card still runs the interpreter — only `SOUN
Voice, frequency, duration. Voices are numbered from 1. The duration is in *jiffies* — Voice, frequency, duration. Voices are numbered from 1. The duration is in *jiffies* —
sixtieths of a second — as on a C128, so 60 is one second. sixtieths of a second — as on a C128, so 60 is one second.
**The frequency is a SID register value, not hertz.** As on a C128 the pitch is
`frequency * 1022730 / 16777216` — the NTSC system clock over the oscillator's 24-bit
accumulator — so the 4000 above is about 244 Hz, and the argument ranges from 0 to
65535. Work the notes you need out once and write the arithmetic down beside them: 4298
is C4, 8579 is C5, 17175 is C6. `PLAY` takes note names and does the conversion for you.
Further arguments give a frequency sweep: a step, a direction and a range, which is Further arguments give a frequency sweep: a step, a direction and a range, which is
what makes a siren or a laser. what makes a siren or a laser.

View 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
```
![](images/breakout-paddle.png)
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.

View File

@@ -0,0 +1,796 @@
# 18. Tutorial: Breakout with artwork
[Chapter 17](17-tutorial-breakout.md) built Breakout out of text and two `DATA` sprites.
This chapter builds it again out of **downloaded artwork**, with powerups, a coloured HUD
and three voices of sound — and almost nothing about the shape of the program survives the
change. The finished listing is
[`examples/breakout/sprites/breakout.bas`](../examples/breakout/sprites/breakout.bas).
Read Chapter 17 first if you have not. The rules it teaches — declare every name up front,
loop with `GOTO`, parenthesise mixed `+` and `-` — all still apply here and are not
repeated.
```sh norun
$ ./build-akgl/basic examples/breakout/sprites/breakout.bas
```
| Key | Does |
|---|---|
| left / right | move the paddle |
| space | start a game, launch the ball, release a stuck ball |
| P | pause |
| S | sound on and off |
| Q or escape | quit |
A broken brick drops a gem about one time in seven. Catch it with the paddle; the colour
tells you which it is.
| Gem | Name | Does | For |
|---|---|---|---|
| red | EXPAND | doubles the paddle's width | 20 seconds |
| yellow | MULTI | throws two more balls off the one in play | until they are lost |
| green | SLOW | drops the ball's speed to about two thirds | 16 seconds |
| blue | STICKY | the ball sticks where it lands; space fires it | 18 seconds |
| purple | CATCH | a second bar appears higher up the field | 24 seconds |
---
## Step 1: Put the artwork on the screen
`SPRSAV` loads an image file straight into a sprite slot, and a sprite loaded that way
keeps **the image's own size** rather than being forced to 24 by 21 — see
[Chapter 8](08-sprites.md#from-an-image-file).
```basic requires=akgl setup=breakout_art screenshot=breakout-artwork
I# = 0
SPRSAV "art/paddleBlu.png", 3
SPRSAV "art/paddleRed.png", 4
SPRSAV "art/ballBlue.png", 5
SPRSAV "art/element_red_polygon_glossy.png", 6
SPRSAV "art/element_green_polygon_glossy.png", 7
SPRSAV "art/element_purple_polygon_glossy.png", 8
FOR I# = 3 TO 8
SPRITE I#, 1, 2
NEXT I#
MOVSPR 3, 20, 20
MOVSPR 4, 20, 60
MOVSPR 5, 160, 30
MOVSPR 6, 30, 120
MOVSPR 7, 130, 120
MOVSPR 8, 230, 120
```
![](images/breakout-artwork.png)
That is the whole game's cast: two bars, a ball and five gems. The path is tried against
the working directory first and then against the directory the program was loaded from,
so a `.bas` stored beside its `art/` runs from anywhere.
The artwork here is Kenney's [Puzzle Pack 1](https://kenney.nl/assets/puzzle-pack-1),
released under [CC0](http://creativecommons.org/publicdomain/zero/1.0/);
`examples/breakout/sprites/art/PROVENANCE.md` records which file is used for what.
Crediting Kenney is not required by CC0 — do it anyway.
**`SPRITE n, 1, 2` turns a sprite on in colour 2.** A sprite's colour *multiplies* the
artwork rather than replacing it, so colour 2 (white) is what leaves the artwork looking
like itself.
## Step 2: Budget the eight sprite slots before you write anything else
There are eight sprites. That is not a limit you will design your way around, so decide
what they are first:
| Slot | Is |
|---|---|
| 1 | the HUD strip — a captured drawing |
| 2 | the playing field — a captured drawing |
| 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**, and Step 3 is why. That leaves six for everything
else, which is the reason **the bricks are drawn rather than made of artwork** — sixty of
them will not fit in six slots, and there is no way to get artwork onto the screen other
than a sprite. `GSHAPE` cannot stamp a sprite and `SPRSAV` cannot read one back out.
It is also the reason for "one gem at a time": there is one slot for it, so a brick
broken while a gem is falling drops nothing.
## Step 3: Turn what you drew into a sprite
In the standalone SDL build the text layer repaints every row of the window, opaque,
after your program's steps have run and before the frame is presented — so **anything
`DRAW`, `BOX` or `CIRCLE` puts on the screen is painted over before anybody sees it**.
Sprites are drawn after the text layer. A sprite is the only thing on the screen a
program can rely on being visible. (`TODO.md` §9 item 3; the figures in this chapter are
rendered by a tool that omits the text layer, which is why they can show a drawing at
all.)
That leaves exactly one way to put a picture up: **draw it, capture it with `SSHAPE`,
install the capture with `SPRSAV`.**
```basic norun
SSHAPE Z$, 0, 60, 800, 600
SPRSAV Z$, 2
SPRITE 2, 1, 2
MOVSPR 2, 0, 60
```
Four lines, and they are the last four of every draw routine in the game. `Z$` holds a
handle rather than pixels — see [Chapter 6](06-graphics.md#saving-and-stamping-regions) —
which is all `SPRSAV` needs.
### Stamp the bricks; do not paint them
Draw one brick per colour, capture the six of them, and stamp them with `GSHAPE`:
```basic requires=akgl screenshot=breakout-stamps size=100x130
DIM BRC#(6)
I# = 0
R# = 0
K# = 0
T1# = 0
T2# = 0
FOR I# = 0 TO 5
READ BRC#(I#)
NEXT I#
GRAPHIC 1, 1
WIDTH 1
FOR R# = 0 TO 5
COLOR 1, BRC#(R#)
T1# = R# * 20
T2# = T1# + 8
FOR K# = 0 TO 7
DRAW 1, 0, T1# + K# TO 67, T1# + K# : DRAW 1, 0, T2# + K# TO 67, T2# + K#
NEXT K#
NEXT R#
DATA 3, 9, 8, 6, 4, 5
```
![](images/breakout-stamps.png)
Six 68 by 16 bricks, filled **two scan lines at a time** so the set costs about a hundred
and thirty lines rather than the two hundred and seventy a line-at-a-time loop would take.
Step 4 explains why that number matters.
**`PAINT` would be one statement instead of sixteen and is not an option.** It costs
nearly four milliseconds a call; sixty of those is seven frames.
`SSHAPE` has sixteen slots, nothing gives one back, and `GRAPHIC 5` gives back all of
them at once. So the game counts what it has spent and rebuilds the stamps from scratch
whenever the pool runs dry:
```basic norun
LABEL DRAWJOB
IF SHN# < 14 THEN GOTO DRAWJOB2
GOSUB DRAWPROTOS
RETURN
```
### Flatten the field before you draw it
The draw routine should not be deciding anything. When a brick breaks, walk the grid and
write out a **list of the bricks still standing**, row by row — so a row is a contiguous
run of that list, and drawing it is a stamp and an advance:
```basic norun
LABEL BUILDLIVE
LN# = 0
FOR R# = 0 TO 5
RS#(R#) = LN#
RC#(R#) = 0
GOSUB BUILDROW
NEXT R#
RETURN
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#
RETURN
```
`RS#(R#)` is where row `R#`'s run starts and `RC#(R#)` is how long it is. This costs about
four lines a brick and has all the time in the world; the draw costs two and has a
deadline. **That trade is the spine of this program** and it comes back in Step 6 for the
lettering.
Now the whole screen, drawn and captured and dressed with artwork:
```basic requires=akgl setup=breakout_art screenshot=breakout-screen size=800x600
DIM BRC#(6)
I# = 0
R# = 0
C# = 0
K# = 0
T1# = 0
T2# = 0
Z$ = ""
FOR I# = 0 TO 5
READ BRC#(I#)
NEXT I#
GRAPHIC 1, 1
WIDTH 1
FOR R# = 0 TO 5
COLOR 1, BRC#(R#)
T1# = R# * 20
T2# = T1# + 8
FOR K# = 0 TO 7
DRAW 1, 0, T1# + K# TO 67, T1# + K# : DRAW 1, 0, T2# + K# TO 67, T2# + K#
NEXT K#
NEXT R#
SSHAPE Z$, 0, 0, 68, 16 : S0$ = Z$
SSHAPE Z$, 0, 20, 68, 36 : S1$ = Z$
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$
GRAPHIC 1, 1
WIDTH 2
COLOR 5, 16 : COLOR 1, 4
BOX 5, 2, 62, 797, 597
BOX 1, 6, 66, 793, 593
FOR C# = 0 TO 9
Z$ = S0$ : GSHAPE Z$, 42 + C# * 72, 108
Z$ = S1$ : GSHAPE Z$, 42 + C# * 72, 132
Z$ = S2$ : GSHAPE Z$, 42 + C# * 72, 156
Z$ = S3$ : GSHAPE Z$, 42 + C# * 72, 180
Z$ = S4$ : GSHAPE Z$, 42 + C# * 72, 204
Z$ = S5$ : GSHAPE Z$, 42 + C# * 72, 228
NEXT C#
SSHAPE Z$, 0, 60, 800, 600
SPRSAV Z$, 2
SPRITE 2, 1, 2
MOVSPR 2, 0, 60
SPRSAV "art/paddleBlu.png", 3
SPRSAV "art/ballBlue.png", 5
SPRITE 3, 1, 2
SPRITE 5, 1, 2
MOVSPR 3, 348, 540
MOVSPR 5, 389, 517
DATA 3, 9, 8, 6, 4, 5
```
![](images/breakout-screen.png)
Everything above the last four lines is a drawing nobody would ever see. `SSHAPE` and
`SPRSAV` are what make it the screen.
The game keeps its six stamps in **six separate scalars** rather than an array, and that
is not a style choice — see the fourth trap at the end of this chapter.
## Step 4: Find the frame boundary
The host runs **256 source lines and then presents the frame**, and presenting throws the
drawing buffer away. So everything between a `GRAPHIC 1, 1` and its `SSHAPE` has to
happen inside one of those batches. Draw more than that and the capture comes back
holding only the tail of what you drew, over whatever the frame before it left behind —
which looks exactly like a ghost.
`TI#` is refreshed from the host's clock once per batch, so **the step on which `TI#`
changes is the first step of a batch**. Spinning until it changes is the only way a
program in this dialect can locate 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 finds the time already spent, returns immediately from
somewhere in the middle of a batch, and the capture that follows is ruined. 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. Every draw routine in the game is written to stay near 200.
**Arithmetic is free.** A routine that computes for two thousand steps costs frame rate
and nothing else. Only drawing has a deadline — which is what makes Step 3's flattening
and Step 6's stroke lists worth their complexity.
## Step 5: Do at most one capture per frame
The frame loop paces first, then draws at most one thing, then plays the game:
```basic norun
LABEL FRAME
GOSUB PACE
GOSUB DRAWJOB
GOSUB READKEYS
IF STATE# = 0 THEN GOSUB TITLETICK
IF STATE# = 1 THEN GOSUB SERVETICK
IF STATE# = 2 THEN GOSUB PLAYTICK
IF STATE# = 3 THEN GOSUB LOSTTICK
IF STATE# = 4 THEN GOSUB CLEARTICK
IF RUNNING# = 0 THEN GOTO SHUTDOWN
GOTO FRAME
```
`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
RETURN
LABEL DRAWJOB3
IF DHUD# = 0 THEN RETURN
GOSUB DRAWHUD
DHUD# = 0
RETURN
```
The game sets `DPLAY# = 1` or `DHUD# = 1` when something changes and never draws
directly. One consequence is visible and deliberate: **the score lags the bricks by one
frame**, because the field goes first. At thirty frames a second nobody can see it.
Those are `LABEL`s and `GOTO`s rather than `BEGIN` blocks, and again that is not a style
choice — a `RETURN` inside a block leaves the interpreter with no `GOSUB` to return from.
The last trap in this chapter is exactly that.
## Step 6: Draw the lettering, because text has no colour
The interpreter's text sink is white and has no verb that changes it; `CHAR` parses a
colour argument and ignores it. A coloured HUD therefore has to be *drawn*, which means
carrying a font.
The one here is four units wide and seven tall, one glyph per `DATA` line: how many
strokes, then that many pairs of points. **A point is coded `X * 10 + Y`**, so 0 is the
top-left corner, 30 the top right and 36 the bottom right.
```basic requires=akgl screenshot=breakout-lettering size=260x110
DIM FNC#(5)
DIM FNI#(5)
DIM FNS#(60)
I# = 0
K# = 0
N# = 0
D# = 0
GP# = 0
GX# = 0
GX2# = 0
P1# = 0
P2# = 0
X1# = 0
Y1# = 0
X2# = 0
Y2# = 0
FOR I# = 0 TO 4
READ N#
FNC#(I#) = N#
FNI#(I#) = GX#
GOSUB READGLYPH
NEXT I#
GRAPHIC 1, 1
WIDTH 2
COLOR 1, 8
SZ# = 9
FOR I# = 0 TO 4
GOSUB DRAWGLYPH
NEXT I#
END
LABEL READGLYPH
FOR K# = 1 TO N# * 2
READ D#
FNS#(GX#) = D#
GX# = GX# + 1
NEXT K#
RETURN
LABEL DRAWGLYPH
GP# = FNI#(I#)
GX2# = 20 + I# * SZ# * 5
FOR K# = 1 TO FNC#(I#)
P1# = FNS#(GP#)
P2# = FNS#(GP# + 1)
GP# = GP# + 2
X1# = GX2# + (P1# / 10) * SZ#
Y1# = 20 + MOD(P1#, 10) * SZ#
X2# = GX2# + (P2# / 10) * SZ#
Y2# = 20 + MOD(P2#, 10) * SZ#
DRAW 1, X1#, Y1# TO X2#, Y2#
NEXT K#
RETURN
REM S
DATA 5, 0,30, 0,3, 3,33, 33,36, 6,36
REM C
DATA 3, 0,30, 0,6, 6,36
REM O
DATA 4, 0,30, 6,36, 0,6, 30,36
REM R
DATA 5, 0,6, 0,30, 3,33, 30,33, 13,36
REM E
DATA 4, 0,6, 0,30, 3,33, 6,36
```
![](images/breakout-lettering.png)
`SZ#` is the scale, so the same table draws a 12-unit `BREAKOUT` on the title screen and
a 4-unit `SCORE` in the HUD. The game reads a character to a glyph number with
`INSTR(ALPHA$, MID(TX$, TXI#, 1))` over a 41-character alphabet, which is why the order of
the `DATA` lines matters.
**Building the strokes and drawing them are separate jobs, on different frames.** Turning
a string into strokes costs about sixteen lines a character and happens when a number
changes; the draw routine merely replays the list at two lines a stroke, and it is the
one with the deadline:
```basic norun
LABEL DRAWHUD
GRAPHIC 1, 1
WIDTH 1
COLOR 0, 1 : COLOR 1, 4 : COLOR 2, 8 : COLOR 3, 5
COLOR 4, 11 : COLOR 5, 16 : COLOR 6, 6
BOX 5, 0, 56, 799, 57
IF HN# = 1 THEN DRAW HC#(0), HX1#(0), HY1#(0) TO HX2#(0), HY2#(0)
IF HN# < 2 THEN GOTO HUDONE
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
RETURN
```
Each stroke carries its own colour source, so one pass over the list draws in as many
colours as it likes: labels cyan, the score yellow, the lives green, the level red.
**Seven colour sources is the ceiling** — which is why the purple gem's banner is the
closest purple the palette has rather than the gem's own.
The one-stroke case is written out beside the loop. That is the second trap below, and
every loop over a list in this program has it.
## Step 7: Bounce the ball without a square root
There is no `SQR` in this dialect, so the game never computes a magnitude. Eight landing
zones across the bar, each holding **very nearly a unit vector**, and a velocity is
always one of them times the current speed:
```basic norun
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
```
```basic norun
LABEL HITBAR
IF BLY%(B#) + 21 < T2# THEN RETURN
IF BLY%(B#) > T2# + 23 THEN RETURN
IF BLX%(B#) + 21 < T1# THEN RETURN
IF BLX%(B#) > T1# + T3# THEN RETURN
T4# = (BLX%(B#) + 11 - T1#) * 8 / T3#
IF T4# < 0 THEN T4# = 0
IF T4# > 7 THEN T4# = 7
BLVX%(B#) = ZVX%(T4#) * SPD%
BLVY%(B#) = ZVY%(T4#) * SPD%
BLY%(B#) = T2# - 23
RETURN
```
A bounce off a wall or a brick only ever flips a sign, so a ball is always travelling at
exactly the `SPD%` that was in force when it last left a bar. That makes the SLOW gem a
**ratio of two speeds** rather than a change of magnitude:
```basic norun
LABEL RESCALE
IF BSPD% < 0.1 THEN BSPD% = SPD%
RAT% = SPD% / BSPD%
BSPD% = SPD%
FOR B# = 0 TO 2
IF BLON#(B#) = 1 THEN BEGIN
BLVX%(B#) = BLVX%(B#) * RAT%
BLVY%(B#) = BLVY%(B#) * RAT%
BEND
NEXT B#
RETURN
```
`RAT%` is a **float** variable on purpose: an integer one holds the 0.75 of a slowdown as
0 and stops the ball dead. That is the first trap below, and it is the expensive one.
Scaling by the vertical component instead — which is what this routine did first — is not
a slowdown at all. A shallow ball's small vertical gets stretched up to the new speed and
drags the large horizontal with it, so SLOW made the ball *faster*. Sixty degrees of the
eight zones are shallow enough to do it.
Brick collision reflects off whichever face the ball has less of itself past, which is the
standard box resolution and the reason a ball clipping the end of a row goes sideways
instead of straight back down:
```basic norun
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#)
```
`T1#` to `T4#` are the overlapping rectangle; its width against its height is the whole
test. A ball can only be over four cells at once, so the cells are worked out from its box
rather than by walking sixty bricks.
## Step 8: Gems and powerups
A gem is one sprite, one type number and two timers. The type picks the artwork, the
banner colour and what `TAKEGEM` does:
```basic norun
LABEL SPAWNGEM
RNMAX# = 5
GOSUB NEXTRAND
GMTYP# = RNVAL# + 1
GMX% = BX1# + 10
GMY% = BY1#
IF GMTYP# = 1 THEN SPRSAV "art/element_red_polygon_glossy.png", 8
IF GMTYP# = 2 THEN SPRSAV "art/element_yellow_polygon_glossy.png", 8
IF GMTYP# = 3 THEN SPRSAV "art/element_green_polygon_glossy.png", 8
IF GMTYP# = 4 THEN SPRSAV "art/element_blue_polygon_glossy.png", 8
IF GMTYP# = 5 THEN SPRSAV "art/element_purple_polygon_glossy.png", 8
GMON# = 1
SPRITE 8, 1, 2
MOVSPR 8, GMX%, GMY%
RETURN
```
**Reloading slot 8 is how one sprite becomes five gems.** `SPRSAV` over a live slot
replaces the artwork; there is no need for a slot per gem, and there was never a slot to
spare.
Every timer is a frame count decremented in one place, so an expiry is where the effect is
undone:
```basic norun
IF PDEXP# > 0 THEN BEGIN
PDEXP# = PDEXP# - 1
IF PDEXP# = 0 THEN BEGIN
PDW# = 104
SPRITE 3, 1, 2, 0, 0, 0
BEND
BEND
```
EXPAND is `SPRITE 3, 1, 2, 0, 1, 0` — the x-expand bit, which is the only scaling a sprite
has, and doubling the artwork is exactly what it wants.
The CATCH bar **mirrors** the paddle rather than following it:
```basic norun
CTX% = 0.0 - PDX% + WALLL# + WALLR# - 104
```
That is deliberate. 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` — trap one
again, and this line was wrong before it was right.
## Step 9: Three voices, and a mute that costs nothing
Voice 1 is the ball hitting things, voice 2 is the gem and the ball being lost, and voice
3 is reserved for `PLAY` so a brick going cannot cut a tune off mid-note.
**`SOUND`'s frequency argument is a SID register value, not hertz.** The pitch is
`register * 1022730 / 16777216` — see [Chapter 7](07-sound.md#sound) — so work the notes
out once and write them down: 17175 is C6, 8579 C5, 4298 C4, 3609 A3. The brick tone is
pitched by the row it came from —
```basic norun
SOUND 1, 17175 - R# * 2100, 4
```
— so the top row rings at C6, each row down drops about a third, and a wall coming apart
plays itself down a scale.
Mute with `VOL 0` rather than a flag tested at eleven call sites:
```basic norun
LABEL PRESSMUTE
SNDON# = 1 - SNDON#
IF SNDON# = 1 THEN VOL 8
IF SNDON# = 0 THEN VOL 0
RETURN
```
A silenced voice costs nothing to issue. One line beats eleven scattered through the game.
## Five things in this dialect that do not do what they look like
Each of these cost an evening. All five are filed in `TODO.md` §9 with a reduction and
the file and line of the cause.
### 1. The left operand decides integer or float arithmetic
```basic
LEVEL# = 4
SPD% = 5.6 + LEVEL# * 0.45
PRINT "INTEGER FIRST " + SPD%
SPD% = 5.6 + 0.45 * LEVEL#
PRINT "FLOAT FIRST " + SPD%
V% = 6.4
PRINT "0 - V% " + (0 - V%)
PRINT "0.0 - V% " + (0.0 - V%)
FOR I# = 0 TO 0
PRINT "THE BODY RAN"
NEXT I#
PRINT "AFTER THE LOOP"
```
```output
INTEGER FIRST 5.600000
FLOAT FIRST 7.400000
0 - V% -6
0.0 - V% -6.400000
AFTER THE LOOP
```
**This is the dangerous one, because nothing fails.** The program computes something else
and carries on. Two live bugs in this game came from it: `SPD% = 5.6 + LEVEL# * 0.45` was
a flat 5.6, so no level ever got faster than level one; and `0 - BLVX%(B#)`, the obvious
way to reverse a ball, quantised its velocity to whole pixels on every bounce and bled
speed out of it. Neither produced a diagnostic.
**Put the float on the left, and put the answer somewhere with a `%` on it.** A float
expression landing in a `#` variable truncates.
### 2. A `FOR` whose bounds are equal does not run its body
The last two lines of that output are the second trap: `FOR I# = 0 TO 0` runs zero times.
Knowing it and remembering it while writing a loop over "the bricks still standing" are
different things, and the last brick of a row is exactly that case — which is why every
loop over a list in this program has its one-item case written out beside it:
```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
```
### 3. A skipped `BEGIN` block containing a loop breaks the enclosing `RETURN`
```basic
T# = 0
GOSUB DOIT
PRINT "CAME BACK"
END
LABEL DOIT
IF 1 = 0 THEN BEGIN
FOR I# = 0 TO 2
T# = T# + 1
NEXT I#
BEND
RETURN
```
```output
? 11 : RUNTIME ERROR RETURN outside the context of GOSUB
```
The routine plainly *was* called by a `GOSUB`. `FOR` creates its environment when the
line is **parsed**, and the block skip is decided when it is **evaluated**, so a skipped
loop pushes a scope that its skipped `NEXT` never pops — and the orphan sits between the
routine and its caller. Outside a routine the same thing exhausts the pool of 32 after
thirty-two skips instead.
Guard loops with `GOTO` rather than wrapping them in a block, which is what every draw
routine in this game does.
### 4. `SSHAPE` and `GSHAPE` ignore the subscript on a string array
```basic requires=akgl
DIM SH$(4)
SSHAPE SH$(2), 0, 0, 61, 4
PRINT "[" + SH$(0) + "] [" + SH$(2) + "]"
```
```output
[SHAPE:0] []
```
The handle lands in element zero whatever subscript you write, and `GSHAPE SH$(2)` then
stamps whatever is in `SH$(0)`. Ordinary assignment and `PRINT` honour the subscript, so
this is the two verbs reading their leaf directly rather than evaluating it. The symptom
is that every brick comes out the colour of the last stamp captured.
**Keep saved shapes in separate scalars.** The six brick stamps are `S0$` to `S5$` for
this reason, and the field is drawn as six runs of one colour rather than brick by brick
— which turned out to be cheaper anyway.
### 5. `READ` walks one cursor through every `DATA` item in the file
```basic
DIM T#(3)
DIM F#(3)
I# = 0
GOSUB LOADFONT
GOSUB LOADTABLE
PRINT "TABLE " + T#(0) + " " + T#(1) + " " + T#(2)
PRINT "FONT " + F#(0) + " " + F#(1) + " " + F#(2)
END
LABEL LOADTABLE
FOR I# = 0 TO 2
READ T#(I#)
NEXT I#
RETURN
LABEL LOADFONT
FOR I# = 0 TO 2
READ F#(I#)
NEXT I#
RETURN
DATA 11, 12, 13
DATA 21, 22, 23
```
```output
TABLE 21 22 23
FONT 11 12 13
```
The `DATA` written first went to whichever routine ran first, not to the one it was
written for. Two loaders means the one whose `DATA` comes first in the file has to be
called first — obvious in hindsight, and not obvious when the symptom is a font table
full of brick colours. `RESTORE` to a label is the way out when the order cannot be
arranged; Chapter 17 uses it to pick a level layout.
## The budgets
This program sits close to four ceilings at once, and knowing where they are is what
stops 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 |
| Value-pool slots | 4096, none reclaimed | none after startup — see [Chapter 17](17-tutorial-breakout.md#step-3-create-every-name-before-the-game-starts) |
| Scopes | 32 | 6 deep at most |
| Tokens on a line | 32, and the 33rd kills the interpreter rather than raising | short lines, temporaries instead of long conditions |
That is also why several things are **not** in the game, and they are worth naming
honestly rather than leaving to be discovered: no music under the play, only event sounds
and two four-note stings; one gem at a time; sticky and multiball share one offset, so two
balls stuck to the paddle sit on top of each other; and no high score on disk, because
there is no disk.
## Where to go next
- **[Chapter 8](08-sprites.md)** is the sprite reference: every form of `SPRSAV`, the
`MOVSPR` forms, collision and what `RSPPOS` reads back.
- **[Chapter 6](06-graphics.md)** is the drawing reference, including `SSHAPE`, `GSHAPE`
and what a shape handle is.
- **[Chapter 7](07-sound.md)** is `SOUND`, `PLAY`, `ENVELOPE` and `VOL`.
- **[Chapter 14](14-architecture.md)** explains the step loop and the pools this chapter
keeps running into, from the interpreter's side.
- `TODO.md` §9 is the eight defects this game found, each with a reduction, the cause and
what a fix would touch.

View File

@@ -13,6 +13,10 @@ everything that behaves differently here and why. **[Chapter 14](14-architecture
the odd one out: it is about the interpreter rather than the language, for anyone the odd one out: it is about the interpreter rather than the language, for anyone
embedding it, debugging it or changing it. embedding it, debugging it or changing it.
**[Chapters 17](17-tutorial-breakout.md)** and **[18](18-tutorial-breakout-artwork.md)**
are tutorials rather than reference: they build one complete game twice, two different
ways, and are where the rules that a real program runs into are collected.
## Chapters ## Chapters
| | | | | |
@@ -33,6 +37,8 @@ embedding it, debugging it or changing it.
| **[14. Architecture](14-architecture.md)** | How the interpreter is put together, how to debug it, how to change it | | **[14. Architecture](14-architecture.md)** | How the interpreter is put together, how to debug it, how to change it |
| **[15. Error codes](15-error-codes.md)** | Appendix: every value `ER#` can hold and every error line the interpreter prints | | **[15. Error codes](15-error-codes.md)** | Appendix: every value `ER#` can hold and every error line the interpreter prints |
| **[16. Structures](16-structures.md)** | `TYPE`, records, strict pointers, and sharing a C struct with an embedding host | | **[16. Structures](16-structures.md)** | `TYPE`, records, strict pointers, and sharing a C struct with an embedding host |
| **[17. Tutorial: Breakout](17-tutorial-breakout.md)** | Build a whole game out of the text grid and two `DATA` sprites, a step at a time |
| **[18. Tutorial: Breakout with artwork](18-tutorial-breakout-artwork.md)** | Build it again out of loaded artwork, powerups and a drawn HUD |
## The shortest possible start ## The shortest possible start

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 B

View File

@@ -0,0 +1,74 @@
# Breakout
Breakout, written entirely in BASIC for the AKGL BASIC interpreter. The ball and the
paddle are sprites defined from `DATA` in the listing; the bricks, the HUD and the
messages are characters in the text grid.
```sh
../../../build-akgl/basic breakout.bas
```
It needs the SDL build. The stdio build has no graphics device, so `RGR` refuses on the
fourth line and the game stops there — which is exactly what it should do.
| Key | Does |
|---|---|
| left / right | move the paddle |
| space | start a game from the title screen, then launch the ball |
| P | pause |
| Q or escape | quit |
The title screen plays itself. Attract mode is a real feature and it is also how the game
was tested without a hand on the keyboard.
Three lives, six rows of bricks worth 60 down to 10 points a row, three level layouts that
repeat with a faster ball each time, and a high score that lasts as long as the process
does.
## How it works, and how to write your own
**[Chapter 17 of the guide](../../../docs/17-tutorial-breakout.md)** builds this program a
step at a time: what to make a sprite and what to make a character, why every name is
declared before the game starts, how a text row has to be written whole, and the rest of
the rules this listing never breaks. It is the tutorial; this is the finished thing.
[Chapter 18](../../../docs/18-tutorial-breakout-artwork.md) does the same for
[`../sprites`](../sprites), which builds the same game out of loaded artwork and comes out
a completely different program.
## Known warts, this game's own
- **The cell size is a constant.** `CW#` and `CH#` are 16, measured with the bundled
C64_Pro_Mono at 16 points in the 800×600 window; the grid is 50×37. BASIC cannot ask —
`WINDOW` knows, but the standalone frontend never offers it. Change the font or the
window size and those two numbers have to change with them.
- **Every character the game draws also goes to stdout**, because the frontend tees the
text sink to the terminal. It made the whole game auditable from a log file during
development and it is pure noise the rest of the time. Redirect it.
- **Collision is against the cell the ball's leading edge is in**, so a brick clipped at
the very corner can be missed by up to seven pixels' worth of ball. Testing the centre
instead was worse: the ball sank four pixels into a brick before it turned.
- **There is a stall watchdog.** Ten seconds without touching a brick and the ball gets a
new angle — at the *next paddle bounce*, never in mid-air.
- **The demo cannot lose**, it re-serves. It also cannot be paused; `P` is ignored until a
real game starts.
- **Sound is asked for once and then believed.** On a machine with no audio device the
game is silent rather than dead.
- **No high score on disk.** There is no disk in this interpreter.
The interpreter defects this game turned up are filed in `TODO.md` §6 item 30 and §9, each
with a reduction that fits on a screen.
## How this was checked
- Parses clean under the stdio `basic` (it then stops at the first graphics verb, as it
should).
- Attract mode run for 145 seconds and again for 110: no runtime error, score climbing
throughout, 39 bricks in the second run.
- Level clear verified against a copy whose first layout is a single row: `LEVEL CLEARED`,
layout 2 loaded, ball speed up, play continues.
- Input driven with `xdotool` against the real window: paddle moves and clamps at both
edges, ball launches, `P` pauses and unpauses, three lives drain to `GAME OVER`, space
starts a new game, `Q` exits printing the final and high scores.
- Every direction change logged for a whole run and read back: each one is a wall, a brick
or the paddle. Nothing turns without a reason.

View File

@@ -0,0 +1,843 @@
REM ####################################################################
REM BREAKOUT
REM
REM One hundred percent BASIC, for the AKGL BASIC interpreter.
REM ../akbasic/build-akgl/basic breakout.bas
REM
REM The ball and the paddle are sprites, defined from DATA in this listing.
REM The bricks, the HUD and the messages are characters in the text grid.
REM Nothing here loads an asset and nothing here uses DRAW, BOX or CIRCLE:
REM the drawing verbs do not survive a frame in this interpreter, while the
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 * Mixed + and - are parenthesised. This parser reads a - b + c as
REM a - (b + c), and matches only ONE unparenthesised AND or OR.
REM
REM Written by Claude Opus (1M)
REM ####################################################################
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
SCW# = RGR(1)
SCH# = RGR(2)
COLS# = SCW# / CW#
ROWS# = SCH# / CH#
REM --- the brick wall, in cells ---------------------------------------
BRW# = 4
BCOLS# = 10
BROWS# = 6
BTOP# = 4
BLEFT# = (COLS# - (BRW# * BCOLS#)) / 2
REM --- the play area, in pixels ---------------------------------------
TOPY# = 2 * CH#
MAXX# = SCW# - 8
PW# = 144
PY# = 528
LOSEY# = PY# + 32
MSGROW# = 35
TITROW# = 20
PSPD# = 10
REM --- pools ----------------------------------------------------------
REM Every name in the program is created HERE, and this is load-bearing
REM twice over.
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
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.
DIM BR#(60)
DIM SB#(63)
DIM SP#(63)
DIM LAY$(6)
DIM BSG$(6)
SCORE# = 0
HIGH# = 0
LIVES# = 3
LEVEL# = 1
LEFTN# = 0
BSPD# = 4
STATE# = 0
DEMO# = 0
PAUSED# = 0
HELD# = 0
PDIR# = 0
PDEC# = 0
PX# = 0
BX# = 0
BY# = 0
BVX# = 0
BVY# = 0
OX# = 0
OY# = 0
K# = 0
HIT# = 0
PVX# = 0
DOFF# = 0
STALL# = 0
NUDGE# = 0
BI# = 0
BR2# = 0
RB# = 0
CB# = 0
RC# = 0
CC2# = 0
TX# = 0
TY# = 0
PTS# = 0
V# = 0
L# = 0
C2# = 0
D# = 0
M# = 0
X2# = 0
X3# = 0
Z# = 0
BB# = 0
RX# = 0
N# = 0
MROW# = 0
RMAX# = 2
RND# = 0
SND# = 0
P$ = ""
H$ = ""
S$ = ""
T$ = ""
MSG$ = ""
BSEG$ = ""
FRAME$ = ""
I# = 0
R# = 0
R2# = 0
C# = 0
CC# = 0
KI# = 0
T# = 0
REM Brick faces, one per wall row. CHAR ignores its colour argument --
REM the sink draws in one colour -- so the rows are told apart by shape.
BSG$(0) = "[##]"
BSG$(1) = "[##]"
BSG$(2) = "[==]"
BSG$(3) = "[==]"
BSG$(4) = "[--]"
BSG$(5) = "[--]"
REM --- the seed -------------------------------------------------------
REM There is no RND in this dialect. TI# is jiffies off the host's clock
REM and is host uptime rather than zero-based, which makes it a fine seed.
SEED# = TI#
REM The ceiling, drawn once. TOPY# is the bottom edge of this row, so the
REM ball turns against something the player can see rather than against an
REM invisible line two rows down from the top of the window.
FRAME$ = "=" * COLS#
CHAR 1, 0, 1, FRAME$
GOSUB MKSPR
GOSUB SNDPROBE
GOTO TITLE
REM ####################################################################
REM SPRITES
REM ####################################################################
LABEL MKSPR
FOR I# = 0 TO 62
READ SB#(I#)
NEXT I#
FOR I# = 0 TO 62
READ SP#(I#)
NEXT I#
SPRSAV SB#, 1
SPRITE 1, 1, 2
SPRSAV SP#, 2
SPRSAV SP#, 3
SPRSAV SP#, 4
SPRITE 2, 1, 15, 0, 1, 0
SPRITE 3, 1, 15, 0, 1, 0
SPRITE 4, 1, 15, 0, 1, 0
RETURN
REM ####################################################################
REM SOUND
REM
REM SOUND refuses on a machine with no audio device, and an untrapped
REM refusal would end the game. Ask once, remember the answer, and never
REM ask again -- the game is silent where there is nothing to play on.
REM ####################################################################
LABEL SNDPROBE
SND# = 1
TRAP NOAUDIO
SOUND 1, 2000, 1
TRAP
RETURN
LABEL NOAUDIO
SND# = 0
RESUME NEXT
LABEL BEEPB
IF SND# = 1 THEN SOUND 1, 12000, 2
RETURN
LABEL BEEPP
IF SND# = 1 THEN SOUND 2, 6000, 3
RETURN
LABEL BEEPW
IF SND# = 1 THEN SOUND 3, 9000, 1
RETURN
LABEL BEEPL
IF SND# = 1 THEN SOUND 1, 1500, 20
RETURN
REM ####################################################################
REM TITLE AND ATTRACT MODE
REM
REM The title screen plays itself. It is also how this game gets tested
REM without a human at the keyboard.
REM ####################################################################
LABEL TITLE
DEMO# = 1
SCORE# = 0
LIVES# = 3
LEVEL# = 1
BSPD# = 4
PAUSED# = 0
GOSUB LOADLAY
GOSUB BUILDW
GOSUB DRAWW
GOSUB DRAWHUD
GOSUB TITTEXT
GOSUB SERVE
STATE# = 0
GOTO TICK
LABEL TITTEXT
MROW# = TITROW#
MSG$ = "B R E A K O U T"
GOSUB SHOWAT
MROW# = TITROW# + 2
MSG$ = "PRESS SPACE TO PLAY"
GOSUB SHOWAT
MROW# = TITROW# + 4
MSG$ = "LEFT AND RIGHT MOVE P PAUSES Q QUITS"
GOSUB SHOWAT
MROW# = TITROW# + 6
MSG$ = "ATTRACT MODE"
GOSUB SHOWAT
RETURN
LABEL CLRTIT
MROW# = TITROW#
GOSUB CLRAT
MROW# = TITROW# + 2
GOSUB CLRAT
MROW# = TITROW# + 4
GOSUB CLRAT
MROW# = TITROW# + 6
GOSUB CLRAT
RETURN
REM ####################################################################
REM GAME AND LEVEL SETUP
REM ####################################################################
LABEL NEWGAME
DEMO# = 0
SCORE# = 0
LIVES# = 3
LEVEL# = 1
BSPD# = 4
PAUSED# = 0
GOSUB CLRTIT
GOSUB NEWLEV
STATE# = 0
GOTO TICK
LABEL NEWLEV
GOSUB LOADLAY
GOSUB BUILDW
GOSUB DRAWW
GOSUB DRAWHUD
GOSUB SERVE
MSG$ = "PRESS SPACE TO LAUNCH"
GOSUB SHOWMSG
RETURN
REM Three layouts, then round again with a faster ball. RESTORE takes a
REM label -- it wants a line and a label evaluates to one -- so a layout is
REM simply a named place in the DATA stream.
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
LABEL BUILDW
LEFTN# = 0
FOR R# = 0 TO 5
S$ = LAY$(R#)
GOSUB BUILDR
NEXT R#
RETURN
LABEL BUILDR
FOR C# = 0 TO 9
T$ = MID(S$, C#, 1)
I# = (R# * BCOLS#) + C#
BR#(I#) = 0
IF T$ = "1" THEN BR#(I#) = 1
IF T$ = "1" THEN LEFTN# = LEFTN# + 1
NEXT C#
RETURN
LABEL DRAWW
FOR R# = 0 TO 5
BR2# = R#
GOSUB DRAWBR
NEXT R#
RETURN
REM One brick row, rebuilt whole and written in a single CHAR. Anything
REM else would truncate the row at the first character it wrote.
LABEL DRAWBR
S$ = ""
IF BLEFT# > 0 THEN S$ = " " * BLEFT#
BSEG$ = BSG$(BR2#)
FOR CC# = 0 TO 9
I# = (BR2# * BCOLS#) + CC#
IF BR#(I#) = 1 THEN S$ = S$ + BSEG$
IF BR#(I#) = 0 THEN S$ = S$ + " "
NEXT CC#
R2# = BTOP# + BR2#
CHAR 1, 0, R2#, S$
RETURN
REM Put the ball back on the paddle, pointing up, left or right at random.
LABEL SERVE
PX# = (SCW# - PW#) / 2
HELD# = 1
IF DEMO# = 1 THEN HELD# = 0
BX# = PX# + ((PW# / 2) - 4)
BY# = PY# - 10
RMAX# = 2
GOSUB RANDOM
BVX# = BSPD#
IF RND# = 0 THEN BVX# = 0 - BSPD#
BVY# = 0 - BSPD#
PDEC# = 0
GOSUB SHOWSPR
RETURN
REM A linear congruential generator, because this dialect has no RND.
REM The multiply stays inside int64 for any seed under 2^31.
LABEL RANDOM
SEED# = MOD(((SEED# * 1103515245) + 12345), 2147483648)
RND# = MOD((SEED# / 65536), RMAX#)
RETURN
REM ####################################################################
REM THE MAIN LOOP
REM
REM A GOTO loop rather than DO/LOOP on purpose: every loop and every
REM GOSUB pushes one of 32 scopes, and a state change that jumped out of
REM a DO would leak one every time. This loop pushes nothing.
REM ####################################################################
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
REM ####################################################################
REM INPUT
REM
REM GET is the only non-blocking read there is, and there is no key-up
REM event and no held-key state. What there IS is the host's own key
REM repeat, which arrives as ordinary key-down events about once per tick.
REM So a keystroke refills a countdown and the paddle coasts to a stop
REM when the repeats stop: held reads as held, tapped reads as a nudge.
REM ####################################################################
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
LABEL KEYSPC
IF DEMO# = 1 THEN STATE# = 5
IF DEMO# = 1 THEN RETURN
IF HELD# = 0 THEN RETURN
HELD# = 0
GOSUB CLRMSG
RETURN
LABEL KEYPAU
IF DEMO# = 1 THEN RETURN
PAUSED# = 1 - PAUSED#
IF PAUSED# = 1 THEN MSG$ = "PAUSED"
IF PAUSED# = 1 THEN GOSUB SHOWMSG
IF PAUSED# = 0 THEN GOSUB CLRMSG
RETURN
REM ####################################################################
REM THE PADDLE
REM ####################################################################
LABEL MOVEPAD
IF DEMO# = 1 THEN GOSUB DEMOPAD
IF DEMO# = 1 THEN GOTO PADCLMP
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
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 HOLDBAL
BX# = PX# + ((PW# / 2) - 4)
BY# = PY# - 10
RETURN
REM ####################################################################
REM THE BALL
REM
REM X first and then Y, each move tested on its own, so a ball arriving
REM at the corner of a brick reverses one axis rather than both.
REM ####################################################################
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
STALL# = STALL# + 1
IF STALL# > 500 THEN NUDGE# = 1
RETURN
LABEL WALLL
BX# = 0
BVX# = 0 - BVX#
GOSUB BEEPW
RETURN
LABEL WALLR
BX# = MAXX#
BVX# = 0 - BVX#
GOSUB BEEPW
RETURN
LABEL WALLT
BY# = TOPY#
BVY# = 0 - BVY#
GOSUB BEEPW
RETURN
REM The leading edge, not the centre: a ball tested by its centre sinks
REM four pixels into a brick before it turns, which looks like the brick
REM was hit late and the one beside it was not hit at all.
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
LABEL YBRICK
TX# = BX# + 4
TY# = BY#
IF BVY# > 0 THEN TY# = BY# + 7
GOSUB HITTEST
IF HIT# = 0 THEN RETURN
BY# = OY#
BVY# = 0 - BVY#
GOSUB KILLBR
RETURN
REM Which brick, if any, holds the pixel (TX#, TY#). This is the only
REM place in the game that converts pixels into cells.
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
LABEL KILLBR
BR#(BI#) = 0
LEFTN# = LEFTN# - 1
STALL# = 0
PTS# = (BROWS# - BR2#) * 10
SCORE# = SCORE# + PTS#
IF DEMO# = 0 THEN GOSUB HISCORE
GOSUB DRAWBR
GOSUB DRAWHUD
GOSUB BEEPB
IF LEFTN# < 1 THEN STATE# = 2
RETURN
REM Paddle bounce. Five zones across the face, so where you catch it
REM decides where it goes -- the whole game, really.
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
REM Dead centre. Keep the direction it came in on and give it a shallow
REM angle: a ball with no sideways speed at all rises and falls down one
REM column for ever, which is how attract mode used to hang itself.
BVX# = 2
IF PVX# < 0 THEN BVX# = 0 - 2
LABEL PADAIM
IF NUDGE# = 1 THEN GOSUB UNSTICK
IF DEMO# = 1 THEN GOSUB DEMOAIM
GOSUB BEEPP
RETURN
REM Ten seconds without touching a brick means the ball has found an orbit
REM that misses the wall -- straight up and straight back down the same
REM empty column, usually. Give it a different angle, but do it HERE, on a
REM bounce the player is already watching, rather than in mid-flight where
REM it would look like the ball hit something that was not there.
LABEL UNSTICK
NUDGE# = 0
STALL# = 0
RMAX# = 4
GOSUB RANDOM
BVX# = (RND# * 3) - 6
IF BVX# = 0 THEN BVX# = 3
RETURN
REM Attract mode aims off-centre by a random amount, so the machine plays
REM like something with a hand on the paddle rather than a mirror.
LABEL DEMOAIM
RMAX# = 81
GOSUB RANDOM
DOFF# = RND# - 40
RETURN
REM ####################################################################
REM DRAWING
REM ####################################################################
REM The sprite position is its top-left corner in device pixels, and the
REM paddle is three 48-wide segments laid end to end. Each coordinate is
REM worked out into a variable first: MOVSPR reads a leading sign as
REM "move by this much" rather than "move to here".
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
LABEL DRAWHUD
V# = SCORE#
GOSUB PAD6
H$ = " SCORE " + P$
H$ = (H$ + " LIVES ") + LIVES#
H$ = (H$ + " LEVEL ") + LEVEL#
V# = HIGH#
GOSUB PAD6
H$ = (H$ + " HIGH ") + P$
CHAR 1, 0, 0, H$
RETURN
LABEL PAD6
P$ = "" + V#
DO WHILE LEN(P$) < 6
P$ = "0" + P$
LOOP
RETURN
LABEL SHOWMSG
MROW# = MSGROW#
GOSUB SHOWAT
RETURN
LABEL CLRMSG
MROW# = MSGROW#
GOSUB CLRAT
RETURN
REM Centre MSG$ on row MROW#. No padding on the right is needed: CHAR
REM terminates the row where the message ends, which erases the rest.
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
REM ####################################################################
REM WHAT HAPPENS WHEN THE BALL GETS PAST YOU
REM ####################################################################
LABEL LOSTLIF
GOSUB BEEPL
IF DEMO# = 1 THEN GOTO DEMOSRV
LIVES# = LIVES# - 1
GOSUB DRAWHUD
IF LIVES# < 1 THEN GOTO GAMEOVR
GOSUB SERVE
MSG$ = "BALL LOST -- PRESS SPACE"
GOSUB SHOWMSG
STATE# = 0
GOTO TICK
LABEL DEMOSRV
GOSUB SERVE
HELD# = 0
STATE# = 0
GOTO TICK
LABEL LEVELUP
MSG$ = "LEVEL CLEARED"
GOSUB SHOWMSG
SLEEP 1.5
LEVEL# = LEVEL# + 1
IF BSPD# < 7 THEN BSPD# = BSPD# + 1
GOSUB NEWLEV
IF DEMO# = 1 THEN GOSUB CLRMSG
STATE# = 0
GOTO TICK
LABEL GAMEOVR
MSG$ = "GAME OVER -- SPACE PLAYS AGAIN, Q QUITS"
GOSUB SHOWMSG
LABEL GOWAIT
GET K#
IF K# = 32 THEN GOTO NEWGAME
IF K# = 113 THEN GOTO BYE
IF K# = 27 THEN GOTO BYE
SLEEP 0.05
GOTO GOWAIT
LABEL BYE
SPRITE 1, 0
SPRITE 2, 0
SPRITE 3, 0
SPRITE 4, 0
SCNCLR
PRINT "BREAKOUT"
PRINT "FINAL SCORE " + SCORE#
PRINT "HIGH SCORE " + HIGH#
SLEEP 2
QUIT
REM The machine does not get on the scoreboard: attract mode keeps score
REM so the wall behaves, but only a player's score is ever the high one.
LABEL HISCORE
IF SCORE# > HIGH# THEN HIGH# = SCORE#
RETURN
REM ####################################################################
REM SPRITE PATTERNS
REM
REM 63 bytes each: three per row, twenty-one rows, most significant bit
REM leftmost. A clear bit is transparent, which is what lets an 8x8 ball
REM live in the corner of a 24x21 pattern with nothing drawn around it.
REM
REM byte 0 byte 1 byte 2
REM 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
REM ^ column 0 ^ column 23
REM ####################################################################
REM Ball: an 8x8 disc in the top-left corner, so its pixel rectangle is
REM the sprite position and eight pixels each way. No offset arithmetic.
REM . . # # # # . . 60
REM . # # # # # # . 126
REM # # # # # # # # 255
DATA 60, 0, 0
DATA 126, 0, 0
DATA 255, 0, 0
DATA 255, 0, 0
DATA 255, 0, 0
DATA 255, 0, 0
DATA 126, 0, 0
DATA 60, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
REM Paddle segment: the full 24 wide and 6 deep, expanded to 48 wide by
REM SPRITE's xexpand bit. Three of them make one 144 pixel paddle.
DATA 255, 255, 255
DATA 255, 255, 255
DATA 255, 255, 255
DATA 255, 255, 255
DATA 255, 255, 255
DATA 255, 255, 255
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
DATA 0, 0, 0
REM ####################################################################
REM LEVEL LAYOUTS
REM
REM Six rows of ten bricks. 1 is a brick, 0 is a hole. RESTORE picks the
REM layout by line number, so adding a fourth is a block and one IF.
REM ####################################################################
LABEL LAY1
REM Level 1: the whole wall.
DATA "1111111111"
DATA "1111111111"
DATA "1111111111"
DATA "1111111111"
DATA "1111111111"
DATA "1111111111"
LABEL LAY2
REM Level 2: a barrel, open at the corners.
DATA "0011111100"
DATA "0111111110"
DATA "1111111111"
DATA "1111111111"
DATA "0111111110"
DATA "0011111100"
LABEL LAY3
REM Level 3: gaps to shoot through.
DATA "1010101010"
DATA "0101010101"
DATA "1111001111"
DATA "1100110011"
DATA "1011111101"
DATA "0110110110"

View File

@@ -0,0 +1,86 @@
# Breakout, with artwork
Breakout, written entirely in BASIC for the AKGL BASIC interpreter, using downloaded CC0
sprite artwork for the paddles, the balls and the powerup gems. The bricks, the HUD and
the lettering are drawn, captured with `SSHAPE` and installed as sprites — which is the
only way a program in this interpreter can put a picture on the screen.
```sh
../../../build-akgl/basic breakout.bas
```
It needs the SDL build. Without a graphics device the first `SPRSAV` refuses by name and
the program stops there, which is the right thing for it to do.
| Key | Does |
|---|---|
| left / right | move the paddle |
| space | start a game, launch the ball, release a stuck ball |
| P | pause |
| S | sound on and off |
| Q or escape | quit |
Three lives, six rows of bricks worth 60 down to 10 points a row, and a field that cycles
six rows, then five, then four while the ball gets faster every level up to a ceiling. A
gem is worth 50 on top of whatever it does.
## The powerups
A broken brick drops a gem about one time in seven, and only ever one at a time. Catch it
with the paddle. The colour of the gem is what tells you which it is; the banner that
flashes over the field names it as well.
| Gem | Name | Does | For |
|---|---|---|---|
| red | EXPAND | doubles the paddle's width | 20 seconds |
| yellow | MULTI | throws two more balls off the one in play | until they are lost |
| green | SLOW | drops the ball's speed to about two thirds | 16 seconds |
| blue | STICKY | the ball sticks where it lands; space fires it | 18 seconds |
| purple | CATCH | a second bar appears higher up the field | 24 seconds |
The second bar **mirrors** the paddle rather than following it, so it covers the side you
just left. 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.
## How it works, and how to write your own
**[Chapter 18 of the guide](../../../docs/18-tutorial-breakout-artwork.md)** builds this
program a step at a time: budgeting the eight sprite slots, capturing a drawing into one,
finding the host's frame boundary with `TI#`, moving everything that is not drawing out of
the deadline, the stroke font, and the five things in this dialect that do not do what
they look like.
[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
`DATA` sprites — a completely different program, and the shorter one to read first.
## Where the artwork came from
Kenney's [Puzzle Pack 1](https://kenney.nl/assets/puzzle-pack-1), released under
[CC0](http://creativecommons.org/publicdomain/zero/1.0/). The files in `art/` are the
pack's own `PNG/Default/` versions, unmodified, with the pack's licence file beside them.
`art/PROVENANCE.md` says which file is used for what and why the double-size set is not.
Crediting Kenney is not required by CC0. It is here because it should be.
## What is not in it
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 is not much room anywhere else. 121 of the interpreter's
128 variables are spoken for, four more are the ones it makes itself, and the label table
is at 61 of 64.
- **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.
The eight interpreter defects this game turned up are filed in `TODO.md` §9, each with a
reduction that fits on a screen, the file and line of the cause, and what a fix would
touch.

View File

@@ -0,0 +1,21 @@
Puzzle Pack (1.1)
Created/distributed by Kenney (www.kenney.nl)
------------------------------
License: (Creative Commons Zero, CC0)
http://creativecommons.org/publicdomain/zero/1.0/
This content is free to use in personal, educational and commercial projects.
Support us by crediting Kenney or www.kenney.nl (this is not mandatory)
------------------------------
Donate: http://support.kenney.nl
Patreon: http://patreon.com/kenney/
Follow on Twitter for updates:
http://twitter.com/KenneyNL

View File

@@ -0,0 +1,35 @@
# Where this art came from
Every PNG in this directory is from **Kenney's Puzzle Pack 1**, released into the public
domain under [Creative Commons Zero](http://creativecommons.org/publicdomain/zero/1.0/).
`License.txt` is the pack's own licence file, copied here unedited.
* Source: <https://kenney.nl/assets/puzzle-pack-1>
* Downloaded: 2026-08-01, `kenney_puzzle-pack-1.zip`
* Author: Kenney (<https://www.kenney.nl>)
* Licence: CC0 1.0. Crediting is not required; it is here because it should be.
The files are the pack's `PNG/Default/` versions, byte for byte — nothing is resized,
recoloured or re-encoded, so the checksum of any of them still matches the distributed
archive.
| File | Size | Used for |
|---|---|---|
| `paddleBlu.png` | 104x24 | the player's paddle |
| `paddleRed.png` | 104x24 | the second paddle, from the CATCHER powerup |
| `ballBlue.png` | 22x22 | the ball |
| `ballGrey.png` | 22x22 | the extra balls from MULTIBALL |
| `element_red_polygon_glossy.png` | 48x46 | the EXPAND powerup |
| `element_yellow_polygon_glossy.png` | 48x46 | the MULTIBALL powerup |
| `element_green_polygon_glossy.png` | 48x46 | the SLOW powerup |
| `element_blue_polygon_glossy.png` | 48x46 | the STICKY powerup |
| `element_purple_polygon_glossy.png` | 48x46 | the CATCHER powerup |
The pack's `PNG/Double/` versions are twice these sizes and are the wrong scale for an
800x600 field. `SPRITE`'s x- and y-expand bits would double them again, which is the only
scaling this interpreter has — there is no way to make a sprite *smaller* — so the
`Default` set is the one that fits.
Everything else on screen — the bricks, the walls, the HUD lettering, the title — is drawn
by the program with `DRAW` and `BOX`. See `README-sprites.md` for why only eight things in
this game can be made of artwork.

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 495 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 B

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
#!/bin/bash
#
# Chapter 18's figures load the Breakout artwork by the same relative path the
# game does, so they need an `art/` beside the program. Copy the example's own
# directory rather than a private duplicate: a figure drawn from a second copy
# of the artwork stops being a figure of the program it documents the moment
# either copy moves, which is the drift this whole arrangement is against.
cp -R "$(dirname "${BASH_SOURCE[0]}")/../../examples/breakout/sprites/art" art