1984 · Mastertronic

Chiller: the source

This minisite was curated by unorig, air.

Every routine, table and variable the game uses, with the names and descriptions given to them. Click an operand to follow it; the counter beside a label lists what refers to it. The reference below the fold is rendered from the game's facts file.

Technical reference: memory map, timing, tables, cheats

Current truth for this game. The workflow lives in kit/skills/; how this understanding developed lives in agent-history.md. Every fact names the routine or table it comes from. Unless marked live, a fact comes from reading the code in the snapshot named in orientation.md.

Build

No build identifier was found in the image: the string sweep (work/sweep_strings.py) turned up no version or date text. The loader's (ANTISOFT) mark is a third party's (orientation.md). This copy is the first, withdrawn Thriller release. Its music player and all 4,238 bytes of tune data at $61F2-$727F match Music/v1music.asm in the steward's https://github.com/unorig/Chiller (branch Latest) byte for byte, and about 1,650 differ from v2music.asm; the steward confirmed on 29 September 2026 that V1 is the withdrawn release. The re-release changes the in-play tune and the gate-off compare at $60B6 (3 to 1); its data is in reference/music-v2.json for the page's player.

Memory layout

Where the CPU runs during play: non-stopping exec checkpoints over ranges, 3 s from work/play-idle.vsf with no input (work/orient_cpu.py, live):

