From cb0e2d0800312103fec612a683ceede4cf6d6641 Mon Sep 17 00:00:00 2001 From: Andrew Kesterson Date: Sat, 1 Aug 2026 22:54:42 -0400 Subject: [PATCH] Add two Breakout examples and the tutorials that build them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- CLAUDE.md | 2 +- README.md | 2 +- TODO.md | 424 +++++ docs/07-sound.md | 6 + docs/17-tutorial-breakout.md | 763 +++++++++ docs/18-tutorial-breakout-artwork.md | 796 ++++++++++ docs/README.md | 6 + docs/images/breakout-artwork.png | Bin 0 -> 8339 bytes docs/images/breakout-lettering.png | Bin 0 -> 451 bytes docs/images/breakout-paddle.png | Bin 0 -> 262 bytes docs/images/breakout-screen.png | Bin 0 -> 5245 bytes docs/images/breakout-stamps.png | Bin 0 -> 388 bytes examples/breakout/characters/README.md | 74 + examples/breakout/characters/breakout.bas | 843 ++++++++++ examples/breakout/sprites/README.md | 86 ++ examples/breakout/sprites/art/License.txt | 21 + examples/breakout/sprites/art/PROVENANCE.md | 35 + examples/breakout/sprites/art/ballBlue.png | Bin 0 -> 513 bytes examples/breakout/sprites/art/ballGrey.png | Bin 0 -> 495 bytes .../art/element_blue_polygon_glossy.png | Bin 0 -> 1514 bytes .../art/element_green_polygon_glossy.png | Bin 0 -> 1503 bytes .../art/element_purple_polygon_glossy.png | Bin 0 -> 1466 bytes .../art/element_red_polygon_glossy.png | Bin 0 -> 1474 bytes .../art/element_yellow_polygon_glossy.png | Bin 0 -> 1522 bytes examples/breakout/sprites/art/paddleBlu.png | Bin 0 -> 762 bytes examples/breakout/sprites/art/paddleRed.png | Bin 0 -> 578 bytes examples/breakout/sprites/breakout.bas | 1360 +++++++++++++++++ tests/docs_setups/breakout_art.sh | 8 + 28 files changed, 4424 insertions(+), 2 deletions(-) create mode 100644 docs/17-tutorial-breakout.md create mode 100644 docs/18-tutorial-breakout-artwork.md create mode 100644 docs/images/breakout-artwork.png create mode 100644 docs/images/breakout-lettering.png create mode 100644 docs/images/breakout-paddle.png create mode 100644 docs/images/breakout-screen.png create mode 100644 docs/images/breakout-stamps.png create mode 100644 examples/breakout/characters/README.md create mode 100644 examples/breakout/characters/breakout.bas create mode 100644 examples/breakout/sprites/README.md create mode 100644 examples/breakout/sprites/art/License.txt create mode 100644 examples/breakout/sprites/art/PROVENANCE.md create mode 100644 examples/breakout/sprites/art/ballBlue.png create mode 100644 examples/breakout/sprites/art/ballGrey.png create mode 100644 examples/breakout/sprites/art/element_blue_polygon_glossy.png create mode 100644 examples/breakout/sprites/art/element_green_polygon_glossy.png create mode 100644 examples/breakout/sprites/art/element_purple_polygon_glossy.png create mode 100644 examples/breakout/sprites/art/element_red_polygon_glossy.png create mode 100644 examples/breakout/sprites/art/element_yellow_polygon_glossy.png create mode 100644 examples/breakout/sprites/art/paddleBlu.png create mode 100644 examples/breakout/sprites/art/paddleRed.png create mode 100644 examples/breakout/sprites/breakout.bas create mode 100755 tests/docs_setups/breakout_art.sh diff --git a/CLAUDE.md b/CLAUDE.md index 61846ce..dd6b763 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,7 +25,7 @@ scripting engine for game authors. | [`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 | | [`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/UPGRADING.md` | 1.0.0's status registry. Required before writing an error code | | `deps//AGENTS.md` | Per-repo rules. Read the relevant one **before editing a submodule** | diff --git a/README.md b/README.md index 793d636..68fd897 100644 --- a/README.md +++ b/README.md @@ -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 | | [`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 | diff --git a/TODO.md b/TODO.md index 5df2ed6..bd9cc6b 100644 --- a/TODO.md +++ b/TODO.md @@ -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 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` 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 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. + +--- + +## 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. diff --git a/docs/07-sound.md b/docs/07-sound.md index 6bd944c..aec7ec2 100644 --- a/docs/07-sound.md +++ b/docs/07-sound.md @@ -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* — 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 what makes a siren or a laser. diff --git a/docs/17-tutorial-breakout.md b/docs/17-tutorial-breakout.md new file mode 100644 index 0000000..63731d3 --- /dev/null +++ b/docs/17-tutorial-breakout.md @@ -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. diff --git a/docs/18-tutorial-breakout-artwork.md b/docs/18-tutorial-breakout-artwork.md new file mode 100644 index 0000000..3184176 --- /dev/null +++ b/docs/18-tutorial-breakout-artwork.md @@ -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. diff --git a/docs/README.md b/docs/README.md index daec805..bb30130 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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 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 | | | @@ -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 | | **[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 | +| **[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 diff --git a/docs/images/breakout-artwork.png b/docs/images/breakout-artwork.png new file mode 100644 index 0000000000000000000000000000000000000000..fa4d45bdcb869806f8b1c5d5c6959544b906f443 GIT binary patch literal 8339 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 literal 0 HcmV?d00001 diff --git a/docs/images/breakout-paddle.png b/docs/images/breakout-paddle.png new file mode 100644 index 0000000000000000000000000000000000000000..06e43b40b867223d0eafef16a3b1fc3affce86ae GIT binary patch literal 262 zcmeAS@N?(olHy`uVBq!ia0vp^AAq=mg9%7FUwKggq|SP}IEGZrd3$#w?;!^PS4a8M zivqF%OtKdy{I+89=6J%9`E;wD@x|8FSGjZ8_oXrdwcr9lJ?|Kc&RE`mZm6@Zeed-j zYbqDN6q^>85dYcj^6}jz{0viBd_^3Ek2=Tk>bI&VzD%vz#*Edh2mj~1+kfZ#?OV1? w_7yhk?<&_69_I=KYj(ZhB4~nD69a?89C7Y+A@NYD>^mS8p00i_>zopr0DUG%4FCWD literal 0 HcmV?d00001 diff --git a/docs/images/breakout-screen.png b/docs/images/breakout-screen.png new file mode 100644 index 0000000000000000000000000000000000000000..47828fad25045c8a0d9818d00f3ba5ce060e2a10 GIT binary patch literal 5245 zcmeHLYgAI{8s2J&W!Y8pWMwnW(Y%zFl{2BxRMSOM6I1bmIyGt~UeXY6pp9LQ$J;6i zvrsE%Yf0iQB}Ig+q%@&IMMLm5sEA;ssCeUS=dAO4{5&(eJCr=zj0f5eS0MJJ3X+tN58x73?VB^t~M}IsMN0E#CGAN-0jxy9h zEb6Xd##Y3n>_qJR>FW2VACMm%)`Oi{aoEz`aJ?$(=7~qNKH9#A_U_jbJg&A=jc!v< zg8#I9M=o7{kcPm-n8b}eG4R`T`eGdcJvRiCz zK#v-ze)r(T@dL>QK#Ke1H3l+Dk}6x)*-@|(VEcLt$m6Tdp>vK{9iEI}ukV~M{y=vW2B7@0mk`92$dNdNr1jh{8H;&^;$L~K7J@ZJT)chxE1d{>e3z@s-YXu8Cd7RV|gMGG&>i;sm5 zqk}o<8d6o4qQHNI;(s3LQCuR{zy@LBn4#)vdO_mFcRO_a4+m+z0Qc4rYG@JIGn(_g zFS(Vik%ol1%NF2UYy4vW%)n#W^w{F$ixPy2IEQARzLW#E^^Z*+vZCTtN)n#1hD`epo>McR6@H5q|>>{|= z%gTQLMY@Zi)PoP%2uXXiXf9CMX9q;4yTJxoz4z>Aml7dnj^|ZELWvQW@q7c@RS@*n z&C{kkL4SEC)zi*d^eUE4`XG(fL{{<@WU&RcL}!%_k`Js(@95oF=Gl@&h5G~|tLs5Z zz{nHAFO?#UOBF8p*2bXiV@nWww4|x0x`qEY{t6FwFsv~6(P73<~J+oc9(ht_{%Yi(pY=W8KR*Lf3Ojpyp;i>1JeLrcZ? zC-&|Hw=mu{Rz%AgjX&(m~QV#Yd`n!U^@Y%HKO(W>d>^ zVEtUA)o>6p`qr-PhHpvoRO>5la-o7+Nla*57^{nwP4YtSt_h#S0t*|^?6p#EA$+2H z4jxm@+5MC?I{|i-!)Rf3qJ<$FGAkwow6jly+7Um@3m>8Y<k5Fv%EyrSTFWsXb}F7+(S;V{xCK+=?S(27SaCk#m2?&=$RYU@9{uAC-5x%5f= zquZVn%=!VPWTE;EYq>@b;fUF4dN+RLpve1jA4!O_C@ znXcOA863RW*+R$>r~!3ba}yk&CB_V!HeM2(V#G=YhsnNjSNv2@U7i{QB@)HFS}~Sa zNf-G*;?QrhrraU^GT(gpu~fEpr$G3uRFU9}6SSSHX<6Kwi7UET=Eupk53a9(n;B`s zB1T&{-U3FovX}`{{4Q=yyzsO7x?_Gd(k3+U&g^URVW; z8YZli5h32=j;u(O?U$93qGeqtbUVw=2_PAf52|z+bj6)V-guF}F!BDGFb`Ap?_{_^ z#QbIU$T~DzQffaIp7d!E<)pgqcsuR!n*^LD>lky^iGHO#A%-BSPY8W6EClfT$g}aj zOy6!+(H@_2FSBCkngzHI5tj_Hv%0{**b&0ne~><2?t02IR*cRUW2mG+MnqL%z2%NX zLR80TXD+nqs#F4c#JS_BKx zWxVuMaNHStgz7GDtP_tL^u#eC@gt)w{qSVeXktV!`%TI^NcEku-S|W(Rvpt{l1rn7 z{8qk(mf#7G5;n^u112@I#afek*4xkhHXaqIDy6NbDdc4VCze0U$ky#E4fvil*`yBJ z$DuWo7vLr!spUh{VQP&D!RgBR3z)hCqCeP+pe(uB@u+iC6wo zSkmxx<}=|0FXOYBS0s!^r-Y4iDVA2P&~ysT3&a(5DK{YqTX!W*^rM`HC({%I{)Ht_ ztKVM7;t}>&cy0QS4IK@S=}QKL%PYu`e*r={+4SD#HtM5!fs~S5S?CdBOzz{iOL_nEbC~g?0hNY4B5xG3nEmrK~#QzB#t&kiC}avZTUaU zO;lC+JLSy@SeW?FBgHOy8AcFk9W6P>ZL9F|6 zfNYBVINQsqcP^qk!?0#hAR!dUyTIY2n}>r}3)2K)z^;hNf&UkGLn@r9BeaV#<7ok( z>MLD?2>psR*u^aXP;Rn&o%X-iSQUYoZ?OmN=}>h3v$G4@xPW}N8aSMJf2-Bf*~y<= KkJ1hY{rWG}7Say@ literal 0 HcmV?d00001 diff --git a/docs/images/breakout-stamps.png b/docs/images/breakout-stamps.png new file mode 100644 index 0000000000000000000000000000000000000000..650ccbe271e0aa05151f2824994d5f5af4d109c9 GIT binary patch literal 388 zcmeAS@N?(olHy`uVBq!ia0vp^DL~xB!2~4j=l+_*z`*G3>EaktG3V`7MK6~`iMGJ? zyEpn7cg`@$-p(o9$~awtM}l>B>}EB=$+@d;7xMDoIo&$HSnR{6k5lriZ0^eRK6?0l z!;60(7rky4)E2-6xf22||NE8lRyu8S_5bLS`{q1HxNt%yi7Q`fcf|j0yE!L+p4i>* zyv!S!a6$$))4O}(*3}-)xtC`zPK4sxnzca1aqIMmF!}BIdDr(Pn(dzdcG2&Bi3(vj gp%^eY54`%vC^1iHw}wr(B#_JC>FVdQ&MBb@09DeE8~^|S literal 0 HcmV?d00001 diff --git a/examples/breakout/characters/README.md b/examples/breakout/characters/README.md new file mode 100644 index 0000000..42613e5 --- /dev/null +++ b/examples/breakout/characters/README.md @@ -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. diff --git a/examples/breakout/characters/breakout.bas b/examples/breakout/characters/breakout.bas new file mode 100644 index 0000000..b446e1d --- /dev/null +++ b/examples/breakout/characters/breakout.bas @@ -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" diff --git a/examples/breakout/sprites/README.md b/examples/breakout/sprites/README.md new file mode 100644 index 0000000..ea5ad38 --- /dev/null +++ b/examples/breakout/sprites/README.md @@ -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. diff --git a/examples/breakout/sprites/art/License.txt b/examples/breakout/sprites/art/License.txt new file mode 100644 index 0000000..d35090b --- /dev/null +++ b/examples/breakout/sprites/art/License.txt @@ -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 \ No newline at end of file diff --git a/examples/breakout/sprites/art/PROVENANCE.md b/examples/breakout/sprites/art/PROVENANCE.md new file mode 100644 index 0000000..adb001f --- /dev/null +++ b/examples/breakout/sprites/art/PROVENANCE.md @@ -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: +* Downloaded: 2026-08-01, `kenney_puzzle-pack-1.zip` +* Author: Kenney () +* 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. diff --git a/examples/breakout/sprites/art/ballBlue.png b/examples/breakout/sprites/art/ballBlue.png new file mode 100644 index 0000000000000000000000000000000000000000..4d0ecc506fcb3524a262533c2210070bc06a24c2 GIT binary patch literal 513 zcmV+c0{;DpP)m01=9j)pat?wMH>K(1?9j)pcuJ0VI>k+E%9j)sC z004N-^P1ZBrQY|G*Y!TR?j^A6F0<^8*7aAy@SoiFwB-0-#qp8X^?AO4Iwe2FW>!aQGxaRpwzVEi>`GC>%aLe+1&-0nu_Nn0aZp!kQ+4e@f?lrdU ziPZFT%<{nL`W>z7de8Hs-SrRhlGEs}cx#uSJMzZ3fmALeXg-RcB|Lkj~ zNlP?Z>w38HsA*_uNrbJQv85*+?C1KIKIj4Wscla1SnT3BKkQN_6yRjdR~sQE$R$1+ zyWt4Q`x-nSxONZRuP6APEiHg0u%)%V1znpm;Qjmul2u0IO!19i00000NkvXXu0mjf D5|8~= literal 0 HcmV?d00001 diff --git a/examples/breakout/sprites/art/ballGrey.png b/examples/breakout/sprites/art/ballGrey.png new file mode 100644 index 0000000000000000000000000000000000000000..6d780d448c95c43e5593547f676bc34e65f95f1c GIT binary patch literal 495 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H3?x5i&EW)6Ea{HEjtmSN`?>!lvI6;&1AIbU zr%s(ZWy%yF2>}o`gq$>K5|9A|GiS~ODnEbz{Ih4zUcY|5YSpU8j~~yPHS6KShYJ@j zeDUJN#fujYA3nTk)24On)_wc-?cKX~KrPFbE!)0*`{&P}Pn|lofB*g`PoBJe`}V?x z3s0Xu{q*V6`t|FVELn2@{{36GZe6)@<@@*VKnL8sdGpn)S5vn^Gf}cL_ld=3#x|zOE*j})f){9VubqF2&EscBYS*^n$-Bf>$`tf$ zD~bzk-f=v7&+3`7^4E7|Z9F?u2=UyLIOm$+^%QM5D?bhqOw#=4Zw1b;<4(fUI@R@+OVA zEtSAAmcc%t%3j6rSHSEdjk$2b=1r^9N3PjYt<+|}S@5`9*49Qfv+l;#VnG&ZNlXui@09A;}(IhI-ki?v)f9l(n+V!D3QEo!{{)U z!78xnB%;?prR0Hn#0UyY3>9z;MXz9EY<)rOsl$JRj&+=}|@^8%YX2|h+&+}}`@?OR9WXJJg$MRyv@pjJfdCv24 z%<^i<@o35Nam?~v#qnp!@n6O9h}83R%<+xZ^hLezYRdA2((^sJ?l!mWM7-}vzVAW1 z?lZOQGPLYExa~5u>o%j&e9!ZH&+}Wv@L0p|D5Tg-zV0Hd=0LjbEvw)pozNeZ#}
)KgD6i-^x9uRS=0m*iHMZ>+g0F4L@fL!w9g@Qq zrs5i?aLe&r#qn{> z@lwF=i`Dd5!|;C5@_^Cva?J5_&GCWJ^IgXAiq-Uq)bxGO@`BRyhtu?f(({JX^n}y& zbj|U0&hmH8@>9X@h}86d(er%J@_Wznde8EB&hlTz@m0d`SHtjN$MP?->0-z7W61Jl z$ns~&@@dNQX36ptrQ#5Mt2DOlO26<{LxE@j000kkQchDpU%#)Md3;g(|QVN}*^=afi}k z#rB3D!enN4XC+Dde7tj?bKZ0BWP*ZJ5e31jeu7l1W(fdHDSl262yj|KE8yrZ_h2v=KR;3Ua4#) zhz`F`?_y5guDgT{TVHazcc3dlWcZywd$9e9n>8OjXh~LrECxt#QN-D@@rws*3!iZs z+0bGB{z?nUXkr<^L40I*P_T9ZeNqV=VI?A-f+QMGafEZCu@!_g&;T%1)q zeyVWr&>e;L^&O62Fd{3|zK@AnR(8On-=3nUM5PgQSEQrK4**uqc86 zGs40nqh(n>px%E4A1#Os59dgVm&ujr z9Q~?^VA0ETiU|uzBKB$+wKE;_PQ(PAOvk+wF@a|~@xzDHOgK*yxgIi`D^rTohpdyN zet{)&33kXJ^<<|XDdga+*UN|BGRRiSOEUchXEIG>_I`|Q5P{2CD*evPMM@&c=k+F; zqMWK^DTxFh0pUzjoEk*QIbinvp-3<_Q%i6zb|>P30J;c%f+*?rOYUFRf1Yr{coD;I Q*#H0l07*qoM6N<$f}-~yjsO4v literal 0 HcmV?d00001 diff --git a/examples/breakout/sprites/art/element_green_polygon_glossy.png b/examples/breakout/sprites/art/element_green_polygon_glossy.png new file mode 100644 index 0000000000000000000000000000000000000000..d3bbd1cf5670347ae52d92ce15977d21cf1fc9c6 GIT binary patch literal 1503 zcmV<51t9u~P)^9;o;0)7VX@L=yW4G|$4#x$Rf?@Rp~XskoG)~hDUq^4j<7w6t~0C8 zR-MH^snbTH!d0oxRI=AwfS@&{$xv~UC3BJ}rp`p1zetU(L7l-!uhUpt2wF8QI@w#jy6( zm@J;aO{2(6cb6-ey+3xBD4)echN(2I&suJdC4r(cbCW5h%SoxqS)9Z@k+MRTz%YHC zF{R2+k+DO6p)irOK9IRAg{&!zuQ_s(C!)tltI<%H!ZohbSChRhjkYUqk0PzmTE5+K zw%KB})@HfeYOmBt%%TuApM~AK{o5MPcv?_w9D37@; za+4#BwJUs~CZ@|yt$ zu+~(u*H^pTY`EHHve#L$*H)_2Nu|n8r^`~N%2ATNEUVN^tJF%Y)lRL|Pp{Tfu+~+s z)lr44C~AozmB22a&M|JP(cJ(501tFhPE$W$zpu}qZ%-dD-w%)P05YX*l>h(&$w@>( zR7l5-m1S4lKoo|DWj!0}?(XjH?(XjH?v6V(97=(tVU?tta(J6b|6}+A_R!g zwS7oPXlUo20FFupArK<_mtt;W1`GnDRbQ~#VeGiO3NIA~j{q@1PcQ?FFQ7c23r+;dC-xp1X&Fb-r*gm=+F-j_7%S2v;G!7dexehvy`rCU`9bAbSfq!R%nt)JL&*fQIH9`l~9t(G)Q6% zjlhWaV1ZsgDaqv;B(sLr_}b#di4go4Q<9SYPlNQ7O-e`aE?`vz6HJeaiH(|Z4HE5taL&v zkwTo3hzJ3Y&uTYx63N-*8&U+GNv<>z{o;70tJ;Y!kwjE5>t%8!x=g=nB3Sh@on|L1 zNFw%X7_~E<@lM19olGaa6ET5jI`zwkvrM>16S*ETyDL+c^0pj*LCU0wEZ&c?9pZ2~SEXOdS*9cseO_;pDM3;tS4m{@5fIKa zPtqVk&IgO{4@HFeIa-3W+?|LECeX#?CkUcmzv%vD{Rf&&wlf~m0#X0~002ovPDHLk FV1f~&*P8$U literal 0 HcmV?d00001 diff --git a/examples/breakout/sprites/art/element_purple_polygon_glossy.png b/examples/breakout/sprites/art/element_purple_polygon_glossy.png new file mode 100644 index 0000000000000000000000000000000000000000..69d92c8afdfd8fe1a274f62c5c1f8b585bb64948 GIT binary patch literal 1466 zcmV;r1x5OaP)k7c8YX_$mv zmvmHujA)x%Kxve1r;ci#UP5eUNpxUDZGm2si({Xba;JS;jaWWqX-RlxNOP8QrEE@n za8Z7PVUluEf{0|9eqD@`aG`fph>vWabykFjWtL<|a)@J^V?}Y4aHM}njD%p5f?$%DaH*JbsFG@> zk!hoxcdVj+vXX0~l53@pXQGpBq>W~tlWeAtYN3#5qKsvom~*L)W}u2?nvZCon02U% zVw|0Mt&eD+jAWgSWuJp#lbd&}j%S{MSCV~Ij(SszfLD@#R*-m7iF{OzcT$LgSd?#2 zevf9On|7>VLv5dYuXk95TR&-rW0ihdj&4kTdR2>TN_%ongI+;vW=3_0V3~wql#6AX zhhvzNZKHo&keG9*d{&KcO@Mk-jDJ^=TtI1LMRIgdg=R){fmoA%R*-yEj+}O^kY=P> zK4)x7d}Bm$h+vv(NqK@_l8|Slc2S3-ezB2gq={jggI|)NezBc+t&wP@g=3YSc&>?J znxTEMpnb5GZm6Goub+Cal4_=tYo?rct%G2bm2Rk&ZK#%SsfuEogkh6}VU&tvn{rNq zl4+%$d9IXfr=WbWjAfjSW}S^^o{VOli)5T)LvUC4Q_K~zY`jg@74)IbzQr`z>xio3hJySux)ySrOm zfqDxqi)_qlgwf}(U0Y&N97qrdDB0l;2+ z!2{5u8Gucwo#3=W$JQ;IcjyeFqc%c>2tB*T#KgvS>jU7aQ4k_w%;3A2JD7n5V0D@c zE;mdZ|3INsVaRY0Bg_Olz=X$?Qp!+(NWn-DY>+tV1>-qmSOK_Q`T}nL)G3uJFI7fO z3mOU1Hh+F4=Lct&b|i>5f8K%*oNenh#>^RfBq-4QIdea9j-{t>f6o~=HZT&znx8oP zCuhT<`2&BSEzJBS&cJcR!(&@2%nt>P1I_Nb~!D*0AqYfNYIXl|V*$hIKY=G6He<3x9INl$;yevf&lQ4 zE}hXAuF+k&2we2EM#4R|bk-=5LW+`z2oWH$wc93%YUVO9rIL5;#JCC?YKK841$zKq4+!z!XcEAc$u5V&IqcAIwUP U)LR)9ZU6uP07*qoM6N<$f>&Um-T(jq literal 0 HcmV?d00001 diff --git a/examples/breakout/sprites/art/element_red_polygon_glossy.png b/examples/breakout/sprites/art/element_red_polygon_glossy.png new file mode 100644 index 0000000000000000000000000000000000000000..f9688ddb4506f260891ea931cfb97878bf56582f GIT binary patch literal 1474 zcmV;z1wHzSP)Oz%en$Dk|$t zOyEjN=U`y-YHHM6T;N__s~jAf6cpSsG1N^>yFNae7Z>bRRqbqSj|~m)W@fD;Bjh|h zz$7H=MMa??Aj&i}o)i?iG&HR%E3Ok0$3{lsX=%ndIH4RI;8$12OH1HkVWA%%x;i@4 zRaLDnF70%5=WA=@H#esyCeuYlx-T!MARy*eR-hUh;7v`(K0fF=I@}Qv#t#pg85yY% z5a>2G#waMQCnw-$X2C*2z&AIV78c-BQ|(`0?^sxv3k%^95!VqB(?G&H~=A@f>V^mB8R5)$)SS?NDN$qx_aB_-q+ z7uFFG(-0884i2*p4xS7Q*%1-j5fSM+I_4f8^l)(IDk|+rNbyoq<`@|BVq)`0N7WG# z?JFyk5fQEp4U`ZN%@Gmg6co@A5%4lH>n0}YA0PB|bdw7U!w(PM5fP{j4f9D!=QA_( zc6Re!UG;^9^ITl?Z*TKUOXxK<={-I5gM;&5U-Wo*^I%}~cX#x8dGu~>^I>82f`avd zf%I){^lNMMdV2MLfAxNT^n85tWMuSZW%Orf^l54HOicBFfb@HN^lED8H#hTRW9T_K z=sP>}P*C$uPTwRX^HEXrQ&aR>JE`auJ0W3np1iKk}bc$-%tq+K{h6q6-4D1^o9ud)RB!H`4K?sJNu|=3$n9*5a zwi^o$C(NC9U*Vp@#841}j07vdtP)BoWfDNJpeG0x$eHNOuYMJFmbD;D#WAkXlZul>k5l#x;Xo-=idXC+7- z{`^mz>YBTllcn!C6C&A45FP%w@t-**6%PqDWp6oShSHTFGW-!=y@;ytkp9Js-ee`n zZh@>`uQ|u+YP>j7`jXQ%5}XKGphf3bob=isKGYSz;52UvfQ42qyOc2wAFU0b`rs2r zvnGwO;DRu>d&s(Ch3yCGG$^WkpfI4L+Z_gGY#pF!&oK?UxQ|tf2##@g8h2{5vJ}w+^YSp*y~|cyS_3e~hV0 zN&2TlQpy&kV^DvvD}o7@MK6urB+K$n?f&cdC@$6=Jq(=WYS>^&oJ`oGE!?1|d83pY zivGoo2#%l8C(;X8la(eG@UkAk!J&L`>kBPW|-bEE6u% zMCwE4R5N8te&nAc^(!oqD!3q<)DxrtGReV3pO+86Wl*4$H|GWlQUXn6^L>n+5QEG4 z8vRo41|^Z`_j;2|@scL_N+Oe=fN-XTk`D25A=vzXC?YJ%)f1$Ro(y%E0+fV(| zF6_i5?#emOtRM2tKJCgt;=)9+jtKkKM!lOAv5yS*&MnQXEaJW}_|;3_xg^1z5vhj= z_0&$cl@!9C8_TB}(y%Glu^sKrPS>?B*S9vmoD#Q{679=G;=nx3 zt0wc(O~Rla_}Esgi3RM(HnETnyqg%elML*|D$J)D;=CrjnGo{JGVRPt=)x=4vn9cv z7304x&7=a-u`bW7Ai9Ho7TK?c@{@P3E!72XLFz?AU=fW-a(Lnv#O#aq8{@P0U#{mA)B>mS${?se} z*-8G=CH>Sf{?|DE)G+VA0shk|{?sb{)j0mqAO6xJ=C}d<);<2w9RATB{?jG>)iwUv zO8(b6{?je^#sJ~80_(g1{mKCJ!vMFG4*kPRB^2#mh!zlXDE9Jc#=er91);s^u71^!==(+*ZsRH)J0OPg+tcnP| zmjc_b0@thp%cBC$r2_lO0P?~B?Y;r$z$N|EGXC6Bs)z#2qXOTu0=bj||Iirz+*JM4 zH2&ac{n<+Y(Hi~KHs`=6{@-Qj!!Z8bSN+&Y{na=A+*SVFSpC&G|Ir-&-(>#ZV*S`h z{@q#r-CO?>2K~zY`jg?h*6hRP1JIVU)1b26LcXxMpcXxM>8xRj6 z4lD$MdvLc9EZElYGiclGm6=KIV|CqgzEjoHg8~(?>h)&j6Qo!*N&sLexnKjRRvp0K zS~9_;g9f!~)~MGQBx6YgKM7hk2@VbkY2FsVR;*wRfB{`DVlH92g@VqcEtoAZWaxFm zHA1hRAo*zt27qC=8TpJp00CAt!RikY!yj_)b9$$P#jGsg;g1;k*vTU&eFuB21i24? z>RZ7J!36h}ATj*u)7}X-ZFngdHAt)kxrRS>=6k`mxH$C5jiz)Z$m9>9&0Y!i z?Em7%_WWmp`gkP>`9sCJ&jmYnefMDP>ZgKoWdX2IsdB>yoON4%_^_(rE~k8%QdqD- z1-re|teb?w^*1Q3e-cpuZ;NLD#Y`KI-tXQFha*~jMzUa#PWtl_}b#dNigmnW_d#5KNS)a z=97;0t-wSCJxmM>kBXLMd5v=aIeauDDm<(+Sm@O-!lcPlWWrWu;XFN*qkE7Wiv=sd z6;|4g$G9+uCgQJ#PCe6c&qQ3%$aKOp5f?sr2Ba$IFM`GRP(6dD*^#BZVb0dOpS$h{WX_g?>l& zY?4UwdcA3;7>6P`B$3`rKsZyTLxmVQ4~*VF6bZ&>s|k)QS0W+kK@+`?V3oA`CD$+O YKM&E?kBnfv*#H0l07*qoM6N<$f^!xI761SM literal 0 HcmV?d00001 diff --git a/examples/breakout/sprites/art/paddleBlu.png b/examples/breakout/sprites/art/paddleBlu.png new file mode 100644 index 0000000000000000000000000000000000000000..57cb69fad6d6eb5f0d4f67b3d511fc4a337ecb9c GIT binary patch literal 762 zcmVc?-00004XF*Lt006O% z3;baP0002qP)t-s5qGK(eyb3Cst|py5Phl;d#wd@EPDyZENjK3GA;-<>lw#?Rz$KX=H?tI1MGPCKw&D6fs-6*c-WW(#D&*65*=!nYY ztkB>RlgSgC)GM&)5|+&ng0UK?;}Vg^6P?#2t>#0!?ly?35}42tj>8_Qx~?+|~k5Phqvs;U&F;!3~p+1c69(9p02goOYA01k9gPE!w0uP>h;?~l(vf4^S< z9YXdW00054Nkldqx8Esf~05EOQ)2Y8vtL)@o9R4DU~M)XuniU!n=Q_#1a*T@?+LDzArD%39^TrgwH30oQxkIh!K<2&D6!=Yas}o1UvotYZ=|nfoGO-6M>2)^H7t0}U=tS2XioiZ#r(?oF92D1I znKw0_%k(c=Ac)~)gKr7q^v28=8qrnSbQU}#>D~D#_kd1&`dx~6T_ZZ?XmJhNoovA7 skCT%1e~*(|srU-=z4HHvTrW3#0w6_*7#k**ZU6uP07*qoM6N<$f|Jj6sQ>@~ literal 0 HcmV?d00001 diff --git a/examples/breakout/sprites/art/paddleRed.png b/examples/breakout/sprites/art/paddleRed.png new file mode 100644 index 0000000000000000000000000000000000000000..480827d6620310b99ece70b89c94897a182133ad GIT binary patch literal 578 zcmeAS@N?(olHy`uVBq!ia0vp^89*$-!3-o7A9#QSSkfJR9T^xl_H+M9WCijU1^9%x z&f(yg$ICkxjOOt0%mpHD?s;GakUNK)8%P4xzEoAcAR@Anm-nfh+*)4VtNi@e6ck=- zX+01TxhN*~Oi}Tcg2EFSna7fn@AUQG8yQ{X=f5u`^iW*Nca&wrGU?}f7R0uz&+e0*2r-J|5_jRjXEgGB>{`AaLQrg_}2T zUdqvH0lI*trF!0S-X zVsXOdf}Qb4=1L*o&^z|mw!ZuH%~GkBH}?L$IpUSf)-npkb9wLbo=6Fpz1by}A(6c` zW7?N3S7j#5TORUGRj}I3%Zq;wr=qRDM!PPL$ihV_3wJ8S3dbJoY;AYD==a(2*Q9lf z{yUyIZF$-vmNM&k7e{v0wK)^~RywFKSiQP)c~ zY-Jg_Z3j)?%UFlZ{P=C+M(0(H`bnLl<{^%=wx464wr4`+zQ1L@EE!yW8O$tqAI*N4 z|6Q#mByZc#4X2zHv_;vMghg&o_ HISCORE# THEN HISCORE# = SCORE# +PRINT "FINAL SCORE " + SCORE# + " BEST " + HISCORE# +END + +REM ===================================================================== +REM Pacing, and the batch boundary it buys +REM +REM TI# is refreshed from the host's clock once per batch, so the step on +REM which it changes is the first step of a batch. Spinning here until it +REM moves is the only way a program in this dialect can find out where a +REM frame boundary is, and a capture that starts anywhere else risks being +REM cut in half by a present. Two jiffies is thirty frames a second. +REM +REM **LASTT# is sampled here rather than carried over from the last +REM frame**, and that is the whole correctness of it. Carried over, a +REM frame whose work ran long would find two jiffies already gone and +REM return on whatever step it happened to be on -- somewhere in the +REM middle of a batch -- and the capture that followed would come back +REM holding half of the frame before it. Sampling here means the loop +REM always sees TI# change under it, and a change is only ever seen on +REM the first step of a batch. +REM ===================================================================== +LABEL PACE +LASTT# = TI# +LABEL PACEEDGE +IF TI# - LASTT# < 2 THEN GOTO PACEEDGE +RETURN + +REM ===================================================================== +REM One capture per frame, and only one +REM +REM Rebuilding the brick stamps comes first, because everything else +REM draws with them and GRAPHIC 5 has just thrown them away. +REM ===================================================================== +REM Labels rather than BEGIN blocks here, and it is not a style choice: a +REM RETURN inside a block leaves the interpreter with no GOSUB to return +REM from and stops the program. +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 + +REM --------------------------------------------------------------------- +REM Six brick stamps, one per row colour. Filled two rules at a time so +REM the set costs about a hundred and thirty lines rather than the two +REM hundred and seventy a rule-at-a-time loop would take. PAINT would be +REM one statement instead of sixteen and is not an option: it costs nearly +REM four milliseconds a call, which is an eighth of a frame. +LABEL DRAWPROTOS +GRAPHIC 5 +GRAPHIC 1, 1 +WIDTH 1 +SHN# = 99 +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$ +SHN# = 6 +DPLAY# = 1 +DHUD# = 1 +RETURN + +REM --------------------------------------------------------------------- +REM The field: walls, whatever bricks are still standing, and whatever +REM lettering belongs on the field. Nothing here computes anything it +REM could have computed on an earlier frame. +LABEL DRAWPLAY +GRAPHIC 1, 1 +WIDTH 2 +COLOR 0, 1 : COLOR 1, 4 : COLOR 2, 8 : COLOR 3, 5 +COLOR 4, 11 : COLOR 5, 16 : COLOR 6, 6 +BOX 5, 2, 62, 797, 597 : BOX 1, 6, 66, 793, 593 +REM Six runs of stamps, one per row colour, because picking the stamp per +REM brick would cost a line per brick inside the one routine in this +REM program that has a deadline. The live list is built row by row, so a +REM row is a contiguous run of it: RS# is where the run starts and RC# how +REM long it is. +Z$ = S0$ : T1# = RS#(0) : T2# = RC#(0) : GOSUB STAMPROW +Z$ = S1$ : T1# = RS#(1) : T2# = RC#(1) : GOSUB STAMPROW +Z$ = S2$ : T1# = RS#(2) : T2# = RC#(2) : GOSUB STAMPROW +Z$ = S3$ : T1# = RS#(3) : T2# = RC#(3) : GOSUB STAMPROW +Z$ = S4$ : T1# = RS#(4) : T2# = RC#(4) : GOSUB STAMPROW +Z$ = S5$ : T1# = RS#(5) : T2# = RC#(5) : GOSUB STAMPROW +REM A FOR inside a BEGIN block does not survive the RETURN at the end of +REM the routine that holds it, so this loop is guarded by a GOTO instead. +IF PN# = 1 THEN DRAW PC#(0), PX1#(0), PY1#(0) TO PX2#(0), PY2#(0) +IF PN# < 2 THEN GOTO PLDONE +FOR I# = 0 TO PN# - 1 + DRAW PC#(I#), PX1#(I#), PY1#(I#) TO PX2#(I#), PY2#(I#) +NEXT I# +LABEL PLDONE +SSHAPE Z$, 0, 60, 800, 600 +SPRSAV Z$, 2 +SPRITE 2, 1, 2 +MOVSPR 2, 0, 60 +SHN# = SHN# + 1 +RETURN + +REM Stamp T2# bricks starting at T1# with the shape in Z$. The count of +REM one is spelled out because a FOR whose bounds are equal does not run +REM its body in this dialect, and one brick left in a row is exactly that. +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 + +REM --------------------------------------------------------------------- +REM The HUD strip. Same shape as DRAWPLAY: one pass over a stroke list +REM that was built on some earlier frame, when there was time. +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 +SHN# = SHN# + 1 +RETURN + +REM ===================================================================== +REM Input +REM +REM GET takes one keystroke per call from a ring the host fills, so the +REM loop empties it every frame. A held arrow arrives as about twenty six +REM repeats a second, which is nearly one a frame -- but the first repeat +REM is a quarter of a second behind the press, and a paddle that stalled +REM for a quarter of a second would be unusable. So a press buys seven +REM frames of travel (PDCO#) and a held key keeps renewing it. +REM ===================================================================== +LABEL READKEYS +GET KEYV# +IF KEYV# = 0 THEN RETURN +IF KEYV# = 1073741904 THEN PDDIR# = 0 - 1 : PDCO# = 7 +IF KEYV# = 1073741903 THEN PDDIR# = 1 : PDCO# = 7 +IF KEYV# = 32 THEN GOSUB PRESSFIRE +IF KEYV# = 112 THEN GOSUB PRESSPAUSE +IF KEYV# = 115 THEN GOSUB PRESSMUTE +IF KEYV# = 113 THEN RUNNING# = 0 +IF KEYV# = 27 THEN RUNNING# = 0 +GOTO READKEYS + +LABEL PRESSFIRE +IF STATE# = 0 THEN GOSUB STARTGAME : RETURN +IF STATE# = 5 THEN GOSUB TITLESCREEN : RETURN +IF STATE# = 1 THEN GOSUB LAUNCH : RETURN +IF STATE# = 2 THEN GOSUB UNSTICK +RETURN + +REM VOL rather than a test around every SOUND: one line here beats eleven +REM scattered through the game, and a silenced voice costs nothing to issue. +LABEL PRESSMUTE +SNDON# = 1 - SNDON# +IF SNDON# = 1 THEN VOL 8 +IF SNDON# = 0 THEN VOL 0 +RETURN + +LABEL PRESSPAUSE +IF STATE# = 2 THEN STATE# = 6 : GMTYP# = 0 : BAN$ = "PAUSED" : GOSUB SETBANNER : RETURN +IF STATE# = 6 THEN STATE# = 2 : BAN$ = "" : GOSUB SETBANNER +RETURN + +REM ===================================================================== +REM The states +REM ===================================================================== +LABEL TITLETICK +STIMER# = STIMER# + 1 +RETURN + +LABEL SERVETICK +GOSUB MOVEPADDLE +BLX%(0) = PDX% + BLOFF# +BLY%(0) = PDY# - 23 +MOVSPR 5, BLX%(0), BLY%(0) +RETURN + +LABEL PLAYTICK +GOSUB MOVEPADDLE +GOSUB MOVEBALLS +GOSUB MOVEGEM +GOSUB TIMERS +IF BRN# = 0 THEN GOSUB LEVELDONE +IF BLN# = 0 AND STATE# = 2 THEN GOSUB BALLGONE +RETURN + +LABEL LOSTTICK +STIMER# = STIMER# - 1 +IF STIMER# > 0 THEN RETURN +IF LIVES# > 0 THEN GOSUB SERVEAGAIN +IF LIVES# < 1 THEN GOSUB GAMEISOVER +RETURN + +LABEL CLEARTICK +STIMER# = STIMER# - 1 +IF STIMER# > 0 THEN RETURN +LEVEL# = LEVEL# + 1 +GOSUB SETUPLEVEL +RETURN + +REM ===================================================================== +REM Paddle +REM ===================================================================== +LABEL MOVEPADDLE +IF PDCO# > 0 THEN BEGIN + PDCO# = PDCO# - 1 + PDX% = PDX% + PDDIR# * 9 +BEND +IF PDCO# < 1 THEN PDDIR# = 0 +IF PDX% < WALLL# THEN PDX% = WALLL# +IF PDX% > WALLR# - PDW# THEN PDX% = WALLR# - PDW# +MOVSPR 3, PDX%, PDY# +IF CTON# = 0 THEN RETURN +REM The catcher mirrors the player, so it guards the side the paddle left. +CTX% = 0.0 - PDX% + WALLL# + WALLR# - 104 +MOVSPR 4, CTX%, 470 +RETURN + +REM ===================================================================== +REM Balls +REM ===================================================================== +LABEL MOVEBALLS +FOR B# = 0 TO 2 + IF BLON#(B#) = 1 THEN GOSUB MOVEONE +NEXT B# +RETURN + +LABEL MOVEONE +IF BLST#(B#) = 1 THEN BEGIN + BLX%(B#) = PDX% + BLOFF# + BLY%(B#) = PDY# - 23 +BEND +IF BLST#(B#) = 0 THEN BEGIN + BLX%(B#) = BLX%(B#) + BLVX%(B#) + BLY%(B#) = BLY%(B#) + BLVY%(B#) + GOSUB BALLWALLS +BEND +IF BLON#(B#) = 0 THEN RETURN +IF BLST#(B#) = 0 THEN BEGIN + GOSUB BALLBRICKS + GOSUB BALLPADDLE +BEND +MOVSPR 5 + B#, BLX%(B#), BLY%(B#) +RETURN + +LABEL BALLWALLS +IF BLX%(B#) < WALLL# THEN BEGIN + BLX%(B#) = WALLL# + BLVX%(B#) = 0.0 - BLVX%(B#) + SOUND 1, 3609, 2 +BEND +IF BLX%(B#) > WALLR# - 22 THEN BEGIN + BLX%(B#) = WALLR# - 22 + BLVX%(B#) = 0.0 - BLVX%(B#) + SOUND 1, 3609, 2 +BEND +IF BLY%(B#) < 72 THEN BEGIN + BLY%(B#) = 72 + BLVY%(B#) = 0.0 - BLVY%(B#) + SOUND 1, 3609, 2 +BEND +IF BLY%(B#) < 596 THEN RETURN +BLON#(B#) = 0 +BLN# = BLN# - 1 +SOUND 2, 2411, 6 +SPRITE 5 + B#, 0 +RETURN + +REM --------------------------------------------------------------------- +REM Brick collision. A ball can only be over four cells at once, so the +REM cells are worked out from its box rather than by walking sixty +REM bricks. DO rather than FOR throughout: a FOR whose bounds are equal +REM does not run its body in this dialect, and a ball inside one column is +REM exactly that case. +LABEL BALLBRICKS +IF BRN# = 0 THEN RETURN +LFT# = BLX%(B#) +TOP# = BLY%(B#) +RGT# = LFT# + 21 +BOT# = TOP# + 21 +IF BOT# < BRKY# THEN RETURN +IF TOP# > BRKY# + 6 * 24 THEN RETURN +CC1# = (LFT# - BRKX#) / 72 +CC2# = (RGT# - BRKX#) / 72 +RR1# = (TOP# - BRKY#) / 24 +RR2# = (BOT# - BRKY#) / 24 +IF CC1# < 0 THEN CC1# = 0 +IF CC2# > 9 THEN CC2# = 9 +IF RR1# < 0 THEN RR1# = 0 +IF RR2# > 5 THEN RR2# = 5 +IF CC2# < CC1# THEN RETURN +IF RR2# < RR1# THEN RETURN +HIT# = 0 +R# = RR1# +DO + C# = CC1# + DO + IF HIT# = 0 THEN GOSUB TESTCELL + C# = C# + 1 + LOOP UNTIL C# > CC2# OR HIT# = 1 + R# = R# + 1 +LOOP UNTIL R# > RR2# OR HIT# = 1 +RETURN + +REM Reflect off whichever face the ball has less of itself past: the +REM standard box resolution, and the reason a ball clipping the end of a +REM row goes sideways instead of straight back down. T1# to T4# are the +REM overlapping rectangle; its width against its height is the whole test. +LABEL TESTCELL +N# = R# * 10 + C# +IF BRK#(N#) = 0 THEN RETURN +BX1# = BRKX# + C# * 72 +BY1# = BRKY# + R# * 24 +IF RGT# < BX1# THEN RETURN +IF LFT# > BX1# + 67 THEN RETURN +IF BOT# < BY1# THEN RETURN +IF TOP# > BY1# + 15 THEN RETURN +T1# = RGT# : IF BX1# + 67 < T1# THEN T1# = BX1# + 67 +T2# = LFT# : IF BX1# > T2# THEN T2# = BX1# +T3# = BOT# : IF BY1# + 15 < T3# THEN T3# = BY1# + 15 +T4# = TOP# : IF BY1# > T4# THEN T4# = BY1# +IF T1# - T2# < T3# - T4# THEN BLVX%(B#) = 0.0 - BLVX%(B#) +IF T1# - T2# >= T3# - T4# THEN BLVY%(B#) = 0.0 - BLVY%(B#) +BRK#(N#) = 0 +BRN# = BRN# - 1 +SCORE# = SCORE# + BRV#(R#) +REM The top row rings at C6 and each row down drops about a third, so the +REM wall plays itself down a scale as it comes apart. +SOUND 1, 17175 - R# * 2100, 4 +HIT# = 1 +GOSUB BUILDLIVE +GOSUB HUDTEXT +DPLAY# = 1 +IF GMON# = 1 THEN RETURN +RNMAX# = 7 +GOSUB NEXTRAND +IF RNVAL# = 0 THEN GOSUB SPAWNGEM +RETURN + +REM --------------------------------------------------------------------- +REM The bars. T1# is the bar's left edge, T2# its top, T3# its width. +LABEL BALLPADDLE +IF BLVY%(B#) < 0 THEN RETURN +HIT# = 0 +T1# = PDX% : T2# = PDY# : T3# = PDW# +GOSUB HITBAR +IF CTON# = 0 THEN RETURN +IF HIT# = 1 THEN RETURN +T1# = CTX% : T2# = 470 : T3# = 104 +GOSUB HITBAR +RETURN + +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 +SOUND 1, 4298, 5 +HIT# = 1 +IF STKON# = 0 THEN RETURN +BLST#(B#) = 1 +BLOFF# = BLX%(B#) - PDX% +RETURN + +LABEL UNSTICK +FOR B# = 0 TO 2 + IF BLST#(B#) = 1 THEN BEGIN + BLST#(B#) = 0 + BLVX%(B#) = ZVX%(4) * SPD% + BLVY%(B#) = ZVY%(4) * SPD% + BEND +NEXT B# +RETURN + +REM ===================================================================== +REM The gem, and what catching one does +REM ===================================================================== +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 +SOUND 2, 10810, 3 +SPRITE 8, 1, 2 +MOVSPR 8, GMX%, GMY% +RETURN + +LABEL MOVEGEM +IF GMON# = 0 THEN RETURN +GMY% = GMY% + 3.2 +MOVSPR 8, GMX%, GMY% +HIT# = 0 +IF GMY% > 596 THEN HIT# = 1 +IF GMY% + 46 > PDY# AND GMY% < PDY# + 24 THEN GOSUB CATCHGEM +IF HIT# = 0 THEN RETURN +GMON# = 0 +SPRITE 8, 0 +RETURN + +LABEL CATCHGEM +IF GMX% + 48 < PDX% THEN RETURN +IF GMX% > PDX% + PDW# THEN RETURN +GOSUB TAKEGEM +HIT# = 1 +RETURN + +REM The x-expand bit is the only scaling a sprite has, and doubling the +REM artwork is exactly what EXPAND wants. +LABEL TAKEGEM +SOUND 2, 8579, 14, 1, 17175, 400 +SCORE# = SCORE# + 50 +GOSUB HUDTEXT +IF GMTYP# = 1 THEN BEGIN + PDW# = 208 + PDEXP# = 600 + SPRITE 3, 1, 2, 0, 1, 0 + BAN$ = "EXPAND" +BEND +IF GMTYP# = 2 THEN BEGIN + BAN$ = "MULTI" + GOSUB SPLITBALLS +BEND +IF GMTYP# = 3 THEN BEGIN + SLOWT# = 480 + SPD% = 4.2 + GOSUB RESCALE + BAN$ = "SLOW" +BEND +IF GMTYP# = 4 THEN BEGIN + STKON# = 1 + STKT# = 540 + BAN$ = "STICKY" +BEND +IF GMTYP# = 5 THEN BEGIN + CTON# = 1 + CTTM# = 720 + SPRITE 4, 1, 2 + BAN$ = "CATCH" +BEND +BANT# = 90 +GOSUB SETBANNER +RETURN + +REM Two more balls, thrown off the first one at slightly different angles +REM so they do not travel as a stack. +LABEL SPLITBALLS +IF BLN# > 2 THEN RETURN +IF BLON#(0) = 0 THEN RETURN +FOR B# = 1 TO 2 + IF BLON#(B#) = 0 THEN BEGIN + BLON#(B#) = 1 + BLST#(B#) = 0 + BLX%(B#) = BLX%(0) + BLY%(B#) = BLY%(0) + BLVX%(B#) = ZVX%(B# * 5 - 4) * SPD% + BLVY%(B#) = ZVY%(B# * 5 - 4) * SPD% + BLN# = BLN# + 1 + SPRITE 5 + B#, 1, 2 + BEND +NEXT B# +RETURN + +REM Bring every ball in play to the current speed without changing where it +REM is heading. +REM +REM Every velocity this program assigns is one of the eight unit vectors in +REM ZVX%/ZVY# times SPD%, and a bounce only ever flips a sign -- so a ball +REM is travelling at exactly the SPD% that was in force when it last left a +REM bar. BSPD% remembers what that was, which makes the rescale a ratio of +REM two speeds rather than a change of magnitude, and no square root is +REM needed. There isn't one in this dialect. +REM +REM Scaling by the vertical component instead, which is what this routine +REM did first, is not a slowdown at all: a shallow ball's small vertical +REM gets stretched up to the new speed and drags the large horizontal with +REM it. SLOW made the ball faster. Sixty degrees of the eight zones are +REM shallow enough to do it. +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 + +REM ===================================================================== +REM Timers +REM ===================================================================== +LABEL TIMERS +IF BANT# > 0 THEN BEGIN + BANT# = BANT# - 1 + IF BANT# = 0 THEN BEGIN + BAN$ = "" + GOSUB SETBANNER + BEND +BEND +IF PDEXP# > 0 THEN BEGIN + PDEXP# = PDEXP# - 1 + IF PDEXP# = 0 THEN BEGIN + PDW# = 104 + SPRITE 3, 1, 2, 0, 0, 0 + BEND +BEND +IF SLOWT# > 0 THEN BEGIN + SLOWT# = SLOWT# - 1 + IF SLOWT# = 0 THEN BEGIN + GOSUB LEVELSPEED + GOSUB RESCALE + BEND +BEND +IF STKT# > 0 THEN BEGIN + STKT# = STKT# - 1 + IF STKT# = 0 THEN BEGIN + STKON# = 0 + GOSUB UNSTICK + BEND +BEND +IF CTTM# > 0 THEN BEGIN + CTTM# = CTTM# - 1 + IF CTTM# = 0 THEN BEGIN + CTON# = 0 + SPRITE 4, 0 + BEND +BEND +RETURN + +REM ===================================================================== +REM Losing a ball, clearing a level, starting and ending a game +REM ===================================================================== +LABEL BALLGONE +SOUND 2, 7218, 30, 2, 1804, 220 +LIVES# = LIVES# - 1 +STATE# = 3 +STIMER# = 45 +GMTYP# = 0 +BAN$ = "MISS" +GOSUB SETBANNER +GOSUB HUDTEXT +RETURN + +LABEL SERVEAGAIN +BAN$ = "" +GOSUB SETBANNER +GOSUB CLEARPOWERS +GOSUB RESETBALL +STATE# = 1 +RETURN + +LABEL GAMEISOVER +PLAY "V3 T1 U8 O4 QG QE QC O3 HG" +IF SCORE# > HISCORE# THEN HISCORE# = SCORE# +STATE# = 5 +GOSUB CLEARPOWERS +BRN# = 0 +FOR I# = 0 TO 59 + BRK#(I#) = 0 +NEXT I# +GOSUB BUILDLIVE +SPRITE 3, 0 +SPRITE 5, 0 +PN# = 0 +TXS# = 9 : TXC# = 4 : TXX# = 148 : TXY# = 230 +TX$ = "GAME OVER" +BUILDH# = 0 +GOSUB BUILDTEXT +TXS# = 5 : TXC# = 2 : TXX# = 200 : TXY# = 380 +T4# = SCORE# +GOSUB FMTNUM +TX$ = "SCORE " + NUM$ +BUILDH# = 0 +GOSUB BUILDTEXT +HN# = 0 +TXS# = 3 : TXC# = 5 : TXY# = 20 : TXX# = 40 +T4# = HISCORE# +GOSUB FMTNUM +TX$ = "BEST " + NUM$ +BUILDH# = 1 +GOSUB BUILDTEXT +TXC# = 6 : TXX# = 400 +TX$ = "SPACE FOR TITLE" +BUILDH# = 1 +GOSUB BUILDTEXT +DPLAY# = 1 +DHUD# = 1 +RETURN + +LABEL LEVELDONE +PLAY "V3 T2 U8 O4 QC QE QG O5 HC" +STATE# = 4 +STIMER# = 60 +GMTYP# = 0 +BAN$ = "CLEAR" +GOSUB SETBANNER +RETURN + +LABEL TITLESCREEN +STATE# = 0 +STIMER# = 0 +GOSUB CLEARPOWERS +BLN# = 0 +BRN# = 0 +FOR I# = 0 TO 59 + BRK#(I#) = 0 +NEXT I# +GOSUB BUILDLIVE +SPRITE 3, 0 +SPRITE 5, 0 +SPRITE 6, 0 +SPRITE 7, 0 +PN# = 0 +TXS# = 12 : TXC# = 2 : TXX# = 120 : TXY# = 170 +TX$ = "BREAKOUT" +BUILDH# = 0 +GOSUB BUILDTEXT +TXS# = 4 : TXC# = 1 : TXX# = 220 : TXY# = 370 +TX$ = "SPACE TO START" +BUILDH# = 0 +GOSUB BUILDTEXT +HN# = 0 +TXS# = 3 : TXC# = 6 : TXX# = 40 : TXY# = 20 +TX$ = "ARROWS MOVE P PAUSE Q QUIT" +BUILDH# = 1 +GOSUB BUILDTEXT +DPLAY# = 1 +DHUD# = 1 +RETURN + +LABEL STARTGAME +PLAY "V3 T2 U8 O4 IC IE IG O5 IC" +SCORE# = 0 +LIVES# = 3 +LEVEL# = 1 +GOSUB SETUPLEVEL +RETURN + +REM --------------------------------------------------------------------- +REM Lay a level out. Every third one is a row shorter, so the field +REM changes shape as well as pace. +LABEL SETUPLEVEL +GOSUB CLEARPOWERS +GOSUB LEVELSPEED +T3# = 6 +IF MOD(LEVEL#, 3) = 2 THEN T3# = 5 +IF MOD(LEVEL#, 3) = 0 THEN T3# = 4 +BRN# = 0 +FOR I# = 0 TO 59 + BRK#(I#) = 0 +NEXT I# +FOR R# = 0 TO 5 + IF R# < T3# THEN GOSUB FILLROW +NEXT R# +GOSUB BUILDLIVE +GOSUB RESETBALL +GOSUB HUDTEXT +GMTYP# = 0 +BAN$ = "" +GOSUB SETBANNER +STATE# = 1 +DPLAY# = 1 +RETURN + +REM Again a subroutine rather than a block, for the same reason. +LABEL FILLROW +FOR C# = 0 TO 9 + BRK#(R# * 10 + C#) = 1 + BRN# = BRN# + 1 +NEXT C# +RETURN + +LABEL LEVELSPEED +SPD% = 5.6 + 0.45 * LEVEL# +IF SPD% > 11.0 THEN SPD% = 11.0 +RETURN + +REM Flatten the brick grid into the list DRAWPLAY walks. Doing it here +REM rather than in the draw routine is the whole trick: this costs four +REM lines a brick and has all the time in the world, the draw costs two +REM and has to finish before the host presents. +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 + +LABEL RESETBALL +FOR B# = 0 TO 2 + BLON#(B#) = 0 + BLST#(B#) = 0 +NEXT B# +SPRITE 6, 0 +SPRITE 7, 0 +BLON#(0) = 1 +BLN# = 1 +BSPD% = SPD% +PDX% = 348 +PDW# = 104 +BLOFF# = 41 +SPRITE 3, 1, 2, 0, 0, 0 +SPRITE 5, 1, 2 +MOVSPR 3, PDX%, PDY# +BLX%(0) = PDX% + BLOFF# +BLY%(0) = PDY# - 23 +MOVSPR 5, BLX%(0), BLY%(0) +RETURN + +LABEL LAUNCH +SOUND 1, 6431, 3 +STATE# = 2 +BLST#(0) = 0 +BLVX%(0) = ZVX%(5) * SPD% +BLVY%(0) = ZVY%(5) * SPD% +RETURN + +LABEL CLEARPOWERS +GOSUB LEVELSPEED +PDW# = 104 +PDEXP# = 0 +SLOWT# = 0 +STKON# = 0 +STKT# = 0 +CTON# = 0 +CTTM# = 0 +GMON# = 0 +BANT# = 0 +BLOFF# = 41 +SPRITE 4, 0 +SPRITE 8, 0 +SPRITE 3, 1, 2, 0, 0, 0 +RETURN + +REM ===================================================================== +REM Text +REM +REM The HUD line, rebuilt whenever a number in it changes. Four colours: +REM labels in cyan, the score in yellow, the lives in light green and the +REM level in light red. Colour is the whole reason the HUD is drawn +REM rather than printed -- the interpreter's text layer is white, and it +REM has no verb that changes that. +REM ===================================================================== +LABEL HUDTEXT +HN# = 0 +BUILDH# = 1 +TXS# = 4 +TXY# = 14 +TXC# = 1 : TXX# = 24 +TX$ = "SCORE" +GOSUB BUILDTEXT +TXC# = 2 : TXX# = 144 +T4# = SCORE# +GOSUB FMTNUM +TX$ = NUM$ +GOSUB BUILDTEXT +TXC# = 1 : TXX# = 316 +TX$ = "LIVES" +GOSUB BUILDTEXT +TXC# = 6 : TXX# = 436 +TX$ = "" + LIVES# +GOSUB BUILDTEXT +TXC# = 1 : TXX# = 500 +TX$ = "LEVEL" +GOSUB BUILDTEXT +TXC# = 4 : TXX# = 620 +TX$ = "" + LEVEL# +GOSUB BUILDTEXT +DHUD# = 1 +RETURN + +REM The banner over the field: whatever BAN$ says, centred, in the colour +REM of the gem that put it there. +LABEL SETBANNER +PN# = 0 +DPLAY# = 1 +IF BAN$ = "" THEN RETURN +BUILDH# = 0 +TXS# = 8 +TXY# = 430 +TXX# = 400 - LEN(BAN$) * 20 +TXC# = 5 +IF GMTYP# = 1 THEN TXC# = 4 +IF GMTYP# = 2 THEN TXC# = 2 +IF GMTYP# = 3 THEN TXC# = 6 +IF GMTYP# = 4 THEN TXC# = 1 +IF GMTYP# = 5 THEN TXC# = 3 +REM Sources 1 to 6 are cyan, yellow, purple, light red, light grey and +REM green; the five gems are red, yellow, green, blue and purple, so the +REM banner is as close to the gem as seven colour sources allow. +TX$ = BAN$ +GOSUB BUILDTEXT +RETURN + +REM --------------------------------------------------------------------- +REM Turn TX$ into strokes, into the HUD list when BUILDH# is 1 and the +REM field list when it is 0. This is the expensive routine in the program, +REM about sixteen lines a character, and it is deliberately nowhere near a +REM capture: it runs when a value changes, and the drawing that replays +REM its output happens on some later frame. +LABEL BUILDTEXT +TLEN# = LEN(TX$) +IF TLEN# = 0 THEN RETURN +TXI# = 0 +DO + GC# = INSTR(ALPHA$, MID(TX$, TXI#, 1)) + IF GC# > 0 - 1 THEN GOSUB BUILDGLYPH + TXI# = TXI# + 1 +LOOP UNTIL TXI# >= TLEN# +RETURN + +LABEL BUILDGLYPH +GN# = FNC#(GC#) +IF GN# = 0 THEN RETURN +GP# = FNI#(GC#) +GX# = TXX# + TXI# * TXS# * 5 +GK# = 0 +DO + P1# = FNS#(GP#) : P2# = FNS#(GP# + 1) : GP# = GP# + 2 + IF BUILDH# = 1 AND HN# < 120 THEN BEGIN + HX1#(HN#) = GX# + (P1# / 10) * TXS# + HY1#(HN#) = TXY# + MOD(P1#, 10) * TXS# + HX2#(HN#) = GX# + (P2# / 10) * TXS# + HY2#(HN#) = TXY# + MOD(P2#, 10) * TXS# + HC#(HN#) = TXC# + HN# = HN# + 1 + BEND + IF BUILDH# = 0 AND PN# < 110 THEN BEGIN + PX1#(PN#) = GX# + (P1# / 10) * TXS# + PY1#(PN#) = TXY# + MOD(P1#, 10) * TXS# + PX2#(PN#) = GX# + (P2# / 10) * TXS# + PY2#(PN#) = TXY# + MOD(P2#, 10) * TXS# + PC#(PN#) = TXC# + PN# = PN# + 1 + BEND + GK# = GK# + 1 +LOOP UNTIL GK# >= GN# +RETURN + +REM T4# as six digits with leading zeros, into NUM$. +LABEL FMTNUM +NUM$ = "" +T2# = T4# +FOR I# = 1 TO 6 + D# = MOD(T2#, 10) + NUM$ = MID("0123456789", D#, 1) + NUM$ + T2# = T2# / 10 +NEXT I# +RETURN + +REM ===================================================================== +REM A generator, because this dialect has no RND +REM +REM The usual constants, kept inside 2^31 so the multiply stays inside a +REM 64 bit integer, and the answer taken from the middle bits because the +REM low ones of a power-of-two modulus barely move. A GOSUB rather than a +REM DEF on purpose: a multi-line DEF that assigns to a global spends a +REM value-pool slot on every call and never gives it back, which empties +REM the pool after a few thousand throws. +REM ===================================================================== +LABEL NEXTRAND +SEED# = MOD(SEED# * 1103515245 + 12345, 2147483648) +RNVAL# = MOD(SHR(SEED#, 16), RNMAX#) +RETURN + +REM ===================================================================== +REM Tables +REM ===================================================================== +LABEL LOADTABLES +FOR I# = 0 TO 5 + READ BRC#(I#) +NEXT I# +FOR I# = 0 TO 5 + READ BRV#(I#) +NEXT I# +FOR I# = 0 TO 7 + READ ZVX%(I#) +NEXT I# +FOR I# = 0 TO 7 + READ ZVY%(I#) +NEXT I# +RETURN + +REM Row colours, top to bottom, and what a brick in that row is worth. +DATA 3, 9, 8, 6, 4, 5 +DATA 60, 50, 40, 30, 20, 10 + +REM The eight landing zones. Each pair is very nearly a unit vector, so +REM the ball leaves a bar at the speed it arrived and only the angle +REM changes. +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 + +REM ===================================================================== +REM The stroke font +REM +REM One glyph per DATA line: how many strokes, then that many pairs of +REM points. A point is coded X*10+Y on a grid four wide and seven tall, +REM so 0 is the top left corner, 30 the top right and 36 the bottom +REM right. INSTR into ALPHA$ is what turns a character into a glyph +REM number, which is why the order of these lines matters. +REM ===================================================================== +LABEL LOADFONT +GX# = 0 +FOR I# = 0 TO 40 + READ N# + FNC#(I#) = N# + FNI#(I#) = GX# + IF N# > 0 THEN GOSUB READGLYPH +NEXT I# +RETURN + +REM Its own subroutine rather than a BEGIN block: a FOR nested inside a +REM block that is itself inside a FOR does not find its own NEXT. +LABEL READGLYPH +FOR K# = 1 TO N# * 2 + READ D# + FNS#(GX#) = D# + GX# = GX# + 1 +NEXT K# +RETURN + +REM A +DATA 4, 0,30, 0,6, 30,36, 3,33 +REM B +DATA 6, 0,6, 0,30, 3,33, 6,36, 30,33, 33,36 +REM C +DATA 3, 0,30, 0,6, 6,36 +REM D +DATA 6, 0,6, 0,20, 20,31, 31,35, 35,26, 26,6 +REM E +DATA 4, 0,6, 0,30, 3,33, 6,36 +REM F +DATA 3, 0,6, 0,30, 3,33 +REM G +DATA 5, 0,30, 0,6, 6,36, 33,36, 13,33 +REM H +DATA 3, 0,6, 30,36, 3,33 +REM I +DATA 1, 10,16 +REM J +DATA 3, 30,36, 6,36, 3,6 +REM K +DATA 3, 0,6, 30,3, 3,36 +REM L +DATA 2, 0,6, 6,36 +REM M +DATA 4, 0,6, 30,36, 0,13, 13,30 +REM N +DATA 3, 0,6, 30,36, 0,36 +REM O +DATA 4, 0,30, 6,36, 0,6, 30,36 +REM P +DATA 4, 0,6, 0,30, 3,33, 30,33 +REM Q +DATA 5, 0,30, 6,36, 0,6, 30,36, 24,36 +REM R +DATA 5, 0,6, 0,30, 3,33, 30,33, 13,36 +REM S +DATA 5, 0,30, 0,3, 3,33, 33,36, 6,36 +REM T +DATA 2, 0,30, 10,16 +REM U +DATA 3, 0,6, 30,36, 6,36 +REM V +DATA 2, 0,16, 16,30 +REM W +DATA 4, 0,6, 30,36, 6,13, 13,36 +REM X +DATA 2, 0,36, 30,6 +REM Y +DATA 3, 0,13, 30,13, 13,16 +REM Z +DATA 3, 0,30, 30,6, 6,36 +REM 0 +DATA 5, 0,30, 6,36, 0,6, 30,36, 30,6 +REM 1 +DATA 2, 10,16, 1,10 +REM 2 +DATA 5, 0,30, 30,33, 3,33, 3,6, 6,36 +REM 3 +DATA 4, 0,30, 30,36, 3,33, 6,36 +REM 4 +DATA 3, 0,3, 3,33, 30,36 +REM 5 +DATA 5, 0,30, 0,3, 3,33, 33,36, 6,36 +REM 6 +DATA 5, 0,30, 0,6, 3,33, 33,36, 6,36 +REM 7 +DATA 2, 0,30, 30,36 +REM 8 +DATA 5, 0,30, 6,36, 0,6, 30,36, 3,33 +REM 9 +DATA 5, 0,30, 0,3, 3,33, 30,36, 6,36 +REM space +DATA 0 +REM colon +DATA 2, 12,13, 14,15 +REM dash +DATA 1, 3,33 +REM full stop +DATA 1, 15,16 +REM exclamation mark +DATA 2, 10,14, 15,16 diff --git a/tests/docs_setups/breakout_art.sh b/tests/docs_setups/breakout_art.sh new file mode 100755 index 0000000..66202d0 --- /dev/null +++ b/tests/docs_setups/breakout_art.sh @@ -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