# 21. Tutorial: GALAGA — the structures and the AI [Chapter 20](20-tutorial-galaga.md) built a C engine that boots the interpreter and hands one actor to BASIC. This chapter builds everything that crosses the boundary — the three shared structures — and then the script that thinks through them: a full wave that enters, forms up, breathes, dives, fires and dies, without another line of engine code. ![The wave assembling: bosses, butterflies and bees under BASIC control](images/galaga-wave.png) The finished script is [`examples/galaga/galaga.bas`](../examples/galaga/galaga.bas) — six `DEF` functions and an `END`, nothing else. Editing it and re-running the game is the whole development loop; the engine never rebuilds. ## What you will do - **[Step 1](#step-1-declare-the-enemy-once-in-c)** — declare the enemy record once, in C, and register it as a BASIC type - **[Step 2](#step-2-bind-the-engines-own-actor)** — bind the engine's own actor as the second type, which is the point of the whole exercise - **[Step 3](#step-3-share-the-frame-and-the-dice)** — share the frame state, and give the script randomness it cannot make itself - **[Step 4](#step-4-why-bindings-and-not-arguments)** — see why the structures are bindings rather than function arguments - **[Step 5](#step-5-the-shape-of-the-script)** — learn the three language rules that shape every enemy function - **[Step 6](#step-6-the-shared-maneuvers)** — write the shared maneuvers: glide home, dive, decide to fire - **[Step 7](#step-7-the-three-kinds)** — write the bee, the butterfly and the boss - **[Step 8](#step-8-the-formation-c-or-basic)** — decide who owns the formation, and lay it out - **[Step 9](#step-9-when-a-script-dies)** — decide what a script error does to the game, and make it do that - **[Step 10](#step-10-prove-it)** — prove the boundary with a test that links the real files --- ## Step 1: Declare the enemy once, in C **Goal: one struct that both languages read and write, with one source of truth.** An enemy is what the state machine needs to remember between frames, plus one inbox and one outbox: ```c wrap=galagatypes requires=akgl #define GALAGA_ENEMY_BEE 0 #define GALAGA_ENEMY_BUTTERFLY 1 #define GALAGA_ENEMY_BOSS 2 /* * galaga_Enemy.state bits. The script owns these transitions; the engine only * writes the word at spawn. * * 8 0 * 0 0 0 0 0 1 1 1 * | | `-- ENTERING: flying its entry path toward the formation slot * | `---- FORMATION: holding (and breathing around) homex/homey * `------ DIVING: attacking, off the grid until it leaves the screen */ #define GALAGA_ES_ENTERING (1 << 0) #define GALAGA_ES_FORMATION (1 << 1) #define GALAGA_ES_DIVING (1 << 2) typedef struct galaga_Enemy { int32_t kind; /* GALAGA_ENEMY_BEE / BUTTERFLY / BOSS */ int32_t state; /* GALAGA_ES_* bit flags */ float homex; /* formation slot, in map pixels */ float homey; float t; /* parametric clock for the current maneuver */ int32_t hp; int32_t fire; /* outbox: script sets 1, engine consumes */ float rnd; /* inbox: engine writes fresh 0..1 each call */ } galaga_Enemy; ``` The C struct *is* the BASIC type. `akbasic_host_register_type()` takes a table of field descriptors — the BASIC name with its suffix, the C representation, and where the member sits — and after that the language's own machinery works across the boundary with no second set of rules ([Chapter 16](16-structures.md)): ```c wrap=galagatypes requires=akgl typedef struct galaga_Enemy { int32_t kind; int32_t state; float homex; float homey; float t; int32_t hp; int32_t fire; float rnd; } galaga_Enemy; static const akbasic_HostField ENEMY_FIELDS[] = { /* struct member BASIC name C representation */ AKBASIC_HOST_FIELD( galaga_Enemy, kind, "KIND#", AKBASIC_HOSTFIELD_INT32 ), AKBASIC_HOST_FIELD( galaga_Enemy, state, "STATE#", AKBASIC_HOSTFIELD_INT32 ), AKBASIC_HOST_FIELD( galaga_Enemy, homex, "HOMEX%", AKBASIC_HOSTFIELD_FLOAT ), AKBASIC_HOST_FIELD( galaga_Enemy, homey, "HOMEY%", AKBASIC_HOSTFIELD_FLOAT ), AKBASIC_HOST_FIELD( galaga_Enemy, t, "T%", AKBASIC_HOSTFIELD_FLOAT ), AKBASIC_HOST_FIELD( galaga_Enemy, hp, "HP#", AKBASIC_HOSTFIELD_INT32 ), AKBASIC_HOST_FIELD( galaga_Enemy, fire, "FIRE#", AKBASIC_HOSTFIELD_INT32 ), AKBASIC_HOST_FIELD( galaga_Enemy, rnd, "RND%", AKBASIC_HOSTFIELD_FLOAT ) }; static const akbasic_HostType ENEMY_TYPE = { "ENEMY", sizeof(galaga_Enemy), ENEMY_FIELDS, 8 }; ``` Three decisions are load-bearing here: - **`AKBASIC_HOST_FIELD` takes the offset and the width from the member itself**, via `offsetof` — so the two sides cannot drift. Writing them out by hand is two chances to name the wrong member and no way to notice. - **The script never declares a `TYPE`.** A host type and a script `TYPE` share one namespace, and a script that tries to redeclare `ENEMY` is refused. The "structure definitions" half of the boundary lives here, once. - **The suffixes are the dialect's**: `#` is integer, `%` is float ([Chapter 3](03-the-language.md)). `HOMEX%` because a formation slot is a pixel coordinate the glide arithmetic must not truncate. The limits that shape the struct: a type may carry 16 fields and the runtime 16 types ([Chapter 16](16-structures.md)). `ENEMY` spends 8 fields; the game spends 3 types. ## Step 2: Bind the engine's own actor **Goal: the script writes the same bytes the renderer reads.** The enemy record is the game's own invention. The second type is not — it is libakgl's `akgl_Actor`, registered field-for-field over the engine's real struct: ```c wrap=galagatypes requires=akgl static const akbasic_HostField ACTOR_FIELDS[] = { AKBASIC_HOST_FIELD( akgl_Actor, x, "X%", AKBASIC_HOSTFIELD_FLOAT ), AKBASIC_HOST_FIELD( akgl_Actor, y, "Y%", AKBASIC_HOSTFIELD_FLOAT ), AKBASIC_HOST_FIELD( akgl_Actor, state, "STATE#", AKBASIC_HOSTFIELD_INT32 ), AKBASIC_HOST_FIELD( akgl_Actor, visible, "VISIBLE#", AKBASIC_HOSTFIELD_BOOL ) }; static const akbasic_HostType ACTOR_TYPE = { "ACTOR", sizeof(akgl_Actor), ACTOR_FIELDS, 4 }; ``` This is the demonstrative point of the whole exercise. When the script writes `ACTOR@.X%`, it writes `akgl_Actor.x` — the same memory the renderer reads on the same frame. There is no copy going in, no copy coming out, and no code between the script's decision and the engine's pixel. Null physics (Chapter 20, Step 1) is what makes that safe: nothing else is trying to move the actor. The per-frame call binds both names to *this* enemy before dispatching — one binding per name, pointed at forty enemies in turn, which is what `akbasic_host_rebind()` is for: ```c wrap=galagacalls requires=akgl CATCH(errctx, akbasic_host_rebind(&SCRIPT, "SELF@", enemy)); CATCH(errctx, akbasic_host_rebind(&SCRIPT, "ACTOR@", actor)); ``` ## Step 3: Share the frame, and the dice **Goal: everything a diving enemy needs to know about the world, in one record.** ```c wrap=galagatypes requires=akgl typedef struct galaga_Shared { float playerx; /* the player actor's position, this frame */ float playery; int32_t wave; float rnd; /* fresh 0..1 each frame; the issue #16 route */ } galaga_Shared; ``` `GAME@` is bound once at boot to this one global instance and never rebound; the engine refreshes it at the top of every frame. The boss reads `GAME@.PLAYERX%` to lead its dive; the fire decision reads it to know whether anything is worth shooting at. The `rnd` fields — one here per frame, one on each enemy per call — exist because the engine's PRNG is the script's **only** source of randomness: write `SELF@.RND% < DT% * 1.5` and an enemy's trigger finger is a dice roll. There is no `RND` verb in this dialect; issue #16 tracks adding one, and Chapter 17's breakout hand-rolls a linear congruential generator in BASIC as the other route. Here the engine fills the field, which also keeps a headless run the same game on every machine — the PRNG is the example's own, not libc's. ## Step 4: Why bindings, and not arguments **Goal: know why `SELF@` is a bound global rather than a parameter.** The language can pass structures to functions — by value with `E@ AS ENEMY`, by reference with `E@ AS PTR TO ENEMY` ([Chapter 16](16-structures.md)) — and a host can construct those argument values, so the obvious alternative interface is honest functions: ```basic norun DEF UPDATEBEE(E@ AS PTR TO ENEMY, A@ AS PTR TO ACTOR, G@ AS PTR TO GAME, DT%) ``` It was measured before this chapter chose. Pointer arguments work — writes through `E@->X%` land in the host struct, the type check refuses a wrong type, by-value copies exactly as documented. What rules them out is the pool math: | | bound globals | pointer arguments | |---|---|---| | value-pool slots per call | 0 | 1 per structure parameter, never returned | | calls before exhaustion | unbounded | 1,015 measured (2,048-slot pool, 2 pointer args) | | at 40 enemies per frame | unbounded | 25 frames | | per-call cost | 148 us | 251 us | A `@`-suffixed name always takes value-pool storage, and that pool never reclaims — a documented property of structures, because a pointer may outlive the scope that `DIM`med it. A *parameter* is a local that dies with the call, but it pays the storage price of a `DIM` that must survive one; the pool drains, and the wave stops thinking mid-flight. Issue #36 tracks it, with the reduction for whoever fixes it. Until then: **bind and rebind for per-frame host calls; pass structures only to functions called a bounded number of times.** ## Step 5: The shape of the script **Goal: the three rules every enemy function is written under.** `galaga.bas` is definitions and an `END` — no top-level code, no line numbers, no `LABEL`s. Three rules of the dialect shape every body in it. **Rule 1: the left operand decides integer or float arithmetic** ([Chapter 3](03-the-language.md)). This will bite every enemy script exactly once, so meet it now. The natural spelling of "move by speed times dt" moves nothing: ```basic norun ACTOR@.Y% = ACTOR@.Y% + 260 * DT% ``` `260` is an integer, it is on the left of `*`, so `DT%` — a float around 0.016 — is converted to integer **zero** before the multiply. Nothing fails; the enemy simply does not move. The working spelling puts the float first: ```basic norun ACTOR@.Y% = ACTOR@.Y% + SPD% * DT% SPD% = SELF@.T% * 150 + 260 ``` Every expression in the finished script is written float-first. When an enemy of yours will not move, this is the first thing to check. **Rule 2: only the last `RETURN` may start a line.** A multi-line `DEF` body runs until `RETURN` — and the *definition* is scanned the same way, ending at the first line that begins with one. An early return therefore always rides an `IF ... THEN RETURN 0` on one line, and exactly one line-leading `RETURN` ends each function. The stagger guard at the top of every update function is the idiom: ```basic norun DEF UPDATEBEE(DT%) SELF@.T% = SELF@.T% + DT% IF SELF@.T% < 0 THEN RETURN 0 ``` **Rule 3: the budgets are small and named.** Eight function slots exist (`AKBASIC_MAX_FUNCTIONS`), each a measured 36 KiB of the runtime's 2.40 MiB. This game defines six: three update functions, two shared maneuvers, one fire decision. Nesting draws from the twelve-slot environment pool exactly as `GOSUB` does; the deepest chain here is three (update → maneuver → nothing). If a design needs a ninth function, raising the limit is one `#define` and +36 KiB per slot — weighed, not assumed. ## Step 6: The shared maneuvers **Goal: three helpers that make the three kinds one page each.** Ease toward the formation slot, with a little entry swirl. Answers 1 once the slot is reached — the caller flips the state on that answer: ```basic DEF GLIDEHOME(DT%) DX% = SELF@.HOMEX% - ACTOR@.X% DY% = SELF@.HOMEY% - ACTOR@.Y% K% = DT% * 4.5 IF K% > 1 THEN K% = 1 ACTOR@.X% = ACTOR@.X% + DX% * K% + SIN(SELF@.T% * 6) * 90 * DT% ACTOR@.Y% = ACTOR@.Y% + DY% * K% IF ABS(DX%) < 3 AND ABS(DY%) < 3 THEN RETURN 1 RETURN 0 END ``` One frame of a dive: accelerate downward, weave, lean toward the player's column, and glide back in from the top after falling out the bottom. The weave and the lean are parameters, which is what makes three kinds out of one maneuver: ```basic DEF DIVESTEP(DT%, WEAVE%, LEAD%) SPD% = SELF@.T% * 150 + 260 ACTOR@.Y% = ACTOR@.Y% + SPD% * DT% ACTOR@.X% = ACTOR@.X% + SIN(SELF@.T% * 4) * WEAVE% * DT% DX% = GAME@.PLAYERX% - ACTOR@.X% IF DX% > 220 THEN DX% = 220 IF DX% < -220 THEN DX% = -220 ACTOR@.X% = ACTOR@.X% + DX% * LEAD% * DT% IF ACTOR@.Y% > 1040 THEN BEGIN ACTOR@.Y% = 0.0 - 90 SELF@.STATE# = 1 SELF@.T% = 0 BEND RETURN 0 END ``` Note the off-screen exit: state back to `1` (ENTERING), clock to zero, and the glide brings it home — a dive that misses rejoins the formation, which is the classic loop. `0.0 - 90` rather than `0 - 90` is Rule 1 again: the float goes first even to make a negative. The fire decision raises the flag when diving roughly above the player. The engine consumes `FIRE#` and does the spawning — the script only wishes, because spawning takes an actor from a bounded pool and pool exhaustion must be a C-side refusal with the house error context, not a script mystery: ```basic DEF DECIDEFIRE(DT%) DX% = GAME@.PLAYERX% - ACTOR@.X% IF ABS(DX%) > 140 THEN RETURN 0 IF ACTOR@.Y% > GAME@.PLAYERY% THEN RETURN 0 IF SELF@.RND% < DT% * 1.5 THEN SELF@.FIRE# = 1 RETURN 0 END ``` ## Step 7: The three kinds **Goal: bee, butterfly, boss — one state machine, three characters.** Every kind is the same three-state machine, dispatched by the bits of `SELF@.STATE#`. The bee is the reference implementation: ```basic DEF GLIDEHOME(DT%) ACTOR@.X% = SELF@.HOMEX% ACTOR@.Y% = SELF@.HOMEY% RETURN 1 DEF DIVESTEP(DT%, WEAVE%, LEAD%) RETURN 0 DEF DECIDEFIRE(DT%) RETURN 0 DEF UPDATEBEE(DT%) SELF@.T% = SELF@.T% + DT% IF SELF@.T% < 0 THEN RETURN 0 S# = SELF@.STATE# IF (S# AND 1) > 0 THEN BEGIN R# = GLIDEHOME(DT%) IF R# = 1 THEN SELF@.STATE# = 2 : SELF@.T% = 0 BEND IF (S# AND 2) > 0 THEN BEGIN ACTOR@.X% = SELF@.HOMEX% + SIN(SELF@.T% * 1.7) * 16 ACTOR@.Y% = SELF@.HOMEY% IF SELF@.RND% < DT% * 0.04 THEN SELF@.STATE# = 4 : SELF@.T% = 0 BEND IF (S# AND 4) > 0 THEN BEGIN R# = DIVESTEP(DT%, 130, 0.2) R# = DECIDEFIRE(DT%) BEND RETURN 0 END ``` (The three helpers above are stubs so this listing runs alone; the real ones are Step 6's. The listing in `galaga.bas` is this function verbatim.) The shape to notice: `S#` is read **once**, so a state flipped this frame does not also run its new state's block this frame — transitions are frame-atomic. Each block is one `IF ... BEGIN`/`BEND`, never nested. The formation block computes position *relative to home* every frame — `HOMEX% + SIN(...)` — so the grid's idle breathing belongs to the script even though C placed the grid. The butterfly is the bee with a wide lateral weave — `DIVESTEP(DT%, 260, 0.1)` — and a slightly itchier trigger. The boss differs three ways: two hit points (C fills `HP#` at spawn), a dive that leads the player — `DIVESTEP(DT%, 60, 0.9)` — and one line that crosses the boundary in the other direction: ```basic norun IF SELF@.HP# = 1 THEN ACTOR@.STATE# = ACTOR@.STATE# OR 8192 ``` 8192 is `AKGL_ACTOR_STATE_UNDEFINED_13`, one of the actor state bits libakgl reserves for the game. The boss's character file maps the state word `ALIVE` to the green sprite and `ALIVE`+bit-13 to the drained one — so when the script raises the bit, the engine's own character machinery swaps the sprite. BASIC decides *that* the boss looks hurt; C never hears about it. ## Step 8: The formation: C or BASIC? **Goal: decide who owns the grid, from the trade-offs rather than taste.** Both can lay out the formation. The choice is argued, not asserted: | | C lays out the grid | BASIC lays out the grid | |---|---|---| | actor pool safety | refusal at spawn, house error path | script can ask for more than 64 exist | | tuning without rebuild | no | yes | | call budget | zero calls | one call per spawn | | who knows the screen size | the engine owns it anyway | needs it exported through `GAME@` | **Decision: C owns the grid, the wave table and the spawn timing; BASIC owns everything an enemy does after it exists.** The slot arrives in `SELF@.HOMEX%`/`HOMEY%`, so the breathing stays the script's (Step 7), and the pool stays behind a C-side refusal. The wave is the aligned table house style already prescribes for tabular data — one row per formation row: ```c wrap=galagagame requires=akgl static const struct { int32_t kind; /* GALAGA_ENEMY_* */ int row; /* formation row */ int first; /* first column filled */ int count; /* columns filled */ int32_t hp; } WAVE_ROWS[] = { /* kind row first count hp */ { GALAGA_ENEMY_BOSS, 0, 3, 4, 2 }, { GALAGA_ENEMY_BUTTERFLY, 1, 1, 8, 1 }, { GALAGA_ENEMY_BUTTERFLY, 2, 1, 8, 1 }, { GALAGA_ENEMY_BEE, 3, 0, 10, 1 }, { GALAGA_ENEMY_BEE, 4, 0, 10, 1 } }; ``` Forty enemies: 4 bosses, 16 butterflies, 20 bees. The actor heap holds 64: ```text player 1 player shots 2 /* the classic two-on-screen rule */ enemies 40 /* 20 bees, 16 butterflies, 4 bosses */ enemy shots 8 explosions 8 /* short-lived actors, released on a timer */ --- 59 of 64 ``` The spawn walks the table, fills each `galaga_Enemy`, and staggers the entry clocks — `t = -0.08 * index`, so each enemy holds still until its own clock crosses zero and the wave pours in as a stream rather than a wall. The full loop is `examples/galaga/enemies.c`. ## Step 9: When a script dies **Goal: a script error costs one enemy's wits, never the frame.** A BASIC-level error in an enemy's function — a misspelled field, arithmetic on the wrong type — reports through the sink and stops the script. The engine's policy, implemented around the call in `script.c`: - **The enemy goes dumb**: state cleared to a formation hold it will never leave, outbox cleared. The other thirty-nine keep thinking. - **The runtime is revived**: a run's first error latches, and while it stands every later call answers a stale value after doing nothing. Revival is two calls — `akbasic_runtime_clear_error()`, then the same `akbasic_runtime_set_mode(RUN)` the boot needed (issue #8's mechanics). - **The first failure is logged, the rest are counted.** Sixty a second of the same message is how a log stops being read; the count lands in the closing readout as `script errors N`, where a headless run cannot miss it. The same detection runs at boot: every function in the dispatch table is called once against a zeroed scratch enemy, so a script that cannot run fails at startup with the function's name in the message — not on frame one of the first wave. ## Step 10: Prove it **Goal: a test that fails the moment the two sides disagree.** `examples/galaga/interop_test.c` links the real `script.c` and loads the real `galaga.bas` — not copies — and pins the four claims this chapter made: ```text ok: a formation bee's sway is written into akgl_Actor.x/y by the script ok: a diving bee above the player raises FIRE# for the engine to consume ok: a boss at one hit point raises actor state bit 13 from BASIC ok: 24000 calls survive the per-call akbasic_environment_zero() regime ``` That last claim is the per-frame contract from Chapter 20 Step 6 under a full game's load — forty enemies at sixty frames a second for ten seconds. CTest runs it as `example_galaga_interop` beside the headless game itself. And because the script is data, the proof extends to scripts nobody planned: run the game with `--script` pointing at a variant — enemies that never dive, enemies that always dive — and the engine neither knows nor cares. That swap-a-brain-without-rebuilding property is what the two chapters were about; the readout tells you how each brain did: ```text galaga: 3000 frames, screen 2, score 2350, alive 0, kills bee 20 bfly 15 boss 1, shots bee 1 bfly 1 boss 1, script errors 0 ``` --- Where to go from here: more waves are rows in the table; a new enemy kind is one table row, one character file and one `DEF`; a smarter boss is edits to a text file while the game is closed — or a different file handed to `--script`. The engine is done. That is the point.