RangeInstructions in 3 s
$0800-$2FFF69,707
$3000-$4FFF0
$5000-$5FFF158,250
$6000-$6FFF1,804
$7000-$7FFF36,859
$8000-$BFFF0
$C000-$CFFF379,203
$E000-$FFFF10,521 (the KERNAL's IRQ path, $EA31 and on)

This corrects orientation.md, which placed the engine's live code at $A000-$BFFF: nothing ran there in 3 s of play.

ThingWhere
Screen$0400 ($D018 = $1D, VIC bank 0)
Character set$3000-$37FF ($D018)
Level records, one per screen, $80 bytes each ($100 between 4 and 5)$7000-$757F; card pointer at +$74
Level cards$4100-$44E8
Music player and tune$60A0-$61F7, tune $61F8-$6A15
IRQ vector $0314music_irq $60F5, set by music_start $60CA

Twin copies

work/sweep_twins.py compares every non-blank 256-byte page with every other; work/sweep_shadow.py does the same for 32-byte windows above $E000.

BlockTwinBytes that differWhich the code reads
$3000-$33FF, the first 128 glyphs$8000-$83FF1Both: $8000 is the forest's set, which setup_screen copies to $3000 (records 0 and 9)
$0500-$06FF, screen rows 7-19$8500-$86FF2$0400. $8450 is the forest's play area as loaded, copied in by record 0; $8400 is the HUD template (hud_template)
$0900-$09FF$CE00-$CEFF0The loader's copier put it there (orientation.md)
$C001-$CDE0, in pieces$F140-$FEDF, at distances $30FF-$313Fnot measured$C000-$CFFF. The only bank switch, copy_under_kernal $72A4 ($01 = $34), is given only the way-back records' descriptors, which read $E028-$EFEF, so nothing reads the copy (work/under_io.py); excluded in game.json
$B600-$BEFFitself, shifted by $100-$8001-11Repeating fill ($FF with $00 every few bytes), not data

Timing

The music is driven from the system IRQ: music_irq counts zp $02 down each interrupt and plays the next command when it reaches the tempo in $02AA. The tune sets the tempo to 6 ($61FE). The IRQ is the KERNAL's CIA timer, not a raster interrupt: music_irq ran 1,799 times in 30 s (live, work/verify7.py), 60 a second on this PAL machine, and it exits through $EA31, which runs SCNKEY ($EA87, 61 hits in 1 s, work/verify5.py).

Play itself is not timed. main_loop $CA00 runs flat out, and the game paces things by counting passes: player_input every 32nd pass ($CF03), joy_move every 8th, the crosses' flash every 40th call (colour_cycle), a jump step every jump_speeds[n] passes. The only raster waits are wait_line16 $5BB0 (before swapping sprites and on the poison flicker) and scroll_wait $C46B (in the scroller nothing uses). So the game's speed depends on how much each pass does, and slows when many enemies are on screen.

Controls

The stick in control port 2 ($DC00) is the only CIA1 port read, and the game never writes to CIA1. The keys come from the KERNAL's own scan, still running in the IRQ: read_controls $C84D compares the key code in $C5 with the level's key table $4512-$4515 and reads SHIFT from the KERNAL's shift flag $028D.

ControlCodeResult
Stick left, rightread_stick $C9C4, bits 2 and 3 → $C1EDwalks, live
Stick up, SHIFTstick bit 0 or $028D → $C1EE → fire_pressed $C19D → start_jump $5418jumps, live for the stick (Y 224 → 205 → 220)
Z, C$C5 = $0C, $14 against $4514, $4515walk left and right, live (X 128 → 105, 128 → 148)
Fire, ?the stick's fire bit, or $C5 = $37 (the / key; ? is its shifted face) → switch_request $58B3 → switch_allowed $7280swaps the boy and the girl, only from level byte 10 on, live
SPACE$C5 = $3C, in the table twice as upnothing: up is not a direction on any screen ($4500 = 0)
RUN/STOPthe KERNALend_game $2CB4

Up on the stick jumps, and he climbs by jumping from ledge to ledge. On all ten screens up and down are switched off as directions ($4500-$4503 = 0, 0, 1, 1, read from the snapshots taken on arrival, work/settings10.py), so the only way up is the jump; no ladder was tried live. He goes down by falling or by sinking slowly through tiles $62-$65 (tile_touch_b). The same run shows gravity on ($45FF = 1), the scroll off ($4555 = $FF) and no fall limit ($457A = $FF) everywhere.

The fire button does not jump. fire_pressed jumps where the level has gravity (every screen) and would bring in the second character where it has not (switch_check $C7D4), which no screen uses.

The live key tests needed the host-key path. vice_keyboard_matrix held by row and column left $C5 at $40 for every key on this build; vice_keyboard_key_press reached it (work/verify4.py, work/verify6.py). It accepts no name for SHIFT, so SHIFT is traced, not tested.

Graphics

Every screen is multicolour text over one character set, with eight sprites. Nothing scrolls and nothing is drawn as a bitmap.

ThingWhereNotes
Play character set$3000-$37FF ($D018 = $1C)Glyphs $00-$7F ($3000-$33FE) are copied in per screen from the level record's +6 descriptor by setup_screen $5E19; the text glyphs $80-$BF stay; $3600 up is sprite data, so glyphs $C0-$FF are never shown. Glyph classes, by code: $00-$29 open, $2A-$4C solid, $4D-$53 a crumbling ledge in seven stages, $54-$57 pick-ups, $58-$7F floors, $80-$BF the text
Per-screen sets, play areas, colour maps$8000-$B1FF, $A00 per outward screenforest $8000, cinema $8A00, ghetto $9400, graveyard $9E00, house $A800: the 1 KB set, then at +$450 the play area (screen rows 2-24, 928 bytes, copied to $0450), then at +$800 the colour map, two cells per byte (unpack_colours $5D20). The way-back records reuse the outward set and colour map and take the play area from the store
Card and title font$B200-$B5FEcopied to $3000 by show_level_card $5BC7; holds the CHILLER logo pieces that draw_logo $5C00 lays out five glyphs deep
Boy's sprites$3600, pointers $D8-$E7walking left $D8, right $DC, jumping $E0/$E1, standing $E2/$E6
Girl's sprites$3A00, pointers $E8-$F7the same order; player_swap $576B patches the frame numbers
Enemy sprites$2000-$29FF ($80-$A7), $0B00-$0FFE ($2C-$3F)four frames an animation, named per screen in the level record (+$1D, +$22); the low blocks are used by records 1, 2 and 5-9 (work/sprites_used.py)
Thrown enemy, dying frames$3E00, $F8-$FCextra_enemy on sprite 7; enemy_dying steps $F9-$FC
Stored screens for the way back$7800, $E000, $E400, $E800, $EC00screen_store $7FE8; see Mechanics

The ten screens, reached live by playing through with crosses poked beside the boy (work/playthrough.py), are in reference/screen-00-forest.png to reference/screen-09-forest-back.png.

The border is the player indicator on every screen, not only on the way back: tick_border $75B0 puts it back each pass to 6 (blue) for the boy or 10 (light red) for the girl, and flash_border $72BA toggles its bit 3 while poison drains the bar.

Hardware register census

work/sweep_regs.py over $0800-$CFFF: every absolute access to $D000-$DFFF (work/sweep-regs.txt, 97 registers). It counts byte patterns, so a hit in data is a false one: the ones at odd addresses such as $D053, $D112, $D74A and $DD99 all fall in graphics or tables and are left out here.

RegistersWhat the game does with themWhere
$D000-$D00F, $D010Sprite positions. 0 is the character under control, 1 the other one (swap_sprites $58E6), 2-6 the five enemy slots, 7 the thrown enemy (extra_enemy)many; $C3E0-$C3F2 is seven DECs of sprites 1-7's Y in a row
$D015Sprite enable, 45 accesses: sprites switched on and off with the screens$0907, $2BF2, $2DA2, …
$D016Multicolour text on (ORA #$10); 40 columns on (ORA #$08) and off (AND #$F7)multicolour at show_card $5C62 and $5D80; $2BF5 sets and $C026 clears bit 3
$D018$15 (the ROM font, screen $0400) before printing through the KERNAL, $1C (font at $3000) after$15 at $2CD1 and $55C4; $1C at $2EFB, show_card $5C6C and $C6B1
$D01B, $D01C, $D01DSprite priority, multicolour, X expansion$2E97, $2D1C, $2D22, …
$D01ESprite-sprite collision: sprite_touch $CE87 takes energy when an enemy touches sprite 0 or 1; restart_screen reads it to clear it; $0987 is in the loader's area$0987, $C6DA, $CE87
$D01FSprite-background collision, read once$2D60
$D020, $D021Border and background. The manual says the border shows who is being controlled on the way back; these writes are where to look$2CC7, $515D, $57B7, $590E, $5A4B; game_over_wait $7720 reads the border
$D022-$D024Multicolour background colours, set per screen$5BD5, $5D8A, $758E, $7623, …
$D025-$D02ESprite colours$2B90, $2D10, $5554, …
$D011, $D012Read only, never written. Two copies of one wait loop spin until the raster is line 16 ($D012 = $10 and $D011 bit 7 clear); $C46B spins until line $4B$5BB0, $72C7, $C46B
$D400-$D40DVoices 1 and 2: the music (music_*, mcmd_*)$60BF-$61D6
$D40E-$D414Voice 3: the sound effects, all outside the music player$2BE3, $2F05, $5485-$552C
$D415-$D418Filter and volume, set once: $D415 = $05, $D416 = $45 (an 11-bit cutoff of $22D), $D417 = $F1 (resonance 15, voice 1 through the filter), $D418 = $3F (low-pass and band-pass, volume 15)music_init_filter $60A0
$DC00The joystick$2D97, $58BD, $C1C0, $C8C5, $C8D3, $C8E1, $C9C4, $C9DB
$D800-Colour RAMmany

Never touched from the game's code: $D013, $D017, $D019, $D01A (no raster interrupt), the CIA timers and interrupt control ($DC04- $DC0F, $DD04-$DD0F), $DD00 (the VIC bank stays at 0), and SID voice 3's pulse width and oscillator read-back ($D410, $D411, $D41B, $D41C).

Mechanics

The journey. level_records $7290 lists ten records by level byte (0, 2 … $12): forest, cinema, ghetto, graveyard, haunted house going out ($7000-$7200), then the house, graveyard, ghetto, cinema and forest coming back ($7300-$7500). A screen ends when its crosses are all taken: crosses_left $7F00 counts $5A15 down from the record's +$72 (5 going out, 10 coming back, live), then next_screen $7680 shows the next card and builds the next screen. After level byte $12 it passes $FE, which ends the game in a win. There is no car: the last screen is the forest.

The way back reuses the screens as they were left. Going out, next_screen copies the finished screen (rows 1-24) into a store before moving on (screen_store $7FE8: $7800, $E000, $E400, $E800, $EC00), and the way-back records read their play areas from there, the four under the KERNAL through copy_under_kernal $72A4. So a mushroom eaten going out is gone coming back. Live: played through, the way-back screens show the outward screens with the poked crosses taken (reference/screen-05-house-back.png onwards); started directly on a way-back screen by poking start_game's operand $C661, the unwritten store shows as garbage.

Two characters, two colours of cross. Each record's +$5E-+$71 holds ten cross positions, five blue and five red (place_crosses $5FC0; colour_cycle $7F80 flashes them by toggling the multicolour bit every 40th call). cross_touch $7F50 reads the colour: the boy ($5A08 = 0) takes only blue, the girl only red. Live: blue crosses poked beside the boy were taken and counted, red ones were left. Going out only the boy plays and each screen needs five; coming back both play and all ten are needed.

Every cross can be taken, and the game has no ending. The three lowest crosses, all blue and all on the way back, sit in the play area's bottom two rows: graveyard-back (record $7380) at screen address $079A, ghetto-back ($7400) at $07C5, cinema-back ($7480) at $07CC. Live (29 September 2026, the game's own code in kit/c64/machine.js, the energy bar kept full): from each level's start the boy takes his cross by walking left along the ground (port/jump.js to reach the level). His own Y stops at $E3, row 22 (see "The boy's own position is capped at row 22", below), but try_move looks for a tile in the cell probe_offset picks, two rows down or one row down and to the side, so from row 22 it reads rows 23 and 24. When a level's last cross is taken, crosses_left $7F00 passes the level byte at +$73 to next_screen $7680; after the last level ($12) it passes $FE, and the game loads the forest going out, with its five crosses and the score kept. Live: finishing the forest on the way back with crosses_left on the machine loads record $7000 with five crosses needed. There is no ending. $7F06-$7F08 are three NOPs, the length of a JSR: something may have been removed there (inferred; the image does not say what).

Switching. switch_request $58B3 takes fire or ? (key code $37); switch_allowed $7280 refuses before level byte 10. player_swap $576B swaps sprites 0 and 1, so the character under control is always sprite 0, patches the frame numbers, and sets the border. Live: / in the forest changed nothing; on the way back fire and / each gave the girl and the light red border ($5A08 0 → 1, border 6 → 10). The character left standing loses energy when an enemy touches it (girl_touch $CD9D).

Jumping. start_jump $5418 lifts the boy for up to 24 steps, each waiting jump_speeds $544A passes (5 rising to 45): a slowing rise, then the same table backwards for a quickening fall. The direction held at take-off is kept; the controls are switched off in the air by writing RTS over the first byte of read_controls and frame_step (jump_lockout $5880). A fall is counted, but the compare with the limit at $457A is followed only by NOPs, so no fall hurts (land $53EF).

The ground. try_move $2A04 probes the cell the boy would enter, turning his sprite position into a screen cell: row (Y-$2C)>>3, column (X-$0C)>>3 with the VIC's X MSB folded in, then probe_offset $5145 picks the cell to look at (his own row for up, two rows down, the next row over for left or right). Tiles $2A-$4C are solid. Tiles $4D-$53 are a crumbling ledge: every 8th step on one advances it a stage, and $53 becomes a space (tile_touch_a $5A9A). Tiles $58-$61 and $70-$7F (tile_touch_b $5AC7, $5B89, $7673) are solid only when the probe is straight down, so they can be walked into from the side but not fallen onto from above; $62-$65 let him sink the same way, one try in three ($5A14); $66-$6F are solid from every direction, with no test of which probe it was.

The boy's own position is capped at row 22. move_sprite $C900 clamps sprite 0's Y at $E3 unconditionally, before any tile is checked ($C930, cmp #$E3), and that is row 22 by try_move's own formula. Live (work/verify_reach4.py): falling through a column cleared to open space, with nothing to land on, the boy's Y still stops at $E3 after 128 frames. The play area is drawn 23 rows deep (screen rows 2-24), and his own sprite position is never computed as row 23 or

  1. The tiles there are still read, through probe_offset: see "Every

cross can be taken", above.

Energy. One bar of 33 cells, 8 steps each, from $042E (find_bar_end $5998), and no lives: new_life and lose_life exist but only unused code reaches them, so an empty bar ends the game (out_of_energy $5A70, screen_done $5D98).

WhatEffectWhere
Walkinga step off per about 765 passes with a direction heldwalk_drain $5A20
Jumpinga step off each time $5A09 runs out in the airdrain_step $59E6
Standing stillnothingdrain_step
An enemy touching the one under controltwo steps of poisonsprite_touch $CE87, poison_add $5B44
A toadstool, tile $5524 steps of poison, each with a border flashtile_touch_b $5AC7, poison_tick $5B2C, flash_border $72BA
A mushroom, tile $5423 steps back, one every 16 calls; on a full bar 10 points each insteadenergy_add_slow $5B18, energy_tick $5B00, energy_up $59C3
A bonus, tile $56100 pointstile_touch_b

Live: a mushroom poked under the boy grew the bar from 119 to 142 eighths in six seconds, against 119 → 119 in the control run (work/verify2.py); the toadstool changed the bar (work/verify.py). Standing still with no input the bar held at 119 for 16 s, then lost 69 steps at once, with 69 hits each on energy_down and poison_add: an enemy reached him and stayed on him (work/verify7.py). The drain with no input seen at Bronze is that, not a drain of its own.

As the bar goes the boy slows (bar_speed $5961): once its seventh cell is no longer full, input is read every $80 passes instead of every $20. The manual's running is not in the code; there is one walking speed.

Enemies. Five slots, each with a path script in the record (+$45-+$5D), animation frames, lives ($45E7: 200, 200, 21, 1, 200) and a respawn chance out of 128 ($45E2: 16, 16, 16, 13, 13; respawn_timer $2FB2). A sixth on sprite 7 is thrown now and then from a random slot (extra_enemy $CCF5). When every slot's lives are used up, level_up $2C19 counts the LEVEL counter on, shows "LEVEL nnn" over the play area, and speed_up $5681 halves each slot's move and frame delays, down to a floor read from $34E8.

Enemy paths, decoded. Each slot's +$3B-+$3F (low bytes) and +$40-+$44 (high bytes) point into path_scripts $4A00-$4A8F (setup_screen, into $575E/enemy_script_hi $CF24); path_step $CC12 steps one enemy a pixel with move_sprite every enemy_move_timer passes and, every 8th step, reads the next byte of its script: 0 up, 1 down, 2 left, 3 right, $FF loops back to the script's start. Nine scripts serve the ten screens' fifty slots (some repeat between screens): three walk down then back up ($4A00, $4A26, $4A46), four hold one direction for ever ($4A1E right, $4A20 up, $4A22 left, $4A24 down), and three are long runs down with a single stray $50 partway through ($4A6B, $4A70, $4A73) that move_sprite ($C900) does not recognise as a direction and so does nothing on that one step — not a fifth direction, just a byte that fails every cmp in the dispatch and falls through to rts. When $453D is set for a slot (no screen uses it), path_step picks a random one of the script's first 64 bytes instead of stepping through it in order. At an edge, a slot leaves the screen unless its $4538 byte (per slot, part of the level settings) allows it to stay (enemy_gone).

Score. Six digits on screen ($0406-$040B), stepped a point at a time (score_inc $CE11, add_score $CE42). Points come only from bonus tiles and from mushrooms eaten on a full bar. The high score is compared digit by digit when a game ends ($7700).

Data tables

TableWhereLayout
Level records$7000-$757F, $80 each (none at $7280)+0 play-area descriptor, +6 set descriptor, +$0C colour map, +$0E/+$0F multicolours, +$10-+$17 start positions, +$18-+$44 enemy tables, +$45-+$5D enemy paths, +$5E-+$71 cross addresses, +$72 crosses needed, +$73 level byte, +$74 card
Record indexlevel_records $7290ten addresses by level byte
Level settings$4500-$45FFdirections $4500-$4503, keys $4512-$4515, enemy lives $45E7, respawn chances $45E2, gravity $45FF and more; the flags checked are the same on all ten screens
Screen storescreen_store $7FE8five destinations by level byte, then 0
Jump timingjump_speeds $544A25 bytes, 5 … 45
Row offsetsrow_offsets $510025 words, 0, 40 … 960
Copy descriptorssix bytes: source, end (exclusive), destinationread by copy_block $2A80
Random numbers$C000-$C0FFrandom $C9F1 takes the next byte and EORs it with the jiffy clock's low byte $A2; the bytes are code (unused_screen_tools) read as data
Frequenciesfreq_lo $C300, freq_hi $C34056 notes, read only by the silenced players

Text

The game has its own character set but keeps the system's glyph order, so nothing needs a private alphabet table. Read from work/play-idle.vsf (work/text_vic.py, work/sweep_strings.py):

TextWhereEncodingPrinted by
Ten level cards, forest to haunted house and back$4100-$44E8PETSCII, row/column first, $01 endprint_card via show_card $5C4F, from $5BE4; each level's record at $7000 + $80·n holds its card's address at +$74
"THE FOREST", the first level's heading$5D0DPETSCII cardshow_forest_card $5E00
"MASTERTRONIC'S"$5D00screen codes + $80show_card $5C71, to $04D6
The title card: welcome, the goal and the controls$7C00PETSCII card$7760, after a game ends (game_over_wait $7720)
"PROGRAMMED BY DAVID AND RICHARD DARLING."$77C0screen codes + $80show_programmed_by $72ED, to screen row 0
HUD, "SCORE … MAGIC CROSSES … HI" and "ENERGY"screen $0400; copies at $8400, and with SILVER CROSSES at $8E00, $9800, $A200, $AC00screen codes + $80hud_template $8400, copied by start_game through hud_descriptor $56C0; the SILVER CROSSES copies are never copied (see Unused)
"LEVEL 001", "GAME OVER"$CFA1, $CFAA; a copy at $0AA1 nothing readsscreen codes + $80level_banner $2C44, show_game_over $CFC0
"PRESS CTRL FOR MENU"$574Ascreen codes + $80show_press_ctrl $5736, reached after GAME OVER, but it only loads and never stores, so the text never appears (see Bugs)
"ANTISOFT", repeated$0806, $0836-$08CFPETSCIIthe loader (orientation.md)

The ten cards, in the order of the records at $7000-$7574:

nRecordCardHeadingVerse
0$7000$4100THE FORESTMOVE THE BOY AROUND THE FOREST / COLLECTING THE BLUE MAGIC CROSSES.
1$7080$4178THE CINEMATHERE'S A MESSAGE SCRAWLED IN BLOOD...
2$7100$41B8THE GHETTOSAVE YOUR ENERGY, YOU'LL NEED IT.
3$7180$4208THE GRAVEYARDTHE MIDNIGHT HOUR IS CLOSE AT HAND, / YOUR DEATH AWAITS IN THIS EVIL LAND.
4$7200$4288THE HAUNTED HOUSEYOUR GIRLFRIEND YOU ARE SEARCHING FOR, / WHEN YOU HAVE THE CROSSES SHE'LL APPEAR / BY THE DOOR.
5$7300$4318THE HAUNTED HOUSEBOTH BOY AND GIRL MUST TURN AROUND, / BECAUSE YOUR QUEST IS HOMEWARD BOUND.
6$7380$4388THE GRAVEYARDBY NOW YOUR ENERGY MUST BE LOW, / SO FIND A MUSHROOM BEFORE YOU SLOW.
7$7400$43F0THE GHETTOTHE GHETTO IS A LONELY PLACE, / SO GET YOUR CROSSES WITH GREAT HASTE.
8$7480$4458THE CINEMATHE FINAL MINUTES ARE WITH YOU, / USE THEM WELL.
9$7500$44B0THE FORESTTHE END IS NIGH, YOU'RE GOING TO DIE.

The records are $80 bytes apart except between 4 and 5, which are $100 apart: $7280-$72FF is not a record, and $72E0-$72FF in it is code (switch_allowed, level_records, show_programmed_by). The fields of a record are under Data tables.

The font has no apostrophe. The cards fake one with cursor codes: up, a comma, down (THERE{up},{down}S), which prints the comma a row higher. After the cinema card's $01 end marker come the bytes "N." and another $01 ($41B5-$41B7); no code was found that points at them.

Sound

The music is a small interpreter on voices 1 and 2, run from the IRQ. music_start $60CA silences the SID, points zp $B7/$B8 (the play position) and $B9/$BA (the loop point) at $61F8, sets the filter (music_init_filter) and puts music_irq $60F5 in $0314. music_stop $61D2 gives $0314 back to the KERNAL's $EA31.

Each tick, music_irq counts zp $02 down. At zero, music_next_command $6100 reads the next byte of the tune and stores it into the low byte of the JMP at $610B (music_dispatch), so each command byte is the low byte of its handler's address in page $61. Operands follow the command and are read by music_fetch $61E9. The handlers, read in the code:

ByteHandlerOperandsWhat it does
$0Emcmd_note_v1freq hi, lovoice 1 frequency, gate off then on with the stored waveform
$26mcmd_note_v2freq hi, lothe same on voice 2
$3Emcmd_note_both2 × (hi, lo)both voices, then falls into $56
$56mcmd_retrigger_bothnonegate both voices off and on again
$6Bmcmd_set_tempo1ticks per step into $02AA, then ends the step
$74mcmd_restnonereloads the counter from $02AA: the step ends with nothing new played
$7Cmcmd_countnoneINC $02FF, then ends the step
$82mcmd_set_waveforms2control bytes for voices 1 and 2 into $02AC/$02AD
$91mcmd_set_envelopes4attack/decay for voices 1 and 2, then sustain/release for 1 and 2
$ACmcmd_set_pulse4pulse width lo/hi for voice 1, then voice 2
$C7mcmd_loopnoneplay position back to the loop point
$00$6100 itselfnonefetch the next byte at once: a no-op

The tune runs from $61F9 to the loop command at $6A15: 341 notes and 592 rests over 998 commands, at tempo 6, starting with pulse and sawtooth ($41, $21) (work/sweep_tune.py, work/sweep-tune.txt).

The tune's pitches are computed for the NTSC clock. Of its 478 frequency values, 464 come within 2 cents of equal temperament taken at the NTSC clock (1,022,727 Hz); taken at PAL (985,248 Hz) none do, and the same 464 sit 33 to 36 cents off, which is 65 cents flat of the note above (work/sweep-tune-ntsc.txt, work/sweep-tune.txt). That is the 0.65 of a semitone the platform reference gives for an NTSC table played on PAL, the machine this image was run on. Named at the NTSC clock, voice 1 opens C2, D2, F2, G2, D2, F3. The other 14 values are open.

$02FF counts mcmd_count commands (cleared by music_start). Two places poll it: $5DAD waits for it to leave zero and then calls music_stop, and $7614 branches on it. So the tune can signal the game: card_wait $7610 holds a card until the card tune's count command, and $5DA9 holds GAME OVER until the jingle's (tune_jingle $6B10). Voice 3 carries the two sound effects: the jump, a triangle wave from $0800 (jump_sound $5483), and the fall, a whistle from $8080 down by $80 a step (fall_sound $54AC). A third player, unused_sound_player $CA80, has every SID store aimed at $FFFF (see Unused).

Unused code and data

Unreached means no call, jump, pointer or absolute access names it, and no run of the kit's simulator (work/sim_all.sh, all ten screens from their start) executed or read it; each is described in symbols.json.

The RAM under the I/O chips ($D000-$DFFF) and under the KERNAL above the stores ($F000-$FFFF) is loaded and never read; game.json excludes both with the reason.

Bugs

Live tests

All from work/play-idle.vsf (the forest, outward, standing) or the snapshots saved from it, with the stick on port 2; each script names its control.

TestScriptResult
The machine runs: jiffy clock and stickverify.pyjiffy +61 in 1 s; X 128 → 151 with the stick right
Blue crosses poked beside the boyverify.py, verify3.pytaken: needed 5 → 0, the card for the next screen clears the HUD, which comes back reading 05
Red crosses beside the boyverify.pyleft, counter unchanged
Five blue crosses end the screenverify3.pythe cinema follows (record $7080)
A mushroom under himverify2.pybar 119 → 142 eighths in 6 s; control 119 → 119
A toadstool under himverify.pythe bar changes
Jump with stick upverify2.pyin-air flag 1, Y 224 → 205 → 220
Jump with SHIFT (host-key path has no name for it)verify_shift.py$028D held at 1, re-poked every frame against SCNKEY's own overwrite; in-air flag 1, Y 224 → 205 within 30 frames
Z, C, SPACE, / by the host-key pathverify6.pyZ, C walk; SPACE nothing; / nothing going out
/ and fire on the way backverify3.py, verify6.pythe girl, border 6 → 10
All ten screens by playplaythrough.pyrecords $7000 … $7500 in order, needed 5 then 10, reference/screen-*.png
Starting on a way-back screenverify3.py, shots10.pythe unwritten store shows as garbage
Level settings on every screensettings10.pydirections 0 0 1 1, gravity 1, scroll $FF, fall limit $FF
What drains the bar standing stillverify7.pyan enemy: 69 hits each on energy_down and poison_add; control music_irq 1,799 in 30 s
The keyboard scan runsverify5.pySCNKEY $EA87 61 in 1 s; the matrix tool left $C5 at $40, the host-key path reached it

Cheats

Pokes that change the game within its own parameters. Each one names the variable it changes and whether it has been tested live. Untested pokes are labelled as candidates.

EffectPokeStatus
routines tables variables strings branch labels

Loading listing…