Loading: one line of BASIC and the whole of memory
The tape holds a BASIC header, a 76-byte program and one headerless block of 41984 bytes. The program is a single line,
1 RANDOMIZE USR (PEEK 23635+256*PEEK 23636+53)
which is PROG plus 53: the machine code sitting after the line's own ENTER. That code loads the big block to $5C00 -- the system variables -- and everything above, to the top of memory. It replaces the running BASIC program with the game's own, 8K of it, and survives doing so because nothing returns to the old program: LD-BYTES is entered with LOADED pushed as its return address. LOADED checks the flags and DE the ROM left (a tape error or a short load resets the machine), switches to interrupt mode 2, and then starts the game's BASIC the way ENTER would: RESTART_BASIC types RUN into the edit line and jumps into the ROM's statement loop. The header's own auto-run line, 1, starts the loader; nothing would start the game's BASIC that the block brings in, so the game starts it itself.
BREAK starts it again
Interrupt mode 2 sends every interrupt through INTERRUPT_VECTORS to INTERRUPT. That passes the interrupt on to the ROM's own routine unless ERR_NR says an error has happened -- BREAK, most likely -- in which case it drops into the same RESTART_BASIC that started the game, and the BASIC program runs again from the top. There is no way to stop it into BASIC.
Half BASIC, half machine code
BASIC runs everything that is not the city: the title screen, "Girl or Boy (g/b)?", the story cards, the score card, "Have another go!", and the setting up of each level, which is a matter of POKEing the object records at PLAYER from DATA (lines 100-190) and the variables at VIEW_ORIGIN (lines 200-230). The ten places the person to be rescued waits are DATA too -- see the levels. The machine code is entered through four USRs: PLAY (32768) to play, GET_KEY (32912) for the girl-or-boy key, WAIT_KEY (32919) to wait for a key, and FINAL_SCRIPT (36594) for the ending. PLAY is called twice a level. With 2 in FRAMES_LEFT it draws two frames and returns, so the city is on the screen behind "READY WHEN YOU ARE"; the second call finds 0 there, counts it round to $FF, and runs until the game stores a small count again -- at the end of the fourth frame after time or energy runs out (CHECK_GAME_OVER), at the end of the same frame after a rescue (CHECK_RESCUED). Pressing 1 leaves at once. BASIC then reads what happened from the rescued person's record: 2 at +$0D (RESCUEE_STATE, BASIC's w) is a rescue.
Eight objects, one record
The player, the person to be rescued, the grenade and five ants are all the same 16-byte record at PLAYER, and all moved by the same code (MOVE_OBJECT).
OffsetWhat
+$00, +$01, +$02x, y and height
+$03first sprite frame: the boy $DC, the girl $6C, ants $F8
+$04facing, 0-3: y up, x up, y down, x down
+$05frames spent falling
+$06frames left stunned; $FF on an ant is paralysed for good
+$07flags: bit 0 may stand on the other person, 1 jump, 2 move, 3 never falls (never set), 4 steps up a one-block rise
+$08animation frame, or with bit 7 set a sprite frame outright
+$09explosion countdown, after which the object goes home
+$0A-+$0Chome: x, y and height
+$0D, +$0Ean ant's speed and its count; the rescued person's state and last distance to the player
+$0Fwhat just happened to it, for HANDLE_EVENTS: a step, a bad fall, a bite, a blast
The player starts every level by exploding: BASIC sets the explosion countdown to 1, and when it runs out, one frame later, MOVE_OR_RESPAWN puts the player at home -- the city gate.
The ants are part of the map
Nothing is solid but the map. So MOVE_ANT takes an ant out of the map before moving it and puts it back afterwards (TOGGLE_MAP_BIT, an XOR on its height's bit), and from then on everything else bumps into it exactly as it would into a wall. The city map in memory changes as the ants walk about. A bite is the other half of the same trick. MOVE_OBJECT starts every move by asking whether the object's own cell is occupied at its own height; the only way that can happen is for an ant to have walked into it. That is a bite (BITTEN): an event for HANDLE_EVENTS, a point of energy off, and the bitten object pushed up a block. Because it is an XOR, the map and the ants' records have to agree. Move an ant's record without its bit -- as an early version of the scenes this disassembly was checked with did -- and it leaves a phantom block where it was and, at the new place, puts a block in its own cell: it is promptly "bitten" by itself and pushed up a block.
How the view is drawn
DRAW_VIEW is a painter's algorithm done with tables, eleven steps a frame.
  1. GATHER_VIEW reads the map along diagonals from VIEW_ORIGIN into VIEW_CELLS: 21 rows of 32 cells, in the order the view needs. There is one routine per view, the same apart from signs.
  2. BUILD_PLANES turns that into PLANES, 512 places each saying which height of block is seen there, or $FF for none. A block one higher is drawn one row further up the screen, so height h simply reads the gathered cells h rows further on -- and the heights are done lowest first, so a higher block overwrites a lower one. That is the whole of the 3D.
  3. PROJECT_SPRITES projects each object into the same places: the half-row is v - u - 2h and the column (u + v) / 2, for the object's position (u, v) relative to the view, turned for it, and h its height.
  4. SORT_SPRITES sorts the objects by place, and SPRITE_DISTANCES turns the sorted places into gaps from one to the next.
  5. DRAW_SCENE paints: every place holding $01, then every $02, and so on, each found with CPIR. The objects are threaded in with no extra test at all: BC, CPIR's count, is the gap to the next object, so when CPIR runs out of count before it finds a block, the next object is due. An object's place counts in which pass it belongs to, so a sprite is painted after every block below its height and before any above it.
  6. COPY_TO_SCREEN copies rows 12-127 of the buffer to the screen.
The block picture itself is code: DRAW_BLOCK and DRAW_BLOCK_TURNED (one for each pair of views, since turning a quarter mirrors the shading) are unrolled runs of immediate stores, each step painting one row of the outline over what is behind it and the row eight below outright. The places tile like bricks, 16 pixels wide, each half-row a byte across and four pixels down from the last. The pass for $40, above the six heights of block, has no blocks in it: it is there for anything standing on top of the tallest walls.
Falling, jumping and standing on each other
MOVE_OBJECT moves everything the same way. Nothing to stand on: fall -- but not on the first frame, when the object can still walk, which is what lets you step off a ledge (FALL). Something in your own cell: bitten. Otherwise jump if asked, and walk forward if moving and the way is clear; a blocked walker with bit 4 set steps up a one-block rise by itself, which is how the rescued person follows you up steps. A landing (LANDED) after five frames' fall or more is a bad fall -- "NASTY FALL !" -- and stuns for eight frames per frame fallen; shorter ones stun briefly. Landing at height 1 means landing on something at ground level, which may be an ant's back: PARALYSE_ANT paralyses it for good ("PARALYSED AN ANT !"). The player and the rescued person may stand on each other (bit 0 of the flags, TEST_BELOW). When they share a cell a block apart, SHARE_CELL clears the player's fall count and both stuns, so dropping onto each other is never a landing at all. And a fall that ends while an object is still stunned is never landed either: the stunned branch clears the fall count without a word. The rescued person starts every level stunned for four frames, which is enough to swallow a short drop.
The ants
ANT_TURN gives each ant a turn every frame but one in +$0D. The first ant's +$0D is 20 (line 140) -- nearly full speed. The other four take BASIC's sp, which is 2 (half speed) for the first four levels and one more for every four rescues after that. MOVE_ANT steps the ant the way it faces and compares its distance from the player before and after. Closer: it walks on, and within three cells turns to face the player. Not closer: it stops and turns left or right at random (RANDOM). There is no path-finding; the city's walls do the rest. A grenade (THROW_GRENADE: S, D, F or G throw for 2, 4, 8 or 16 frames) explodes where it is when the time runs out. BLAST_ANT blows up any ant at the same height within four cells ("GOOD SHOT!") and stuns one within seven. The grenade is checked against the player's and the rescued person's cells on every frame of its flight, not just the last: anything it meets on the way is blown up.
Finding someone and leading them out
While the person waits, MOVE_RESCUEE colours the scanner (SCANNER): green if the player is closer than last frame, red if not. Within three cells at the same height they are found -- "MY HERO!" -- and from then on FOLLOW_PLAYER walks them after the player, stopping beside them or when left more than five cells behind. The rescue is CHECK_RESCUED: once both are outside the walls, with a coordinate below $80, it is done.
Messages and sounds are one thing
Every message and every sound is a script in SCRIPTS, run by RUN_SCRIPT: a stream to print to, then printable characters and control codes, bytes of $80 and above that play a note or a burst of noise, $7D to fill the play area with a colour and $7E to print a number from the variables. So "MY HERO!" is a tune with letters between the notes. See the scripts page for all seventeen.
Leftovers
How this was worked out
The code/data split is not a guess. The build plays the game in SkoolKit's Z80 simulator and records every instruction executed: random play, then a rescue, running out of time, the ending, and a dozen staged scenes -- an ant beside the player, a grenade thrown at an ant, a drop onto an ant's back, onto a wall, onto the rescued person's head -- each placed on the simulated machine and then left to the game's own code. Between them they run every instruction in the game but three. Branches out of executed code are followed as well, which finds nothing the scenes have not. The assembly is fed back through sjasmplus and compared with the tape: all 41984 bytes, from the system variables up, match byte for byte, and the build fails if they ever stop matching.