From 6536658c92905991a813d486e1a4e67e6e61fd93 Mon Sep 17 00:00:00 2001 From: Andrew Kesterson Date: Sun, 2 Aug 2026 15:58:47 -0400 Subject: [PATCH] Add the AKLABS megademo example A six-scene demoscene production that pushes the interpreter to its edges on purpose: an LCG where there is no RND, redrawn strokes where the palette cannot be written, the sprite bank as the scrolltext, a supernova and black hole out of SOLID/COLLISION/RSPPOS, wireframe 3D from two rotations and a perspective divide, and a single-queue beeper-style soundtrack with a noise drum break. The same file runs in the standalone frontend and in the clockless screenshot host, which gets each scene as a still. Three interpreter lessons learned the hard way are recorded in the example's README and TODO.md section 4: a scalar first assigned in a GOSUB dies with its scope, predeclaring scratch runs the 128-slot variable pool dry, and a TI# busy-wait starves the PLAY queue of its one release per frame. Co-Authored-By: Claude Code (Fable 5) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL --- TODO.md | 67 ++ examples/megademo/README.md | 99 +++ examples/megademo/megademo.bas | 1369 ++++++++++++++++++++++++++++++++ 3 files changed, 1535 insertions(+) create mode 100644 examples/megademo/README.md create mode 100644 examples/megademo/megademo.bas diff --git a/TODO.md b/TODO.md index 844a9f2..c1f0758 100644 --- a/TODO.md +++ b/TODO.md @@ -727,6 +727,73 @@ Also on the queue, from the reference's own defect list: so moving one operand out of the way only shifted the collision along. Covered by `tests/language/arrays_in_parameter_lists.bas` and `tests/runtime_evaluate.c`. +### What a demoscene program wanted and could not have + +`examples/megademo/megademo.bas` was written to push the interpreter to its edges, and these +are the edges it hit. None of them blocked the demo — every one has a workaround, and the +program carries all six — but each workaround is a routine every future game will carry too, +and that is the argument for the verb. Ordered by how much BASIC each one would delete. + +- **`RND` is still missing.** Chapter 17 teaches the LCG workaround and the demo's `RANDOM` + label carries it: nine tokens, two convention globals (`RMAX#` in, `RND#` out), and a seed + the caller must remember to take from `TI#`. That is the right *teaching* example and the + wrong thing to make every program transcribe. Closing it is the standard function shape — + dispatch-table row, exec handler, tests, a Chapter 12 row — and Chapter 17's section becomes + a history lesson instead of a requirement. `ASC` is in the same boat with a worse workaround + (`INSTR` into a 41-character alphabet string); `INT` and `SQR` are not, because integer + assignment and `^ 0.5` already are those functions. +- **The palette cannot be written.** `PALETTE[17]` in `src/graphics_tables.c` is `static + const`, and `COLOR` can only choose among its sixteen entries. Palette rotation was the + cheapest animation of the whole era — one register write cycled every pixel of a colour — + and here the same effect is a full redraw: the demo's title "colour cycle" re-strokes the + logo every frame (`DRAWTEXT` with `CM# = 1`), and its raster bars erase and redraw five + ramp lines apiece per frame. Closing it means palette state moves from a static table to + the graphics backend, plus a verb form to set an index (`COLOR` grew nothing new on a C128, + so this would be ours — Chapter 13 material), plus the docs figures that assume Pepto's + values stay put. +- **`GSHAPE` stamps in one mode.** BASIC 7.0's takes a mode argument — replace, OR, AND, + XOR — and XOR is the one that mattered: stamp to draw, stamp again to erase, no damage to + what was underneath. That is software sprites beyond the eight real ones, and it is the + difference between the demo's raster bars chewing holes in the starfield (they erase in + black) and leaving it intact. Needs a blend argument through `SSHAPE`/`GSHAPE`'s render + path in `src/graphics_akgl.c`, and libakgl may need to offer the blend mode first — check + before filing there. +- **Nothing reports whether `PLAY` has drained.** The queue depth exists in + `akbasic_AudioState` (`count - head`); no function reads it. The demo's `TUNE` label + requeues each batch on a wall-clock guess — it stamps `MT#` with the batch's computed + length plus a breath, and the sums drift against `TEMPO` and had better stay under the + queue's 128-note cap when a requeue lands early. An `RPLAY(v)` returning notes pending — + same shape as `RSPPOS` — deletes the arithmetic. It also fixes the quiet failure Chapter 7 + warns about, where a program `QUIT`s before its music finishes and never knows. +- **`PLAY` is monophonic: one queue, one clock, one instrument per batch.** An earlier + version of this entry claimed there was no noise waveform, and that was wrong — + `ENVELOPE`'s sixth argument selects triangle, sawtooth, square or noise + (`src/runtime_audio.c`), which Chapter 7 does not document and should. The real gap is in + `akbasic_play_service()` (`src/play.c`): every voice's notes go into **one** queue with one + release clock, and the envelope applied when a note sounds is whatever the last `T` + *parsed* selected — so a program that writes three `V` tracks gets them end to end, on one + instrument, and `M` is a no-op precisely because there is nothing to synchronise. The + backend already has three independent voices and `tone()` takes a voice number; what is + wanted is a queue head and clock per voice, and the envelope resolved at parse time into + the note rather than read from shared state at release. The demo works around it the way + 1985 did on one channel: an interleaved kick–bass–arpeggio line (the `TUNE` label), which + is authentic and should not be mandatory. +- **The only vsync is spinning on `TI#`, and the spin is worse than it looks.** Chapter 13 + documents the polling; the demo's first cut had the loop — read `TI#`, branch to yourself + until it moves — and it did not just waste CPU, it **starved the `PLAY` queue dry**. The + chain is measurable: the host calls `settime()` once per `runtime_run()` batch, and + `akbasic_play_service()` can therefore release at most one note per batch (after a release, + `nextms` sits ahead of the frozen clock). A batch of 256 *held* `SLEEP` steps is nearly + free, but a batch of 256 *executed* spin statements is slow enough to drop the release rate + below a sixteenth-note tune's fifteen notes a second — and the queue never reclaims slots + until it drains completely, so the backlog compounded across scenes and overflowed the + 128-note queue with `PLAY queue is full`. Reproduced both ways in isolation: the same tune + requeued eight times over a `SLEEP` loop survives, over a `TI#` spin it dies on the third + batch. The demo's `VSYNC` label now sleeps a frame instead. A verb that parks the program + until the next batch boundary (`SLEEP 0` is an available spelling) is the same + hold-without-blocking shape `SLEEP` and `GETKEY` already have in `src/runtime_console.c`, + and it would make the honest spelling also the safe one. + --- ## 5. Deliberate deviations from the reference diff --git a/examples/megademo/README.md b/examples/megademo/README.md new file mode 100644 index 0000000..70cd44b --- /dev/null +++ b/examples/megademo/README.md @@ -0,0 +1,99 @@ +# AKLABS MEGADEMO '85 + +A demoscene production for akbasic, written as if it were 1985 and this dialect were +the machine under the tree. Code and FX by Tachikoma, dedicated to GameCube, +megagreetz to starfort. + +```sh +./build-akgl/basic examples/megademo/megademo.bas +``` + +Q or ESC quits. Six scenes: a title card, a Lissajous braid, a spinning wireframe +cube and octahedron — which drift together, ignite a pulsing origin point strobing +the next scene's colours, swallow the camera as the perspective scale outgrows the +window, and split that origin into the first two raster bars — then the raster +bars proper, the sprite bank going supernova — the letters fall into the middle, a +fireball throws them at the walls, and a black hole drags everything back into the +centre as the transition out — and a finale that composes all of it. A sawtooth +chiptune in A minor throughout, with a noise drum break, if a SID answers. + +## What it leans on + +This is a tour of the interpreter's edges, on purpose: + +- **No `RND`** — it carries Chapter 17's LCG, seeded from `TI#`. +- **No palette writes** — the "colour cycle" on the logo is an honest redraw of the + same strokes in the next colour, and the raster bars are erased and redrawn two + scanlines at a time. The palette is sixteen fixed colours and the demo says so on + screen. +- **Eight sprites** — so the lettering *is* the sprite bank. Each letter of GAMECUBE + is drawn with the stroke font from `examples/breakout`, captured with `SSHAPE`, + installed with `SPRSAV`, and tinted with `SPRITE`. +- **`SOLID` walls and `COLLISION 2`** — the supernova registers four rectangles and + reverses a bearing per `RCOLLISION(n, 7)`. The interpreter does the physics; + BASIC does one subtraction. The letters leave the blast on `MOVSPR`'s continuous + form, eight bearings fanned evenly around the compass. +- **The black hole is `RSPPOS` plus one rotation** — read each letter's position + back, rotate the offset from centre, shrink it by 0.86, put it there. Thirty-four + frames later everything is in the middle. The screen goes with it: an iris of + collapsing black `CIRCLE`s, which works because the drawing verbs clip silently + at the window edge — a fireball and an iris can both start bigger than the + screen. +- **The camera is a multiplier** — the wireframe zoom-through is nothing but the + perspective scale growing fourteen percent a frame until the solids' edges leave + the window, which the silent clipping permits. The pulsing origin point strobes + through the bar ramp table because it *is* the next scene: it splits, rides the + Y axis to the band edges, and stretches into the first two raster bars in place. +- **3D with no matrices** — the wireframe scene rotates fourteen pooled vertices + through two composed axis rotations and a perspective divide, all of it `SIN`, + `COS` and one division, with the float kept on the left of every product because + the left operand decides the arithmetic. Erase is the raster bars' trick: redraw + the previous frame's twenty-four edges in black. +- **`PLAY` is one queue** — every note holds it for its length and the whole batch + plays on one instrument, so three `V` tracks come out end to end, not layered. The + soundtrack is therefore beeper music, the way single-channel tracker tunes were + written: sixteenths that take turns being the kick (an `O1` root), the pumping bass + (`O2`/`O3` octaves) and the rolling arpeggio (`O4` and up), two alternating + eight-beat batches — Am F C G, then Am F Dm E with the G sharp — and every third + batch a bar of noise drums. Noise is real: `ENVELOPE`'s sixth argument is a + waveform, 0 to 3, which Chapter 7 forgot to mention. Voice 1 carries the tune so + the collision blips (voice 2) and scene sweeps (voice 3) never steal its channel. + +It also probes for its hardware like a proper boot loader, with one `TRAP` per +device: no graphics refuses by name and exits, no audio mutes the soundtrack, no +text grid drops the status row, and a host that never sets the clock — which is +exactly what `tools/screenshot.c` is — renders each scene as a still and leaves the +finale as the picture. The same file runs everywhere; nothing hangs. + +## The ones that bit + +**A scalar first assigned inside a `GOSUB` dies with that call's scope.** The first +cut computed text centring in a subroutine and read the result after `RETURN`; every +line of text rendered flush left, because the variable read back as a fresh zero. +Reads climb the scope chain, so a nested subroutine can *use* your globals — but a +write to a name that exists nowhere creates it in the current scope, and the scope +is not coming back. That is why Chapter 18's listings predeclare. + +**Then the predeclaring bit back.** The obvious lesson — predeclare everything — ran +the 128-slot variable pool dry the moment the wireframes arrived: `Maximum runtime +variables reached`. A predeclared scratch variable occupies a slot forever; a scoped +one gives its slot back at `RETURN`. So the rule is narrower than it first looks: +predeclare a subroutine's *outputs* and nothing else. This program predeclares four. + +**And the raster wait strangled the SID.** The `VSYNC` label used to spin on `TI#` +until the jiffy turned — romantic, and fatal. The host releases at most one queued +`PLAY` note per frame, and a frame of 256 *executed* spin statements is slow enough +to drop the release rate below the tune's fifteen notes a second. The queue never +reclaims slots until it drains completely, so the backlog compounded quietly for two +scenes and then `PLAY queue is full` killed the show. `VSYNC` now sleeps a frame — +`SLEEP` holds at no per-step cost — and `TUNE` traps the overflow on hosts slower +than this one, dropping a bar instead of the whole demo. `TODO.md` §4 carries the +measurement. + +## What it wanted and could not have + +Recorded properly in `TODO.md` §4, "What a demoscene program wanted and could not +have": `RND` as a real function, palette writes, `GSHAPE` XOR stamping, a way to ask +whether `PLAY` has drained, per-voice `PLAY` queues so three voices can actually +sound together, and a frame-edge wait that is not a busy loop. None of them blocked +the demo; all of them left a routine behind that every future game will carry too. diff --git a/examples/megademo/megademo.bas b/examples/megademo/megademo.bas new file mode 100644 index 0000000..db90aca --- /dev/null +++ b/examples/megademo/megademo.bas @@ -0,0 +1,1369 @@ +REM ===================================================================== +REM +REM A K L A B S M E G A D E M O ' 8 5 +REM +REM CODE AND FX ............. TACHIKOMA +REM DEDICATED TO ............ GAMECUBE +REM MEGAGREETZ .............. STARFORT +REM +REM A demoscene production for the akbasic interpreter, written as if +REM it were 1985 and this dialect were the machine under the tree. +REM It leans on every corner of the interpreter on purpose: +REM +REM - there is no RND, so it carries chapter 17's LCG and seeds it +REM from the jiffy clock +REM - the palette cannot be rewritten, so every "colour cycle" is an +REM honest redraw of the same strokes in the next colour +REM - there are eight sprites, so the lettering IS the sprite bank: +REM each letter of GAMECUBE is drawn with the stroke font, captured +REM with SSHAPE and installed with SPRSAV +REM - SOLID walls and COLLISION 2 bounce that lettering around the +REM screen with RCOLLISION(n, 7) saying which axis to reverse +REM - the PLAY queue is one queue -- monophonic, one instrument per +REM batch (TODO.md 4) -- so the soundtrack is beeper-style: one +REM interleaved line that is its own kick, bass and arpeggio, +REM with a noise drum break every third batch. Voice 1 is music, +REM voice 2 the collision blips, voice 3 the scene sweeps +REM - 3D wireframes with no matrices: two composed rotations and a +REM perspective divide per vertex, floats kept on the left the +REM whole way because the left operand decides the arithmetic +REM +REM It probes for its hardware like a proper boot loader. No audio +REM device mutes it. No text grid drops the status row. A host with no +REM clock -- tools/screenshot.c -- gets each scene as a still and the +REM finale as the picture, because every animation loop is gated on +REM TI# actually moving. +REM +REM Q or ESC quits. TODO.md section 4 records what this program wanted +REM and could not have. +REM +REM ===================================================================== + +REM ---- every pool up front: the DIM pool never frees (chapter 13) ---- +DIM FNC#(41) +DIM FNI#(41) +DIM FNS#(280) +DIM RBW#(7) +DIM STC#(4) +DIM BARC#(20) +DIM SBG#(9) +DIM BYO%(4) +DIM WVX%(14) +DIM WVY%(14) +DIM WVZ%(14) +DIM WPX#(14) +DIM WPY#(14) +DIM WQX#(14) +DIM WQY#(14) +DIM WE1#(24) +DIM WE2#(24) + +ALPHA$ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 :-.!" +GC$ = "GAMECUBE" +SEED# = 314159 +QF# = 0 +RB# = 0 +PH% = 0 +MT# = 0 +W# = 320 +H# = 200 + +REM A scalar FIRST assigned inside a GOSUB lives in that call's scope +REM and dies at RETURN, so a subroutine's OUTPUT must be assigned here +REM first to make it a global -- that is why chapter 18 predeclares. +REM But only the outputs: there are 128 variable slots in the whole +REM machine and a predeclared scratch variable occupies one forever, +REM where a scoped one gives its slot back. The first cut of this +REM program predeclared everything and ran the pool dry. +RMAX# = 0 +RND# = 0 +TX% = 0 +TB# = 0 + +REM ===================================================================== +REM Probe the machine. Graphics is mandatory; everything else degrades. +REM ===================================================================== +GFX# = 1 +TRAP NOGFX +W# = RGR(1) +H# = RGR(2) +TRAP +IF GFX# = 1 THEN GOTO HAVEGFX +PRINT "THIS DEMO NEEDS THE SDL BUILD (-DAKBASIC_WITH_AKGL=ON)." +END +LABEL HAVEGFX + +SND# = 1 +TRAP NOSID +SOUND 1, 17175, 4 +VOL 12 +TRAP + +TXT# = 1 +TRAP NOGRID +WINDOW 0, 0, 39, 0 +TRAP + +KEYS# = 1 +K# = 0 +TRAP NOKEYS +GET K# +TRAP + +REM A host that never sets the clock is the screenshot tool. TI# stays +REM zero there, so every wait would spin forever: render stills instead. +HL# = 0 +SLEEP 0.3 +IF TI# = 0 THEN HL# = 1 +SEED# = SEED# + TI# + +GOSUB LOADTABLES +GOSUB LOADWIRE +GOSUB LOADFONT +IF SND# = 1 THEN GOSUB SIDINIT +GOSUB TUNE +IF TXT# = 1 THEN CHAR 1, 0, 0, "AKLABS MEGADEMO 85 - PRESS Q TO EXIT" + +REM ===================================================================== +REM SCENE 1 : TITLE. Starfield from the LCG, the logo in rainbow, +REM and the "palette cycle" that is really a redraw. +REM ===================================================================== +GRAPHIC 1, 1 +WIDTH 1 +NS# = 120 +GOSUB STARS +WIDTH 2 +T$ = "AKLABS" +SZ% = 0.025 * W# +GOSUB CENTERX +TY% = 0.12 * H# +CM# = 1 +GOSUB DRAWTEXT +CM# = 0 +COLOR 1, 2 +T$ = "PRESENTS" +SZ% = 0.008 * W# +GOSUB CENTERX +TY% = 0.45 * H# +GOSUB DRAWTEXT +COLOR 1, 4 +T$ = "A 1985 DEMO FOR GAMECUBE" +SZ% = 0.006 * W# +GOSUB CENTERX +TY% = 0.58 * H# +GOSUB DRAWTEXT +GOSUB SWEEP +IF HL# = 1 THEN GOTO S1DONE +T$ = "AKLABS" +SZ% = 0.025 * W# +GOSUB CENTERX +TY% = 0.12 * H# +DL# = TI# + 420 +DO WHILE TI# < DL# + RB# = RB# + 1 + CM# = 1 + GOSUB DRAWTEXT + CM# = 0 + GOSUB POLLKEY + IF QF# = 1 THEN EXIT + SLEEP 0.12 +LOOP +LABEL S1DONE +IF QF# = 1 THEN GOTO BYE + +REM ===================================================================== +REM SCENE 2 : LISSAJOUS BRAID. Twelve phase-shifted 3:2 curves, drawn +REM once each -- the drawing layer persists, so the braid accumulates. +REM ===================================================================== +GRAPHIC 1, 1 +WIDTH 1 +CX% = 0.5 * W# +CY% = 0.5 * H# +R1% = 0.3 * W# +R2% = 0.38 * H# +PN# = 0 +DO WHILE PN# < 12 + COLOR 1, RBW#(MOD(PN#, 7)) + GOSUB SPIRO1 + IF HL# = 0 THEN SLEEP 0.25 + GOSUB POLLKEY + IF QF# = 1 THEN EXIT + PN# = PN# + 1 +LOOP +WIDTH 2 +COLOR 1, 2 +T$ = "TACHIKOMA FX" +SZ% = 0.007 * W# +GOSUB CENTERX +TY% = 0.88 * H# +GOSUB DRAWTEXT +IF HL# = 0 THEN SLEEP 1.5 +IF QF# = 1 THEN GOTO BYE + +REM ===================================================================== +REM SCENE 3 : WIREFRAMES. A cube and an octahedron out of one pooled +REM vertex and edge table -- two composed rotations and a perspective +REM divide per vertex, no matrices anywhere. Each frame erases the +REM previous edges in black and draws the new ones, the raster bars' +REM trick applied to twenty-four lines. A clockless host gets five +REM exposures without the erase: a tumble, as a still. +REM +REM The way out is the way in: the solids drift together, the point +REM they share ignites -- pulsing through the bar ramp table, because +REM it is the origin of the next scene -- the camera dives through +REM the frames as the perspective scale outgrows the window, and the +REM origin splits in two, rides the Y axis to the bars' band, and +REM stretches into the first two raster bars itself. +REM ===================================================================== +GRAPHIC 1, 1 +WIDTH 1 +NS# = 90 +GOSUB STARS +WIDTH 2 +COLOR 1, 2 +T$ = "TWO SOLIDS - NO MATRICES - PURE SIN" +SZ% = 0.0045 * W# +GOSUB CENTERX +TY% = 0.06 * H# +GOSUB DRAWTEXT +SF% = 0.24 * H# +CY% = 0.55 * H# +A1% = 0.4 +B1% = 0.9 +A2% = 1.1 +B2% = 0.3 +FC# = 0 +IF HL# = 0 THEN GOTO WIRELIVE +PN# = 0 +DO WHILE PN# < 5 + RA% = A1% + RB% = B1% + CX% = 0.3 * W# + VS# = 0 + VE# = 8 + ES# = 0 + EE# = 12 + GOSUB WIRESPIN + COLOR 1, RBW#(PN#) + GOSUB WIREDRAW + RA% = A2% + RB% = B2% + CX% = 0.7 * W# + VS# = 8 + VE# = 14 + ES# = 12 + EE# = 24 + GOSUB WIRESPIN + GOSUB WIREDRAW + A1% = A1% + 0.3 + B1% = B1% + 0.19 + A2% = A2% - 0.26 + B2% = B2% + 0.17 + PN# = PN# + 1 +LOOP +GOTO WIREDONE +LABEL WIRELIVE +C1X% = 0.3 * W# +C2X% = 0.7 * W# +DL# = TI# + 360 +DO WHILE TI# < DL# + GOSUB SPINSTEP + IF TI# > MT# THEN GOSUB TUNE + GOSUB POLLKEY + IF QF# = 1 THEN EXIT + GOSUB VSYNC +LOOP +IF QF# = 1 THEN GOTO WIREDONE +REM Converge: the two solids drift to the shared centre, spinning +REM the whole way in. +XS1% = (0.2 * W#) / 36 +GF# = 0 +DO WHILE GF# < 36 + C1X% = C1X% + XS1% + C2X% = C2X% - XS1% + GOSUB SPINSTEP + IF TI# > MT# THEN GOSUB TUNE + GOSUB POLLKEY + GOSUB VSYNC + GF# = GF# + 1 +LOOP +C1X% = 0.5 * W# +C2X% = 0.5 * W# +REM The origin point ignites inside them. +BF# = 0 +PB% = 0 +BBR% = 22.0 +GF# = 0 +DO WHILE GF# < 40 + GOSUB SPINSTEP + GOSUB BALLFX + IF TI# > MT# THEN GOSUB TUNE + GOSUB POLLKEY + GOSUB VSYNC + GF# = GF# + 1 +LOOP +REM The camera goes in: the perspective scale grows past the window +REM and the solids pass out of view around us. The drawing verbs +REM clip silently, so the coordinates are allowed to leave. +GOSUB ZOOMSND +GF# = 0 +DO WHILE GF# < 26 + SF% = 1.14 * SF% + BBR% = BBR% + 1.2 + GOSUB SPINSTEP + GOSUB BALLFX + IF TI# > MT# THEN GOSUB TUNE + GOSUB POLLKEY + GOSUB VSYNC + GF# = GF# + 1 +LOOP +ES# = 0 +EE# = 24 +GOSUB WIRERASE +REM The origin splits; each half rides the Y axis out to where the +REM bars are about to be born. +IF SND# = 1 THEN SOUND 2, 30000, 4 +YO% = 0 +GF# = 0 +DO WHILE GF# < 18 + DCI# = 1 + DR% = PB% + DCX% = C1X% + DCY% = CY% - YO% + GOSUB DISC + DCY% = CY% + YO% + GOSUB DISC + YO% = YO% + 4.4 + IF YO% > 70 THEN YO% = 70 + BF# = BF# + 1 + DR% = 16.0 + (6.0 * SIN(1.1 * BF#)) + DCI# = BARC#(MOD(BF#, 20)) + DCY% = CY% - YO% + GOSUB DISC + DCI# = BARC#(MOD(BF# + 10, 20)) + DCY% = CY% + YO% + GOSUB DISC + PB% = DR% + GOSUB POLLKEY + GOSUB VSYNC + GF# = GF# + 1 +LOOP +DCI# = 1 +DR% = PB% +DCY% = CY% - YO% +GOSUB DISC +DCY% = CY% + YO% +GOSUB DISC +REM Each ball shoots out to the full width of the screen and is a +REM raster bar by the time it gets there. +XR% = 4.0 +DO WHILE XR% < (0.5 * W#) + BY% = (CY% - YO%) - 9 + BR# = 0 + GOSUB PROTOBAR + BY% = (CY% + YO%) - 9 + BR# = 5 + GOSUB PROTOBAR + XR% = XR% + (0.045 * W#) + GOSUB POLLKEY + GOSUB VSYNC +LOOP +SLEEP 0.3 +LABEL WIREDONE +IF QF# = 1 THEN GOTO BYE + +REM ===================================================================== +REM SCENE 4 : RASTER BARS. The palette is not writable, so these are +REM not palette tricks: every frame erases four bars in black and +REM redraws them two scanlines at a time. Racing TI# is the beam. +REM ===================================================================== +GRAPHIC 1, 1 +WIDTH 1 +NS# = 90 +GOSUB STARS +WIDTH 2 +COLOR 1, 2 +T$ = "NO PALETTE WRITES - EVERY CYCLE REDRAWN" +SZ% = 0.0045 * W# +GOSUB CENTERX +TY% = 0.06 * H# +GOSUB DRAWTEXT +CY3% = 0.55 * H# +IF HL# = 0 THEN GOTO BARLIVE +BI# = 0 +DO WHILE BI# < 4 + BY% = CY3% + (70.0 * SIN(1.5 * BI#)) + BR# = BI# * 5 + EB# = 0 + GOSUB DRAWBAR + BI# = BI# + 1 +LOOP +GOTO BARDONE +LABEL BARLIVE +DL# = TI# + 480 +DO WHILE TI# < DL# + BI# = 0 + DO WHILE BI# < 4 + BY% = BYO%(BI#) + BR# = BI# * 5 + EB# = 1 + GOSUB DRAWBAR + BI# = BI# + 1 + LOOP + PH% = PH% + 0.13 + BI# = 0 + DO WHILE BI# < 4 + BY% = CY3% + (70.0 * SIN(PH% + (1.5 * BI#))) + BR# = BI# * 5 + EB# = 0 + GOSUB DRAWBAR + BYO%(BI#) = BY% + BI# = BI# + 1 + LOOP + IF TI# > MT# THEN GOSUB TUNE + GOSUB POLLKEY + IF QF# = 1 THEN EXIT + GOSUB VSYNC +LOOP +LABEL BARDONE +IF QF# = 1 THEN GOTO BYE + +REM ===================================================================== +REM SCENE 5 : THE SPRITE BANK IS THE SCROLLTEXT, AND THEN IT EXPLODES. +REM Eight letters, eight sprites: draw each with the stroke font, +REM SSHAPE it, SPRSAV it, surf it on a sine. Then the letters fall +REM into the middle, a supernova throws them at the walls -- SOLID +REM and COLLISION 2 do the physics while BASIC reverses a bearing -- +REM and a black hole drags everything back to the centre as the +REM transition out. A clockless host gets the wave over a frozen +REM fireball as its still. +REM ===================================================================== +WIDTH 2 +SZ% = 7 +L# = 1 +DO WHILE L# < 9 + GRAPHIC 1, 1 + WIDTH 2 + COLOR 1, 2 + C$ = MID(GC$, L# - 1, 1) + GI# = INSTR(ALPHA$, C$) + GX2% = 10 + GY2% = 10 + GOSUB DRAWGLYPH + SSHAPE SH$, 6, 6, 42, 56 + SPRSAV SH$, L# + SPRITE L#, 1, RBW#(MOD(L# - 1, 7)) + L# = L# + 1 +LOOP +GRAPHIC 1, 1 +WIDTH 1 +NS# = 100 +GOSUB STARS +WIDTH 2 +SPX% = 0.055 * W# +WX0% = 0.5 * (W# - (SPX% * 8)) +WCY% = 0.5 * H# +AMP% = 60.0 +GOSUB WAVE +CX0% = 0.5 * W# +CY0% = 0.5 * H# +PR% = 0 +IF HL# = 0 THEN GOTO SNLIVE +ER% = 130 +GOSUB BOOMFX +GOTO WAVEDONE +LABEL SNLIVE +DL# = TI# + 420 +DO WHILE TI# < DL# + PH% = PH% + 0.15 + GOSUB WAVE + IF TI# > MT# THEN GOSUB TUNE + GOSUB POLLKEY + IF QF# = 1 THEN EXIT + GOSUB VSYNC +LOOP +IF QF# = 1 THEN GOTO BYE +COLOR 1, 2 +T$ = "EIGHT SPRITES - ONE SUPERNOVA - ZERO MATHS" +SZ% = 0.0045 * W# +GOSUB CENTERX +TY% = 0.03 * H# +GOSUB DRAWTEXT +REM Wind-up: the letters fall into the middle on a slow spiral. +VC% = COS(0.1) +VS2% = SIN(0.1) +VF% = 0.78 +GF# = 0 +DO WHILE GF# < 14 + GOSUB VORTEX + GOSUB VSYNC + GF# = GF# + 1 +LOOP +REM The supernova. Every letter leaves the centre on its own bearing, +REM eight of them fanned evenly around the compass, fast, while the +REM fireball expands around them and the walls wait to ring. +GOSUB BOOMSND +SOLID 61, 0, 0, W# - 1, 12 +SOLID 62, 0, H# - 13, W# - 1, H# - 1 +SOLID 63, 0, 0, 12, H# - 1 +SOLID 64, W# - 13, 0, W# - 1, H# - 1 +COLLISION 2, BOUNCE +L# = 1 +DO WHILE L# < 9 + SBG#(L#) = MOD((L# * 45) + 23, 360) + MOVSPR L#, CX0%, CY0% + MOVSPR L#, SBG#(L#) # 14 + L# = L# + 1 +LOOP +ER% = 16 +DO WHILE ER% < 430 + GOSUB BOOMFX + ER% = (1.18 * ER%) + 6 + GOSUB POLLKEY + IF QF# = 1 THEN EXIT + GOSUB VSYNC +LOOP +BCF# = 1 +BR2% = PR% +GOSUB BOOMPASS +REM Debris. Let them ricochet for a while. +DL# = TI# + 300 +DO WHILE TI# < DL# + GOSUB SHEPHERD + IF TI# > MT# THEN GOSUB TUNE + GOSUB POLLKEY + IF QF# = 1 THEN EXIT + SLEEP 0.05 +LOOP +COLLISION 2 +SOLID +L# = 1 +DO WHILE L# < 9 + MOVSPR L#, 0 # 0 + L# = L# + 1 +LOOP +IF QF# = 1 THEN GOTO WAVEDONE +REM The black hole. An iris of collapsing black circles swallows the +REM screen from the outside in, the accretion arcs spin over the top, +REM and the vortex drags every letter into the middle. The clear at +REM the end is the event horizon closing over the corners. +GOSUB HOLESND +VC% = COS(0.26) +VS2% = SIN(0.26) +VF% = 0.86 +HA0# = 0 +IR% = 470 +HF# = 0 +DO WHILE HF# < 34 + GOSUB HOLEFX + IR% = IR% - 14 + IF IR% < 24 THEN IR% = 24 + HAC# = 0 + GOSUB HOLEARC + HA0# = MOD(HA0# + 26, 360) + IF IR% > 70 THEN HAC# = 1 + IF IR% > 70 THEN GOSUB HOLEARC + GOSUB VORTEX + GOSUB POLLKEY + GOSUB VSYNC + HF# = HF# + 1 +LOOP +GRAPHIC 1, 1 +LABEL WAVEDONE +IF QF# = 1 THEN GOTO BYE + +REM ===================================================================== +REM SCENE 6 : THE FINALE CARD. Everything at once: stars, bars, the +REM logo, the greetz, and the sprite bank surfing a sine. This is the +REM frame a clockless host leaves on the screen. +REM ===================================================================== +GRAPHIC 1, 1 +WIDTH 1 +NS# = 150 +GOSUB STARS +WIDTH 2 +EB# = 0 +BY% = 26 +BR# = 10 +GOSUB DRAWBAR +BY% = H# - 24 +BR# = 5 +GOSUB DRAWBAR +T$ = "AKLABS" +SZ% = 0.02 * W# +GOSUB CENTERX +TY% = 0.07 * H# +CM# = 1 +GOSUB DRAWTEXT +CM# = 0 +COLOR 1, 2 +T$ = "MEGADEMO 85" +SZ% = 0.009 * W# +GOSUB CENTERX +TY% = 0.32 * H# +GOSUB DRAWTEXT +COLOR 1, 4 +T$ = "CODE AND FX : TACHIKOMA" +SZ% = 0.004 * W# +GOSUB CENTERX +TY% = 0.72 * H# +GOSUB DRAWTEXT +COLOR 1, 14 +T$ = "DEDICATED TO GAMECUBE" +GOSUB CENTERX +TY% = 0.77 * H# +GOSUB DRAWTEXT +COLOR 1, 8 +T$ = "MEGAGREETZ TO STARFORT - KEEP THE FORGE LIT" +GOSUB CENTERX +TY% = 0.82 * H# +GOSUB DRAWTEXT +COLOR 1, 16 +T$ = "100 PCT BASIC - NO MACHINE CODE" +GOSUB CENTERX +TY% = 0.88 * H# +GOSUB DRAWTEXT +WCY% = 0.55 * H# +AMP% = 40.0 +PH% = 1.0 +GOSUB WAVE +IF HL# = 1 THEN GOTO BYE +T$ = "AKLABS" +SZ% = 0.02 * W# +GOSUB CENTERX +TY% = 0.07 * H# +DO WHILE QF# = 0 + RB# = RB# + 1 + CM# = 1 + GOSUB DRAWTEXT + CM# = 0 + PH% = PH% + 0.1 + GOSUB WAVE + IF TI# > MT# THEN GOSUB TUNE + GOSUB POLLKEY + SLEEP 0.1 +LOOP +GOTO BYE + +REM ===================================================================== +REM Shutdown: disarm the handler, retire the rectangles, mute the SID. +REM The picture stays -- that is what END is for. +REM ===================================================================== +LABEL BYE +COLLISION 2 +SOLID +IF SND# = 1 THEN VOL 0 +IF TXT# = 1 THEN CHAR 1, 0, 0, "AKLABS 85 - KEEP THE SCENE ALIVE " +END + +REM ===================================================================== +REM Subroutines. +REM ===================================================================== + +REM Chapter 17's generator, verbatim: there is no RND in this dialect. +REM Answers 0 to RMAX#-1 in RND#, from the middle bits of the seed. +LABEL RANDOM +SEED# = MOD(((SEED# * 1103515245) + 12345), 2147483648) +RND# = MOD((SEED# / 65536), RMAX#) +RETURN + +REM NS# stars in four brightnesses. Two LCG pulls a star, and the +REM brightness rides the low bits of the seed for free. +LABEL STARS +SI# = 0 +DO WHILE SI# < NS# + RMAX# = W# + GOSUB RANDOM + X# = RND# + RMAX# = H# + GOSUB RANDOM + Y# = RND# + B# = MOD(SEED#, 4) + COLOR 1, STC#(B#) + DRAW 1, X#, Y# + SI# = SI# + 1 +LOOP +RETURN + +REM One Lissajous pass: 3:2, phase-shifted 25 degrees per PN#. The +REM first point is plotted bare, the rest chain with TO. +LABEL SPIRO1 +PX% = CX% + (R1% * SIN(RAD(25 * PN#))) +PY% = CY% + R2% +DRAW 1, PX%, PY% +FOR T# = 8 TO 360 STEP 8 + X% = CX% + (R1% * SIN(RAD((3 * T#) + (25 * PN#)))) + Y% = CY% + (R2% * COS(RAD(2 * T#))) + DRAW 1, PX%, PY% TO X%, Y% + PX% = X% + PY% = Y% +NEXT T# +RETURN + +REM Centre T$ at scale SZ%: a glyph cell is five units wide. +LABEL CENTERX +LW% = SZ% * 5 +TW% = LW% * LEN(T$) +TX% = 0.5 * (W# - TW%) +RETURN + +REM Draw T$ at TX%, TY%, scale SZ%. CM# = 1 paints each letter from +REM the rainbow table, offset by RB# -- the redraw that stands in for +REM the palette cycle this machine does not have. +LABEL DRAWTEXT +TL# = LEN(T$) +TC# = 0 +DO WHILE TC# < TL# + C$ = MID(T$, TC#, 1) + GI# = INSTR(ALPHA$, C$) + IF CM# = 1 THEN COLOR 1, RBW#(MOD(TC# + RB#, 7)) + LW% = SZ% * 5 + GX2% = TX% + (LW% * TC#) + GY2% = TY% + IF GI# >= 0 THEN GOSUB DRAWGLYPH + TC# = TC# + 1 +LOOP +RETURN + +REM One glyph. A DO rather than a FOR: single-stroke glyphs exist and +REM FOR 1 TO 1 never runs its body (TODO.md section 6). +LABEL DRAWGLYPH +GN# = FNC#(GI#) +IF GN# = 0 THEN RETURN +GP# = FNI#(GI#) +GK# = 0 +DO WHILE GK# < GN# + P1# = FNS#(GP#) + P2# = FNS#(GP# + 1) + GP# = GP# + 2 + X1% = GX2% + (SZ% * (P1# / 10)) + Y1% = GY2% + (SZ% * MOD(P1#, 10)) + X2% = GX2% + (SZ% * (P2# / 10)) + Y2% = GY2% + (SZ% * MOD(P2#, 10)) + DRAW 1, X1%, Y1% TO X2%, Y2% + GK# = GK# + 1 +LOOP +RETURN + +REM One raster bar: a five-step ramp, two double-width lines per step +REM for a twenty-pixel bar -- or all black when EB# says erase. +LABEL DRAWBAR +BL# = 0 +DO WHILE BL# < 5 + CI# = BARC#(BR# + BL#) + IF EB# = 1 THEN CI# = 1 + COLOR 1, CI# + BLY% = BY% + (BL# * 4) + DRAW 1, 0, BLY% TO W# - 1, BLY% + BLY% = BLY% + 2 + DRAW 1, 0, BLY% TO W# - 1, BLY% + BL# = BL# + 1 +LOOP +RETURN + +REM ===================================================================== +REM The wireframe engine. One pooled model: cube vertices 0-7 and +REM octahedron vertices 8-13, edge list indexing into the pool. +REM VS#/VE# and ES#/EE# select the object, CX%/CY% place it, SF% +REM scales it, RA%/RB% are its two rotation angles. +REM ===================================================================== + +REM Rotate the selected vertices about X then Y and project them. +REM Every product keeps the float on the left -- the left operand +REM decides the arithmetic, and one integer would floor the trig. +LABEL WIRESPIN +CS1% = COS(RA%) +SN1% = SIN(RA%) +CS2% = COS(RB%) +SN2% = SIN(RB%) +V# = VS# +DO WHILE V# < VE# + RY% = (WVY%(V#) * CS1%) - (WVZ%(V#) * SN1%) + RZ% = (WVY%(V#) * SN1%) + (WVZ%(V#) * CS1%) + RX% = (WVX%(V#) * CS2%) + (RZ% * SN2%) + RZ% = (RZ% * CS2%) - (WVX%(V#) * SN2%) + PJ% = SF% / (3.2 + RZ%) + WPX#(V#) = CX% + (RX% * PJ%) + WPY#(V#) = CY% + (RY% * PJ%) + V# = V# + 1 +LOOP +RETURN + +REM Draw the selected edges from the projected points. The caller has +REM already bound COLOR 1, so a rainbow costs nothing extra. +LABEL WIREDRAW +E# = ES# +DO WHILE E# < EE# + V1# = WE1#(E#) + V2# = WE2#(E#) + DRAW 1, WPX#(V1#), WPY#(V1#) TO WPX#(V2#), WPY#(V2#) + E# = E# + 1 +LOOP +RETURN + +REM Draw the previous frame's edges in black. The first frame erases +REM a degenerate point at the origin, which costs one black pixel. +LABEL WIRERASE +COLOR 1, 1 +E# = ES# +DO WHILE E# < EE# + V1# = WE1#(E#) + V2# = WE2#(E#) + DRAW 1, WQX#(V1#), WQY#(V1#) TO WQX#(V2#), WQY#(V2#) + E# = E# + 1 +LOOP +RETURN + +REM Remember this frame's points so the next frame can erase them. +LABEL WIRESAVE +V# = VS# +DO WHILE V# < VE# + WQX#(V#) = WPX#(V#) + WQY#(V#) = WPY#(V#) + V# = V# + 1 +LOOP +RETURN + +LABEL CUBETICK +VS# = 0 +VE# = 8 +ES# = 0 +EE# = 12 +GOSUB WIRERASE +RA% = A1% +RB% = B1% +CX% = C1X% +GOSUB WIRESPIN +COLOR 1, RBW#(MOD(FC# / 8, 7)) +GOSUB WIREDRAW +GOSUB WIRESAVE +RETURN + +LABEL OCTATICK +VS# = 8 +VE# = 14 +ES# = 12 +EE# = 24 +GOSUB WIRERASE +RA% = A2% +RB% = B2% +CX% = C2X% +GOSUB WIRESPIN +COLOR 1, RBW#(MOD((FC# / 8) + 3, 7)) +GOSUB WIREDRAW +GOSUB WIRESAVE +RETURN + +REM One frame of both solids: advance the four angles, tick each. +LABEL SPINSTEP +FC# = FC# + 1 +A1% = A1% + 0.061 +B1% = B1% + 0.043 +A2% = A2% - 0.053 +B2% = B2% + 0.037 +GOSUB CUBETICK +GOSUB OCTATICK +RETURN + +REM A ball of light: concentric circles three pixels apart, solid at +REM WIDTH 2. DCX%/DCY% centre it, DR% sizes it, DCI# colours it -- +REM DCI# = 1 is the eraser. +LABEL DISC +COLOR 1, DCI# +R3% = DR% +DO WHILE R3% > 1 + CIRCLE 1, DCX%, DCY%, R3%, R3% + R3% = R3% - 3 +LOOP +RETURN + +REM The origin point: erase it where it was, redraw it pulsing on +REM SIN(BF#) and strobing through the bar ramp table -- the colours +REM of the next scene, because this is where its bars come from. +LABEL BALLFX +IF PB% <= 0 THEN GOTO BALLFX2 +DCI# = 1 +DR% = PB% +DCX% = C1X% +DCY% = CY% +GOSUB DISC +LABEL BALLFX2 +DR% = BBR% + ((0.45 * BBR%) * SIN(1.1 * BF#)) +DCI# = BARC#(MOD(BF#, 20)) +DCX% = C1X% +DCY% = CY% +GOSUB DISC +PB% = DR% +BF# = BF# + 1 +RETURN + +REM A raster bar being born: the same five-step ramp as DRAWBAR, but +REM spanning only C1X% plus and minus XR%. Each frame's lines cover +REM the last frame's, so the stretch needs no eraser. +LABEL PROTOBAR +BL# = 0 +DO WHILE BL# < 5 + COLOR 1, BARC#(BR# + BL#) + PX1% = C1X% - XR% + PX2% = C1X% + XR% + BLY% = BY% + (BL# * 4) + DRAW 1, PX1%, BLY% TO PX2%, BLY% + BLY% = BLY% + 2 + DRAW 1, PX1%, BLY% TO PX2%, BLY% + BL# = BL# + 1 +LOOP +RETURN + +REM The approach: one long sweep rising as the camera goes in. +LABEL ZOOMSND +IF SND# = 0 THEN RETURN +SOUND 3, 4000, 40, 1, 52000, 800 +RETURN + +REM Park the eight letter sprites on a sine. +LABEL WAVE +L# = 1 +DO WHILE L# < 9 + WX% = WX0% + (SPX% * (L# - 1)) + WY% = WCY% + (AMP% * SIN(PH% + (0.8 * L#))) + MOVSPR L#, WX%, WY% + L# = L# + 1 +LOOP +RETURN + +REM ===================================================================== +REM The supernova and the black hole. +REM ===================================================================== + +REM Pull every letter one step around and toward (CX0%, CY0%): read +REM its position back, rotate the offset by the angle in VC%/VS2%, +REM shrink it by VF%, put it there. Fourteen frames of 0.78 is a +REM wind-up; thirty-four of 0.86 is an event horizon. +LABEL VORTEX +L# = 1 +DO WHILE L# < 9 + SX% = RSPPOS(L#, 0) - CX0% + SY% = RSPPOS(L#, 1) - CY0% + NX% = ((SX% * VC%) - (SY% * VS2%)) * VF% + NY% = ((SX% * VS2%) + (SY% * VC%)) * VF% + MOVSPR L#, CX0% + NX%, CY0% + NY% + L# = L# + 1 +LOOP +RETURN + +REM One frame of fireball: erase the previous radius, draw the new +REM one. PR% carries the radius to erase between calls. +LABEL BOOMFX +IF PR% <= 0 THEN GOTO BOOMFX2 +BCF# = 1 +BR2% = PR% +GOSUB BOOMPASS +LABEL BOOMFX2 +BCF# = 0 +BR2% = ER% +GOSUB BOOMPASS +PR% = ER% +RETURN + +REM The fireball at radius BR2%: three rings, white heart to orange +REM rim, and twelve red spokes thrown past the outer ring. BCF# = 1 +REM draws the whole thing in black, which is the eraser. +LABEL BOOMPASS +R3% = BR2% +IF BCF# = 1 THEN COLOR 1, 1 ELSE COLOR 1, 2 +CIRCLE 1, CX0%, CY0%, R3%, R3% +R4% = 0.72 * R3% +IF BCF# = 1 THEN COLOR 1, 1 ELSE COLOR 1, 8 +CIRCLE 1, CX0%, CY0%, R4%, R4% +R4% = 0.45 * R3% +IF BCF# = 1 THEN COLOR 1, 1 ELSE COLOR 1, 9 +CIRCLE 1, CX0%, CY0%, R4%, R4% +IF BCF# = 1 THEN COLOR 1, 1 ELSE COLOR 1, 3 +SK# = 0 +DO WHILE SK# < 12 + SA% = RAD((SK# * 30) + 15) + X1B% = CX0% + ((0.55 * R3%) * SIN(SA%)) + Y1B% = CY0% - ((0.55 * R3%) * COS(SA%)) + X2B% = CX0% + ((1.14 * R3%) * SIN(SA%)) + Y2B% = CY0% - ((1.14 * R3%) * COS(SA%)) + DRAW 1, X1B%, Y1B% TO X2B%, Y2B% + SK# = SK# + 1 +LOOP +RETURN + +REM One frame of iris: ten black circles two pixels apart, WIDTH 2, +REM a closed band from IR% outward that the loop walks inward. +LABEL HOLEFX +COLOR 1, 1 +HC# = 0 +R3% = IR% +DO WHILE HC# < 10 + CIRCLE 1, CX0%, CY0%, R3%, R3% + R3% = R3% + 2 + HC# = HC# + 1 +LOOP +RETURN + +REM The accretion disc: three arcs at three radii, rotated to HA0#. +REM HAC# = 0 is the eraser, drawn at the angle before the turn. +LABEL HOLEARC +IF HAC# = 1 THEN COLOR 1, 5 ELSE COLOR 1, 1 +CIRCLE 1, CX0%, CY0%, 58, 58, HA0#, HA0# + 130 +IF HAC# = 1 THEN COLOR 1, 13 ELSE COLOR 1, 1 +CIRCLE 1, CX0%, CY0%, 40, 40, HA0# + 120, HA0# + 250 +IF HAC# = 1 THEN COLOR 1, 7 ELSE COLOR 1, 1 +CIRCLE 1, CX0%, CY0%, 25, 25, HA0# + 240, HA0# + 370 +RETURN + +REM The bang: a long rumble falling to the floor on voice 2 and a +REM crack sweeping down over it on voice 3. +LABEL BOOMSND +IF SND# = 0 THEN RETURN +SOUND 2, 9000, 45, 2, 150, 350 +SOUND 3, 52000, 30, 2, 900, 1400 +RETURN + +REM The suck: one long sweep rising off the top of the register. +LABEL HOLESND +IF SND# = 0 THEN RETURN +SOUND 3, 1500, 55, 1, 58000, 950 +RETURN + +REM COLLISION 2 lands here between lines. BUMP(2) says who, and +REM RCOLLISION(n, 7) says which axis -- reflecting a bearing is one +REM subtraction per axis, done in BOUNCE1. +LABEL BOUNCE +BM# = BUMP(2) +BS# = 1 +DO WHILE BS# < 9 + BB# = SHL(1, BS# - 1) + IF (BM# AND BB#) <> 0 THEN GOSUB BOUNCE1 + BS# = BS# + 1 +LOOP +RETURN + +LABEL BOUNCE1 +AX# = RCOLLISION(BS#, 7) +IF AX# = 1 THEN SBG#(BS#) = MOD(720 - SBG#(BS#), 360) +IF AX# = 2 THEN SBG#(BS#) = MOD(540 - SBG#(BS#), 360) +MOVSPR BS#, SBG#(BS#) # 5 +IF SND# = 1 THEN SOUND 2, 38000, 2 +RETURN + +REM A continuous mover can tunnel a thin wall between handler entries. +REM Anyone who escapes is rehomed to the centre with a fresh bearing. +LABEL SHEPHERD +L2# = 1 +DO WHILE L2# < 9 + SX% = RSPPOS(L2#, 0) + SY% = RSPPOS(L2#, 1) + OUT# = 0 + IF SX% < 2 THEN OUT# = 1 + IF SX% > W# - 40 THEN OUT# = 1 + IF SY% < 2 THEN OUT# = 1 + IF SY% > H# - 55 THEN OUT# = 1 + IF OUT# = 1 THEN GOSUB REHOME + L2# = L2# + 1 +LOOP +RETURN + +LABEL REHOME +MOVSPR L2#, 0.5 * W#, 0.5 * H# +RMAX# = 360 +GOSUB RANDOM +SBG#(L2#) = RND# +MOVSPR L2#, SBG#(L2#) # 5 +RETURN + +REM Hold for about a frame. The first cut spun on TI# -- the closest +REM thing this machine has to a raster wait -- and the spin starved +REM the music: the host releases at most ONE queued PLAY note per +REM frame, and a frame is 256 *executed* statements, so a busy-wait +REM makes frames slow enough to drop the release rate under the +REM tune's fifteen notes a second. The queue backed up and overflowed +REM two scenes later. SLEEP holds the program at no per-step cost, so +REM the frames stay fast and the music drains on time. TODO.md +REM section 4 carries the measurement. +LABEL VSYNC +IF HL# = 1 THEN RETURN +SLEEP 0.02 +RETURN + +LABEL POLLKEY +IF KEYS# = 0 THEN RETURN +GET K# +IF K# = 113 THEN QF# = 1 +IF K# = 27 THEN QF# = 1 +RETURN + +LABEL SWEEP +IF SND# = 0 THEN RETURN +SOUND 3, 62000, 16, 2, 5000, 700 +RETURN + +REM ENVELOPE's sixth argument is a waveform -- 0 triangle, 1 sawtooth, +REM 2 square, 3 noise -- which Chapter 7 does not mention. Preset 3 is +REM the sawtooth lead and preset 4 the noise drum kit. +LABEL SIDINIT +ENVELOPE 3, 0, 5, 9, 1, 1 +ENVELOPE 4, 0, 6, 3, 1, 3 +VOL 12 +TEMPO 15 +RETURN + +REM The PLAY queue is one queue: each note holds it for its length, +REM and the envelope applied when a note sounds is whatever the last +REM T parsed selected -- so naming three voices plays three tracks +REM end to end with one instrument, not a chord (TODO.md section 4). +REM So this is beeper music, the way single-channel tracker tunes +REM were written: sixteenths that take turns being the kick (the O1 +REM root), the pumping bass (O2/O3) and the rolling arpeggio (O4 up). +REM Batch A walks Am F C G, batch B turns Am F Dm E with the G sharp, +REM and every third batch is a bar of noise drums on preset 4. +REM +REM There is no way to ask whether the queue has drained, so each +REM batch stamps MT# with its own length plus a breath, and the scene +REM loops requeue when TI# passes it. A batch is 64 notes and the +REM queue holds 128, so a slightly early requeue appends safely. +LABEL TUNE +MT# = TI# + 300 +IF SND# = 0 THEN RETURN +TRAP QFULL +IF TB# = 0 THEN GOSUB TUNEA +IF TB# = 1 THEN GOSUB TUNEB +IF TB# = 2 THEN GOSUB TUNED +TRAP +TB# = TB# + 1 +IF TB# > 2 THEN TB# = 0 +RETURN + +LABEL TUNEA +PLAY "V1T3U9S O1AO2AO3AO2AO3AO4CEAO1AO2AO4CEAO5CO4AE O1FO2FO3FO2FO3FAO4CFO1FO2FO3AO4CFAO5CO4A O1CO2CO3CO2CO3CEGO4CO1CO2CO3EGO4CEGO5C O1GO2GO3GO2GO3GBO4DGO1GO2GO3BO4DGBO5DO4B" +MT# = TI# + 270 +RETURN + +LABEL TUNEB +PLAY "V1T3U9S O1AO2AO3AO2AO3AO4CEAO1AO2AO4CEAO5CEO4A O1FO2FO3FO2FO3FAO4CFO1FO2FO3AO4CFAO5CO4A O1DO2DO3DO2DO3DFAO4DO1DO2DO3FAO4DFAO5D O1EO2EO3EO2EO3E#GBO4EO1EO2EO3#GBO4E#GBO5E" +MT# = TI# + 270 +RETURN + +LABEL TUNED +PLAY "V1T4U9S O1ARO5ARO1AAO5ARO1ARO5ARO1AO5AAA" +MT# = TI# + 80 +RETURN + +REM Table loaders. READ walks every DATA line in file order, so these +REM run before LOADFONT and their DATA sits above the glyphs. +LABEL LOADTABLES +I# = 0 +DO WHILE I# < 7 + READ D# + RBW#(I#) = D# + I# = I# + 1 +LOOP +I# = 0 +DO WHILE I# < 4 + READ D# + STC#(I#) = D# + I# = I# + 1 +LOOP +I# = 0 +DO WHILE I# < 20 + READ D# + BARC#(I#) = D# + I# = I# + 1 +LOOP +RETURN + +REM The wireframe models: fourteen vertices, then twenty-four edges +REM as pairs of vertex indices. DM% because the octahedron's reach is +REM a float and READ types by the variable it lands in. +LABEL LOADWIRE +I# = 0 +DO WHILE I# < 14 + READ DM% + WVX%(I#) = DM% + READ DM% + WVY%(I#) = DM% + READ DM% + WVZ%(I#) = DM% + I# = I# + 1 +LOOP +I# = 0 +DO WHILE I# < 24 + READ D# + WE1#(I#) = D# + READ D# + WE2#(I#) = D# + I# = I# + 1 +LOOP +RETURN + +REM The stroke font, shared with examples/breakout: one glyph per DATA +REM line, stroke count then point pairs, a point coded X*10+Y on a +REM grid four wide and seven tall. INSTR into ALPHA$ finds the glyph. +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 + +LABEL READGLYPH +FOR K# = 1 TO N# * 2 + READ D# + FNS#(GX#) = D# + GX# = GX# + 1 +NEXT K# +RETURN + +REM ===================================================================== +REM Trap handlers for the hardware probes. Each marks its device gone +REM and lets the probe fall through to the next line. +REM ===================================================================== +LABEL NOGFX +GFX# = 0 +RESUME NEXT + +LABEL NOSID +SND# = 0 +RESUME NEXT + +LABEL NOGRID +TXT# = 0 +RESUME NEXT + +LABEL NOKEYS +KEYS# = 0 +RESUME NEXT + +REM A host slow enough to fall behind the tune finds the note queue +REM full. Dropping the rest of the batch is a skipped bar, not a +REM crash -- MT# is already stamped, so the music picks itself up. +LABEL QFULL +RESUME NEXT + +REM ===================================================================== +REM Data. The rainbow, the star brightnesses, four raster-bar ramps, +REM then the stroke font. +REM ===================================================================== +REM rainbow: red orange yellow green lt-green cyan lt-blue +DATA 3, 9, 8, 6, 14, 4, 15 +REM star brightnesses: dark grey, mid grey, light grey, white +DATA 12, 13, 16, 2 +REM bar ramps, five entries each: red, green, blue, yellow +DATA 3, 11, 2, 11, 3 +DATA 6, 14, 2, 14, 6 +DATA 7, 15, 2, 15, 7 +DATA 9, 8, 2, 8, 9 + +REM cube vertices 0-7, one per line, x y z +DATA -1, -1, -1 +DATA 1, -1, -1 +DATA 1, 1, -1 +DATA -1, 1, -1 +DATA -1, -1, 1 +DATA 1, -1, 1 +DATA 1, 1, 1 +DATA -1, 1, 1 +REM octahedron vertices 8-13; 1.4 so its reach matches the cube's +DATA 1.4, 0, 0 +DATA -1.4, 0, 0 +DATA 0, 1.4, 0 +DATA 0, -1.4, 0 +DATA 0, 0, 1.4 +DATA 0, 0, -1.4 +REM cube edges: bottom ring, top ring, four pillars +DATA 0, 1, 1, 2, 2, 3, 3, 0 +DATA 4, 5, 5, 6, 6, 7, 7, 4 +DATA 0, 4, 1, 5, 2, 6, 3, 7 +REM octahedron edges: every equator vertex to every pole +DATA 8, 10, 8, 11, 8, 12, 8, 13 +DATA 9, 10, 9, 11, 9, 12, 9, 13 +DATA 10, 12, 10, 13, 11, 12, 11, 13 + +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