# Bomberman (U) — Game Flow from Power-On This document describes the execution flow of the disassembled Bomberman (U) PRG bank (16 KB, mapped at CPU **$C000–$FFFF**), starting from power-on. It is based entirely on `copilot/prg.s` (with `header.s` and `chr.s` for the iNES header and CHR data) in the repository. --- ## 1. ROM and Memory Layout | Property | Value | |----------|-------| | Format | iNES / NES 2.0 | | PRG | 1 × 16 KB (mapped at CPU $C000–$FFFF) | | CHR | 1 × 8 KB | | Mirroring | Vertical | | PRG bank start | CPU $C000 | | Source files | `copilot/prg.s` (7,553 lines, ~493 KB), `header.s`, `chr.s` | | Output | `reconstructed.nes` (24,592 bytes) | ### Interrupt vectors (CPU $FFFA–$FFFF) | Vector | Address | Handler | |--------|---------|---------| | NMI | $C01A | `NmiHandler` | | RESET | $C000 | `Reset` | | IRQ / BRK | $FFF9 | `IrqHandler` (just `rti` — unused) | --- ## 2. Build Status `make` in `copilot/` assembles and links successfully, producing `reconstructed.nes` (24,592 bytes). **Verification result:** The reconstructed ROM is **byte-identical** to `bomberman.nes` — headers, CHR, and all 16,384 PRG bytes match exactly. ### Naming fixes applied | Change | Location | Reason | |--------|----------|--------| | `RandomState0` kept at `$54`; added `RandomStateAlt = $E4` | `locals.inc` | Zero-page reuse: `$54` is the random-state byte used by most code; `$E4` is a distinct byte decremented at CPU `$E71B` | | `dec RandomState0` → `dec RandomStateAlt` | `prg.s` (was `main.s:6972`) | The original ROM has `dec $E4` at CPU `$E71B`, not `dec $54` | | `ControllerState` → `ControllerState1` | `prg.s` (16 refs) | Clarifies this is controller port 1; added `ControllerState2 = $13` for port 2 | | `Main_HandleControl_C368` → `MainGameLoop` | `prg.s` (3 refs) | Main per-frame game loop entry point | | `Gameplay_HandleControl_C79D` → `GameplayUpdate` | `prg.s` (4 refs) | Gameplay object dispatcher/update | | `Sound_HandleControl_E65F` → `SoundUpdate` | `prg.s` (3 refs) | Sound engine update called from NMI | ### Pass 3 symbol application (2026-10-19) | Pass | Substitutions | Detail | |------|---------------|--------| | locals.inc (single-name) | 354 operands | All zero-page addresses with exactly one name in `locals.inc` | | locals.inc (multi-alias, site-resolved) | 8 operands | `$E0/$E1`: `IndirectPtrLo/Hi` in the RAM-clear loop (`InitializeSystemAndRunMainLoop` → `Main_WriteSoundDispatchIndex_C317`); `SoundDelay0` (indexed, `$E1,x`) in sound engine (`Gameplay_ReadSoundDelay0_E57B`, `InitializeApu`, `Sound_ReadSoundDelay0_E661`) | | symbols.inc (hardware) | 0 operands | `prg.s` already contains no numeric hardware operands ($2000–$4017) | | In-file data-table labels | 24 operands | 24 ROM-space table reads (`lda $C2E7,x` → `lda SystemPaletteData,x`, etc.) substituted via `reconstructed.lbl` | **Residue:** 758 numeric operands remain, addressing bytes not named in either `.inc` file: 505 zero-page (133 distinct addresses), 171 RAM $0100–$07FF (46 addresses), and 82 ROM table reads (54 addresses, 34 of which are interior bytes of labelled tables). ### Pass 3 data-table naming (2026-10-21) Every labelled `.byte` group in `prg.s` has been interpreted and renamed, and the inventory is in **section 11**. The work produced three kinds of change: | Change | Detail | |--------|--------| | Audit-trail repair | The last 515 emitting lines of `prg.s` (from `Sound_HandleControl_E6CD` at $E6C9 to the interrupt vectors at $FFFA) carried damaged audit comments: the instruction comments showed `??` instead of bytes, and the `.byte` rows of the final data region carried PRG offsets that were up to $1A too low. The comments were re-anchored from the ca65 listing (ground truth) and validated byte-for-byte against `prg.bin`; **no instruction and no byte value changed** and `make` still prints *Byte-identical ROM verified.* | | Renames | 26 data labels were renamed to ROM-specific names (e.g. `MainStateDispatchTable` → `ObjectTileBlockTable`, `StatusDigitTileTable` → `GameVersionText`, `StageLayoutPointerData` → `MazeBlockPaletteTable`), with their operand references updated | | Splits | 22 previously undifferentiated byte ranges were given their own labels where the structure is proven (the 2×2 maze tile-block table, the two halves of the screen palette, the maze column pointer table, the sound note tables, …) | --- ## 3. Reset — CPU $FFFC → $C000 **`Reset`** ($C000) On power-on or reset: 1. `sei` — disable maskable interrupts. 2. `lda #$00 / sta PPUMASK / sta PPUCTRL` — disable PPU rendering and NMI. 3. `cld` — clear decimal mode. 4. `ldx #$FF / txs` — initialize stack pointer to $FF. 5. **Wait for two startup VBlanks** — `WaitForFirstStartupVBlank` ($C00D) and `WaitForSecondStartupVBlank` ($C012) each poll `PPUSTATUS` ($2002) for the VBlank bit. 6. `jmp InitializeSystemAndRunMainLoop` ($C30C) — transfer control to the system initializer. --- ## 4. System Initialization — `InitializeSystemAndRunMainLoop` ($C30C) This is the main entry point after reset: 1. **Clear RAM** — writes $00 to zero page via `Main_WriteZp00_C333` ($C333) and to $0100–$07FF via an indirect pointer `IndirectPtrLo`/`IndirectPtrHi` ($E0/$E1) in a 7-page loop (`Main_WriteSoundDispatchIndex_C317` at $C317). 2. `jsr InitializeVideoSystem` ($C1F2) — disables rendering, clears shadow OAM, uploads the system palette, enables NMI. 3. `jsr InitializeApu` ($E642) — silences all APU channels, resets sound delay/state variables (`SoundDelay0`–`SoundDelay2` at $E1–$E3). 4. Writes zero to sound state variables ($E0–$E3). 5. Sets stack pointer to $FF, enables APU ($4015 = $0F). 6. `jsr ComposeStatusScreen` ($DE83) — draws the initial status/title screen. 7. `jsr EnableNmiInPpuControl` ($C2B0) — enables NMI via PPUCTRL shadow. 8. Sets `ScreenState` ($72) = $01. 9. Enters the main loop at `MainGameLoop` ($C368). --- ## 5. NMI / Frame Heartbeat — `NmiHandler` ($C01A) The NMI fires once per frame and is the central real-time driver: 1. **Save registers** (A, X, Y on stack). 2. Set `FrameSyncFlag` ($0B) = $00. 3. Set OAM address to $00. 4. **Sprite DMA path** — if $60 is zero, write OAM entry $0700–$0703 (sprite #0 at Y=$F8) and trigger OAMDMA ($4014 = $07). 5. **VRAM transfer path** — if `VramReadOffset` ($14) ≠ `VramWriteLimit` ($15), transfer VRAM data from a table at $0600–$0607, indexing into pointer data at $D9C6–$D9C9. Handles both write and read packet types. 6. **Screen state / NMI work pending** — based on `ScreenState` ($72) and `NmiWorkPending` ($60), transfers various VRAM data blocks (title screen, gameplay screen, etc.) using `SetPpuAddress` ($C196). 7. **PPU status / audio** — reads PPUSTATUS, commits PPU state and scroll via `CommitPpuStateAndScroll` ($C296), increments `FrameCounter` ($33), handles audio channel timing at $69/$6A. 8. `jsr Ppu_WriteZp10_C19D` ($C19D) — reads and debounces both controller ports, stores results in `ControllerState1` ($12) and `ControllerState2` ($13). 9. `jsr Gameplay_ReadZpB2_E4C7` ($E4C7) — gameplay input handling. 10. `jsr SoundUpdate` ($E65F) — sound engine update. 11. **DMC control** — if $38 and `GameMode` ($59) are zero, starts DMC playback ($4010 = $0E, DMC address $C000, length $FF, enable channels). 12. **PPU scroll wait** — if $60 is set, waits for VBlank (polls PPUSTATUS bit 6), then applies scroll from $0E/$0F. 13. Toggles `FrameParity` ($DB) with `EOR #$05` (frame parity / flip-flop flag). 14. **Restore registers, `rti`.** ### Controller reading (`Ppu_ReadControllerAndReturnA_C1BF` / $C1BF) - Strobes `JOY1` ($4016) with $01 then $00. - Reads 8 bits per controller, shifting into X. - `Ppu_HandleControl_C1EB` masks to the low 2 bits ($03) and increments X if non-zero (button press detection). - Port 1 result → `ControllerState1` ($12); port 2 result → $13 (`ControllerState2`). --- ## 6. Main Loop The main loop starts at **`MainGameLoop`** ($C368): ``` MainGameLoop ($C368): wait until ControllerState1 ($12) is nonzero (button pressed) └─ branch on SELECT ($20) to toggle $5F └─ decrement sub-delay $71 / main delay $70 └─ when delays expire, increment $59 (game mode / screen state) ``` Based on `$59` and `$5F`, the loop dispatches to: - `RunMenuSelectionScreen` ($DA8E) — title/menu screen - Gameplay initialization paths ($C3B6–$C5FF) — sets up object positions, stage, sound - `Main_WriteStageCounter_C613` ($C613) — stage counter display The core per-frame gameplay dispatch uses a **state table** at `ObjectTileBlockTable` ($C75D–$C79C), which maps an object's state/phase to the 2×2 background tile block drawn for it ($00–$27, indexed into `MazeBlockTileTable`). Gameplay object processing walks arrays at $03E6 (state), $0436, $0486 (X/Y position), $04D6, $0526 (timer/flags), with up to 80 ($4F) object slots. Key main-loop subroutines: - `Main_ReadGameMode_C60B` ($C60B) — waits for `FrameDelay` ($B5) to reach zero. - `Main_WriteStageCounter_C613` ($C613) — updates stage counter, handles START button to begin gameplay. - `GameplayUpdate` ($C79D) — main gameplay object dispatcher (80 slots). - `Gameplay_Dispatch_C7A4` / `Gameplay_Dispatch_C835` — object state dispatch. - `Stage_WriteObjectWorkX_D8FC` ($D8FC) — object work initialization. --- ## 7. Gameplay Systems ### Object management - Up to 80 object slots ($00–$4F), tracked by state at $03E6,X. - Position arrays: $0436,X / $0486,X (X/Y), $04D6,X (state/flags), $0526,X (timer/direction). - `Stage_WriteObjectWorkX_D8FC` ($D8FC) initializes object work fields. - Objects are processed in a loop with `dex`/index retreat patterns ($C756, $C8A6). ### Bomb / explosion / player mechanics Referenced throughout the gameplay code: - `Gameplay_HandleControl_C96A`, `Gameplay_HandleControl_CA1B`, `GameplayUpdate` — player/bomb input and state. - `Object_WriteStageProgress_CFB0` ($CFB0) — stage progress tracking. - `Frame_ReadObjectIndex_CE5B` ($CE5B), `Frame_ReadObjectX_CDD4` ($CDD4) — frame-based object reading. ### Stage system - `Stage_ReadZp58_D69A` ($D69A) — stage data lookup. - Stage counter display via `Main_WriteStageCounter_C613`. --- ## 8. Audio / Sound Engine ### `InitializeApu` ($E642) - Clears DMC direct register ($4011), sound delays `SoundDelay0`–`SoundDelay2` ($E1–$E3). - Silences pulse 1, pulse 2, triangle, noise channel controls ($4000, $4004, $4008, $400C). - Enables APU channels ($4015 = $0F). ### `SoundUpdate` ($E65F) - Loops over 3 sound channels (X = 2 down to 0). - Decrements per-channel delay at `SoundDelay0,x` ($E1,X). - When delay expires, reads sound command state from `SoundCommandState` ($DF): - If $DF < 0 → read sound RAM pointer ($E6BC table). - If $DF == 3 → write sound command state. - Otherwise → return or handle based on $E0 (indirect pointer). - Calls into sound dispatch via `SoundCommandHandlerTable` ($E6AD–$E6C8). ### Sound dispatch table (`SoundCommandHandlerTable`, $E6AD) A jump table of 14 sound handler addresses ($E641, $E6C8, $E6CC, $E6DE, $E727, $E766, $E792, $E6AB, $E6AB, $E6AB, $E6AB, $E70A, $E733, $E6AB, $E79E), each handling a specific sound effect or music channel. The last seven entries double as `SoundVoiceHandlerTable` ($E6BB), the per-voice jump table read at $E6A4/$E6A8. ### Channel-specific sound paths - `Sound_UpdateAudioChannelPath_E6CF` ($E6CF) — noise channel ($400E–$400F). - `Sound_UpdateAudioChannelPath_E6DF` ($E6DF) — pulse 2 channel: sets sweep ($4005 = $84), timer lo/hi ($4006/$4007), noise period/length. - `Sound_UpdateAudioChannelPath_E70B` ($E70B) — decrements `RandomState0`, reconfigures pulse 2 when expired. ### Sound state (zero page) | Address | Symbol | Use | |---------|--------|-----| | $DF | `SoundCommandState` | Sound command / state | | $E0 | `IndirectPtrLo` / `SoundDispatchIndex` | Indirect pointer low; sound dispatch index | | $E1–$E3 | `SoundDelay0`–`SoundDelay2` | Per-channel sound delays | --- ## 9. PPU / Rendering ### `InitializeVideoSystem` ($C1F2) - `jsr DisableRendering` ($C257) — wait for VBlank, turn off PPUMASK. - `jsr Ppu_HandleControl_C2B9` ($C2B9) — apply PPUCTRL shadow with NMI disabled. - Set `PpuCtrlShadow` ($0C) = $10, clear $0E/$0F/$14/$15. - `jsr HideAllSprites` ($C268) — fills $0700–$07FF with $F8 (offscreen) in 32-byte strides. - `jsr UploadSystemPalette` ($C2C3) — uploads 32 bytes of palette data from `SystemPaletteData` ($C2E7) to $3F00–$3F1F. - `jsr EnableNmiInPpuControl` ($C2B0) — enable NMI. ### `CommitPpuStateAndScroll` ($C296) - Writes `PpuMaskShadow` ($0D) to PPUMASK ($2001). - Sets PPU address to $0000, writes $00 to $2005 twice (scroll). - Writes `PpuCtrlShadow` ($0C) to PPUCTRL ($2000). ### Palette data (`SystemPaletteData`, $C2E7) 32 bytes of NES palette colors starting at $3F00. --- ## 10. Zero-Page / RAM Symbols The symbol table lives in `copilot/locals.inc` (hardware constants are in `copilot/symbols.inc`). Every address it names is used by name in `prg.s`: | Address | Symbol | Use | |---------|--------|-----| | $0B | `FrameSyncFlag` | Main-thread/NMI frame handoff | | $0C | `PpuCtrlShadow` | Software copy of PPUCTRL | | $0D | `PpuMaskShadow` | Software copy of PPUMASK | | $12 | `ControllerState1` | Controller port 1 sample | | $13 | `ControllerState2` | Controller port 2 sample | | $14 | `VramReadOffset` | NMI VRAM-transfer index into the $0600 table | | $15 | `VramWriteLimit` | Upper limit compared against $14 | | $16 | `NmiPathSelector` | $60=0 → sprite DMA path; $60≠0 → VRAM data path | | $1F | `ObjectScreenX` | Object coordinate temporaries used by object processing | | $20 | `ObjectScreenY` | | | $28 | `ObjectSpawnParam` | Object spawn parameters | | $29 | `ObjectSpawnY` | | | $2A | `ObjectSpawnX` | | | $2B | `ObjectSpawnWidth` | | | $2C | `ObjectSpawnHeight` | | | $33 | `FrameCounter` | Frame counter | | $54–$57 | `RandomState0`–`RandomState3` | Four-byte PRNG state used by `AdvanceRandomState` | | $59 | `GameMode` | Game mode / screen selector | | $5C | `ObjectStateFlags` | Object state trio | | $5D | `ObjectStateTimer` | | | $5E | `ObjectStateExtra` | | | $60 | `NmiWorkPending` | Nonzero → the NMI performs VRAM transfers | | $70 | `MainDelay` | Main-loop delay counter | | $71 | `SubDelay` | Secondary delay counter | | $72 | `ScreenState` | Screen state flag | | $73 | `ObjectXLimit` | Object X limit | | $93 | `StageCounter` | Stage counter | | $9C–$9F | `ObjectWork0`–`ObjectWork3` | Object work fields | | $A4 | `BonusTimer` | Bonus timer | | $AC–$AF | `AudioPtrLo/Hi`, `AudioArgLo/Hi` | Audio data / argument pointers | | $B0 | `GameModeCounter` | Game-mode counter | | $B1 | `ObjectType` | Object type | | $B5 | `FrameDelay` | Frame delay | | $DB | `FrameParity` | Frame-parity toggle, flipped by the NMI each frame | | $DF | `SoundCommandState` | Sound command state | | $E0 | `IndirectPtrLo` / `SoundDispatchIndex` | Indirect pointer for the RAM-clear loop; sound dispatch index in the sound engine | | $E1 | `IndirectPtrHi` / `SoundDelay0` | Indirect pointer high byte; per-channel delay base (indexed by X) | | $E2–$E3 | `SoundDelay1` / `SoundDelay2` | Remaining per-channel delays | | $E4 | `RandomStateAlt` | Distinct byte decremented at CPU $E71B (zero-page reuse) | **Aliases.** Two addresses carry two names each; `prg.s` uses the one that matches the calling routine. `$E0`/`$E1` are `IndirectPtrLo/Hi` in the RAM-clear loop (`InitializeSystemAndRunMainLoop` / `Main_WriteSoundDispatchIndex_C317`) and in the sound engine's pointer work, but `$E1` is `SoundDelay0` wherever the sound engine indexes the per-channel delay table (`SoundDelay0,x`, i.e. `$E1`–`$E3`). **Unnamed residue.** Most zero-page/RAM bytes the game touches have no entry in `locals.inc` and stay numeric — 758 operands address 179 distinct addresses outside the table above (e.g. the `$34`/`$35` indirect pointer used by gameplay data writes). Naming them is Pass-3 semantic work, not a substitution. --- ## 11. Pass 3 data-table inventory Every labelled `.byte` group in `prg.s` is listed below. The **Address** column gives the CPU range the block occupies in the 16 KB PRG image (mapped at $C000–$FFFF), **Size** its length in bytes, **Name** the label now in `prg.s` (`-` where the span carries no label of its own), **Structure** what the bytes actually are, and **Read by** the instruction(s) in this dump that consume them. Three spans are code that the tracer classified as data; they are listed so the count of data blocks is complete, and their names say so. | Address | Size | Name | Structure | Read by | |---------|------|------|------------|---------| | $C2E7–$C306 | 32 | `SystemPaletteData` | 32 NES palette-RAM bytes, one per PPU colour $3F00–$3F1F (3-bit colour in bits 0–2 for $3F00–$3F0F, bits 4–6 for $3F10–$3F1F) | `lda SystemPaletteData,x` at $C2CC (32 iterations → PPU $3F00–$3F1F, routine `UploadSystemPalette` $C2C3) | | $C75D–$C79C | 64 | `ObjectTileBlockTable` | 64 one-byte indices (0–$27) into the 50-entry 2×2 background tile-block table at `MazeBlockTileTable`; the index is the object's state byte ($03E6,x, masked $7F) or the object's movement/phase bits ($0526,x) | `lda ObjectTileBlockTable,y` at $C682; `lda $C764,y` at $C6D7; `lda $C778,y` at $C6AC | | $C9DE–$C9E2 | 5 | `DirectionCodeRemapTable` | 5 one-byte remaps from an animation direction index (0–4) to a movement code — 0→$FF (none), 1→3, 2→4, 3→1, 4→2; the result is held in $2E and compared with the new direction at $C9E3 | `lda DirectionCodeRemapTable,y` at $C9BB | | $CA11–$CA15 | 5 | `DirectionMoveYDeltaTable` | 5 signed one-cell Y deltas (0, 0, −1, 0, +1) for movement codes 0–4, added to the object's screen Y | `adc DirectionMoveYDeltaTable,y` at $C863; `adc DirectionMoveYDeltaTable,x` at $C9F4 | | $CA16–$CA1A | 5 | `MoveXDeltaTable` | 5 signed one-cell X deltas (0, +1, 0, −1, 0) for movement codes 0–4, added to the object's screen X | `adc MoveXDeltaTable,y` at $C85A; `adc MoveXDeltaTable,x` at $C9FD | | $CA47–$CA4A | 4 | `ObjectSpriteTileBlockTable` | 4 one-byte maze tile-block indices (8, 9, 10, 8) choosing the 2×2 background block drawn for an object, selected by bits 4–5 of $03D2,x | `lda ObjectSpriteTileBlockTable,y` at $CA3D | | $CC04–$CC35 | 50 | `UnreferencedStepPatternData` | 50 one-byte values in 1–8 — a per-object step/animation pattern is the obvious reading, but the record structure is not established | not referenced by any instruction | | $CE0F | 1 | `UnreferencedSingleByteData` | a single $60 (RTS) byte left between `jmp $CE2E` at $CE0C and the next routine at $CE10 | not referenced by any instruction | | $CED2–$CEE8 | 23 | `ObjectSpriteTileTable` | 23 sprite tile indices (`10 11 12 11 00 01 02 01 03 04 05 04 06 07 08 07 09 0A 0B 0C 0D 0E 0F`) selected by the sprite's height/size field ($2C) when the OAM sprite is written | `lda ObjectSpriteTileTable,x` at $CECC; `lda $CED6,x` at $CEA1 — the two readers use the same list with bases four bytes apart | | $CFE3–$CFE8 | 6 | `UnreferencedBoundaryData` | 6 bytes (20 07 D2 20 06 D0) sitting between two routines; neither a 3- nor a 6-byte record reading is confirmed | not referenced by any instruction | | $D124–$D153 | 48 | `EnemyStepOpCodeTable` | 16 three-byte records {6502 opcode, zero-page address, value}: INC $46–$49 with value 0, DEC $46–$49 with value 0, then the same eight with value $46 — the enemy movement micro-steps | not referenced by any instruction | | $D154–$D185 | 50 | `EnemySpriteTileTable` | 50 one-byte sprite tile indices for the enemy movement types (the first 32 are used; values $18–$3D) | `lda EnemySpriteTileTable,y` at $D05F (Y = the enemy's movement type in $05A8,x, 0–31) | | $D186–$D1A5 | 32 | `EnemySpriteAttrTable` | 32 one-byte sprite attribute bytes for the enemy movement types (values $14–$3D) | `lda EnemySpriteAttrTable,y` at $D05A | | $D1A6–$D1C7 | 34 | `UnreferencedEnemyStepData` | 34 bytes of small values ending in the doubling series 04 08 0A 14 28 50 64 C8 (a speed/duration ladder) | not referenced by any instruction | | $D207–$D245 | 63 | `EnemyBehaviourCodeRegion` | **not data** — 63 bytes of 6502 code (LDX $4D; LDA $41…; STA $05E4,X; RTS) that the tracer classified as data | not reached by any instruction in this dump | | $D246–$D25D | 24 | `EnemyBehaviourTable` | little-endian 16-bit code addresses, one per enemy type, at $D246 + 2×(type−1); the first ten point at the enemy-behaviour routines in $D2xx–$D3xx ($D320, $D2E7, $D2C8, $D305, $D2B4, $D319, $D3A0, $D38A, $D2A2, $D264) | `lda $D244,y` at $CFDE (low byte) and `lda $D245,y` at $CFDA (high byte), with Y = 2 × the enemy type — the reader's base is two bytes below the table's first entry because the type index starts at 1 | | $D25E–$D5F1 | 916 | - | **not data** — the enemy-behaviour routines (DEC $49 / BNE / LDA / INC …) that the tracer classified as data; the `EnemyBehaviourTable` label's `.byte` rows cover this span | the target of the jump table at $D246, which is itself unreached | | $D708–$D8FB | 500 | `StageEnemyTypeTable` | 50 rows × 10 one-byte enemy-type ids (0 = no enemy, 1–7 = enemy kind); row *n* is the enemy roster for a stage with *n* enemies, column = enemy slot | `lda ($97),y` at $D6BE, with the indirect pointer ($97)/($98) set to $D708 at $D6AC–$D6AE and Y = 10 × (n−1) | | $D994–$D9C5 | 50 | `MazeBlockPaletteTable` | 50 one-byte background-palette numbers (0–3), one per 2×2 maze tile block; the value is masked and written into the PPU attribute byte | `lda MazeBlockPaletteTable,y` at $D929 (Y = the tile-block index) | | $D9C6–$DA8D | 200 | `MazeBlockTileTable` | 50 records × 4 background tile indices (top-left, top-right, bottom-left, bottom-right) — the 2×2 tile block drawn for one maze cell | `lda MazeBlockTileTable,x` at $C05C and $CB78; `lda $D9C7,x` at $C062 and $CB7E; `lda $D9C8,x` at $C075 and $CB91; `lda $D9C9,x` at $C07B and $CB97 (X = 4 × the block index, held in $0603) | | $DC53–$DD03 | 177 | `StatusScreenTextScript` | a script of text records {PPU address high, PPU address low, ASCII string, $00} — seven addressed records (`CONGRATULATIONS` at $2088, `YOU`␣`HAVE`␣`SUCCEEDED`␣`IN` at $20E4, `HELPING`␣`BOMBERMAN`␣`TO`␣`BECOME` at $2122, `A`␣`HUMAN`␣`BING` at $2162, `MAYBE`␣`YOU`␣`CAN`␣`RECOGNIZE`␣`HIM` at $21A4, `IN`␣`ANOTHER`␣`HUDSON`␣`SOFT`␣`GAME` at $21E2, `GOOD`␣`BYE` at $224B) plus an eighth, unaddressed string `ENTER`␣`SECRET`␣`CODE` at $DCF2 that the seven-call draw routine never reaches. In the transcription `␣` is tile $3A, a solid background-colour tile the game uses as its word separator — not the ASCII colon | `lda StatusScreenTextScript,y` at $DC39 (address high), `ldx StatusScreenTextScript,y` at $DC3D (address low), `lda StatusScreenTextScript,y` at $DC44 (string bytes) | | $DD22–$DD3D | 28 | `StatusPaletteSetTable` | 7 groups of 4 NES palette bytes; the group chosen by A is written to PPU $3F1C–$3F1F (six groups are `0F 00 00 00`, the seventh is `0F 15 36 21`) | `lda StatusPaletteSetTable,x` at $DD15, X = 4 × A | | $DD62–$DD6A | 9 | `GameVersionText` | the ASCII string `GAME`␣`VER` stored in reverse (`REV`␣`EMAG`) so it can be output with a decrementing index; written to PPU $2100–$2108 (␣ = tile $3A) | `lda GameVersionText,x` at $DD50, X = 8…0 | | $DE15–$DE1C | 8 | `StatusScreenTimeLeftText` | two reversed 4-character ASCII strings — `TIME` ($DE15–$DE18) and `LEFT` ($DE19–$DE1C) | `lda StatusScreenTimeLeftText,x` at $DDE1 (→ `TIME` at $2041); `lda $DE19,x` at $DE07 (→ `LEFT` at $2058) | | $DE4E–$DE52 | 5 | `StatusScreenStageText` | the ASCII string `STAGE` stored in reverse (`EGATS`); written to PPU $21EA | `lda StatusScreenStageText,x` at $DE33, X = 4…0 | | $DE78–$DE82 | 11 | `BonusStageText` | the ASCII string `BONUS`␣`STAGE` stored in reverse (`EGATS`␣`SUNOB`); written to PPU $21EA (␣ = tile $3A) | `lda BonusStageText,x` at $DE69, X = 10…0 | | $DF8C–$DF9B | 16 | `MenuPaletteData` | 16 NES palette bytes uploaded to PPU $3F00–$3F0F for the menu screens; the block **overlaps** `ScreenPaletteData` ($DF90–$DF9F), which the status screen uploads to the same addresses | `lda MenuPaletteData,x` at $DF37 (16 iterations) | | $DF90–$DF9F | 16 | `ScreenPaletteData` | 16 NES palette bytes uploaded to PPU $3F00–$3F0F for the status screen (`0F 30 0F 06 26 37 0F 0F 0F 0F 05 00 09 04 0D 07`) | `lda ScreenPaletteData,x` at $DE95 (16 iterations) | | $DFA0–$DFAF | 16 | `MenuScreenAttrTable` | 16 background-attribute bytes, one per menu-screen column (`05 00 09 04 0D 07 02 0A 0F 0C 03 08 0B 0E 01`) | `lda MenuScreenAttrTable,x` at $DB51, X = the screen column & $0F | | $DFB0–$E0AF | 256 | `StatusTileMapTop` | 256 background tile indices — nametable rows 0–7 of the status screen, written to PPU $2000–$20FF | `lda StatusTileMapTop,x` at $DEB9 (256 iterations) | | $E0B0–$E1AF | 256 | `StatusTileMapBottom` | 256 background tile indices — nametable rows 8–15 of the status screen, written to PPU $2100–$21FF | `lda StatusTileMapBottom,x` at $DEC2 (256 iterations) | | $E1B0–$E1BF | 16 | `UnreferencedTrailingBytes` | 16 bytes (`$F4` followed by fifteen `$F9`); the values are tile indices in the same range as the status-screen map | not referenced by any instruction | | $E1E5–$E242 | 94 | `CreditTextScript` | five text records {PPU address high, PPU address low, ASCII string, $FF} — `START␣␣␣CONTINUE` at $2269, `TOP` at $22AA, `TM␣AND␣©␣1987␣HUDSON␣SOFT` at $22E3, `LICENSED␣BY` at $232A, `dNINTENDO␣OF␣AMERICA␣INC` + a dot-like glyph at $2364; here `␣` is tile $B0 (a fully blank tile), `$FE` is the circled © mark and `$FD` a small dot-like glyph | `lda CreditTextScript,y` at $E1E0 (routine `RenderEncodedTextRecords` $E1C0, called from $DEA3) | | $E243–$E2A2 | 96 | `MazeLayoutTemplate` | 96 one-byte cell flags (0/1) of the initial maze layout; the first 33 bytes are copied into the RAM maze buffer at $0200 | `lda MazeLayoutTemplate,x` at $CABE (X = 0…32) | | $E2A3–$E2AF | 13 | `MazeColPtrLoTable` | the 13 low bytes of the maze-column pointer table (`00 20 40 60 80 A0 C0 E0 00 20 40 60 80`) — each maze column starts at $0200 + 32 × column in the RAM maze buffer | `lda MazeColPtrLoTable,y` at $C943, $C974, $CAEB, $CBEB, $CC73, $CCF1, $CD47, $CD7E, $CDB0, $CDE1 | | $E2B0–$E2BC | 13 | `MazeColPtrHiTable` | the 13 high bytes of the same table (`$02` × 8 then `$03` × 5), so the columns map to $0200…$0380 | `lda MazeColPtrHiTable,y` at $C948, $C979, $CAF0, $CBF0, $CC78, $CCF6, $CD4C, $CD83, $CDB5, $CDE6 | | $E360–$E387 | 40 | `ScoreDigitPointerTable` | 20 little-endian 16-bit pointers into zero page ($0061–$00DD), one per score digit position | `lda ScoreDigitPointerTable,x` at $E353 (low byte) and $E359 (high byte) | | $E388–$E397 | 16 | `ScoreAttrTable` | 16 background-attribute bytes for the score display area (`41 4F 46 4B 43 50 47 45 4C 42 48 4D 4A 44 4E 49`) | `lda ScoreAttrTable,y` at $E32F (Y = 0…15) | | $E4BC–$E4C1 | 6 | `PowerUpScoreTable` | 6 one-byte values, one per power-up type (`01 02 64 32 03 C8`); the byte is compared against $64 at $E3D5 to decide which score counter is credited | `lda PowerUpScoreTable,x` at $E3D2, X = the power-up id in `ObjectWork1` ($9D) | | $E5E6–$E5F5 | 16 | `SoundVoiceJumpTable` | 8 little-endian 16-bit code addresses ($E5F5, $E5FA, $E601, $E60F, $E61A, $E62C, $E633, $E63A) selecting the sound routine for a voice; the index is 255 − the sound-command byte | `lda SoundVoiceJumpTable,y` at $E5E1 (low byte) and `lda $E5E7,y` at $E5DD (high byte), Y = 2 × (255 − $B4) | | $E5F6–$E641 | 76 | - | **not data** — the sound-driver code that follows the jump table (the tracer classified it as data) | reached through `SoundVoiceJumpTable` | | $E6AD–$E6C8 | 28 | `SoundCommandHandlerTable` | 14 little-endian 16-bit code addresses, one per sound command (0–13): $E641, $E6C8, $E6CC, $E6DE, $E727, $E766, $E792, $E6AB (×4), $E70A, $E733, $E6AB, $E79E — the last seven entries are the same bytes as `SoundVoiceHandlerTable` | `lda SoundCommandHandlerTable,x` at $E697 (low byte) and `lda $E6AE,x` at $E693 (high byte), X = 2 × the sound command | | $E6BB–$E6C8 | 14 | `SoundVoiceHandlerTable` | 7 little-endian 16-bit code addresses, one per sound voice (0–6): $E6AB, $E6AB, $E6AB, $E70A, $E733, $E6AB, $E79E | `lda SoundVoiceHandlerTable,x` at $E6A8 (low byte) and `lda $E6BC,x` at $E6A4 (high byte), X = 2 × the sound command | | $E763–$E766 | 4 | `SoundEffectPitchTable` | 4 APU pulse-2 timer low bytes (`65 87 B4 F0`) giving four effect pitches; the timer high byte is $10 | `lda SoundEffectPitchTable,x` at $E751, X = $E5 | | $E7DE–$E7E1 | 4 | `SoundFxPulseTimerBytes` | 4 APU pulse timer low bytes — $A9/$A0 for pulse 1 and $6A/$64 for pulse 2 | `lda SoundFxPulseTimerBytes,x` at $E7B0 (pulse 1); `lda SoundFxPulseTimerBytes+2,x` at $E7B6 (pulse 2) | | $E7E2–$E87B | 154 | `SoundNoteTimerTable` | 77 little-endian 16-bit APU period values, one per semitone, descending from $07F0 (≈55 Hz) to $001A (≈4.3 kHz); the exponential spacing (a factor of 2^(−1/12) per step) is a chromatic scale | `lda SoundNoteTimerTable,x` at $E5B8 (low byte) and `lda $E7E3,x` at $E5BE (high byte), X = 2 × the note index in $B4 | | $E87C–$E8D3 | 88 | `SoundNoteConfigTable` | 11 records × 8 bytes: 6 bytes of per-note configuration copied to $BC–$C1, then a 16-bit pointer (into $E8xx–$EDxx) stored in $D3/$D4 | `lda $E87C,y` at $E4E5 and $E4F0 (low byte), `lda $E87D,y` at $E4F5 (high byte), Y = 8 × the note index | | $E8D4–$EDF1 | 1310 | `SoundSequencePatternData` | a byte stream built from ~30-byte records that repeat with small variations (the 15-byte run `86 29 A4 30 86 30 35 F9 8C 2E F8 98 2D 86 2D` occurs at $E8D0 and $E8F3); it is the music/sequence script the sound engine plays, but its record format is not established | not referenced by any instruction in this dump | | $EDF2–$EDF3 | 2 | `SoundScriptSeedValue` | the first 16-bit value of the sound script ($0381), loaded into the script argument pointer $AE/$AF | `lda SoundScriptSeedValue` at $C400 and `lda SoundScriptSeedValue+1` at $C405 | | $EDF4–$FFF8 | 4613 | `SoundScriptData` | the sound-script byte stream the engine reads sequentially through the pointer in $AC/$AD (set to $EDF4 at $C3FA), followed by the 2bpp title/ending-screen graphics (the tile bitmaps become legible from about $FE75) | the pointer is set by `sta AudioPtrLo`/`sta AudioPtrHi` at $C3FA/$C3FE and advanced by `lda (AudioPtrLo),y` at $CF94/$CF9B; no absolute read of the block exists | **Totals:** 50 labelled blocks covering 9,487 bytes — 47 data blocks and 3 spans that the tracer misclassified as data but which hold 6502 code ($D207–$D245, $D25E–$D5F1 and $E5F6–$E641; the `EnemyBehaviourTable` label's `.byte` rows also cover the code at $D25E–$D5F1). Two blocks overlap on purpose: the menu and status palettes share $DF90–$DF9B, and the sound command and sound voice tables share $E6BB–$E6C8. Seven data blocks are read by no instruction in this dump; they are recorded with their ROM provenance rather than named after something the ROM does not support. `SoundScriptData` is read, but only indirectly, through the pointer in $AC/$AD. ### 11.1 How the readings were established * **Font as an oracle.** The CHR pattern table at $1000 (the background table, selected by bit 4 of PPUCTRL) carries an ASCII-mapped font at tiles $20–$5F: tile $30 is `0`, $31 is `1`, $41 is `A`, $47 is `G`, $52 is `R`. Rendering the eight tiles `$47 $41 $4D $45 $3A $56 $45 $52` spells **GAME:VER**, which is exactly the byte sequence `GameVersionText` stores. Every reversed ASCII string in the ROM was therefore readable directly, and every tile index in a table could be checked against the graphics. * **Two blank tiles.** `$3A` is a solid background-colour tile (the word separator in the status and bonus strings) and `$B0` is a fully blank tile (the separator in the credit text and the fill written over the top of the status screen at $DEAF). Neither is the ASCII colon, so the `␣` in the transcriptions above stands for the separator tile, not for `:`. * **Indirect readers.** `StageEnemyTypeTable` is read through the zero-page pointer ($97)/($98), which the routine at $D6A8 sets to $D708 before the loop at $D6BE; `MazeLayoutTemplate` is copied through the pointer ($34)/($35) at $CABE; the credit and status texts are read through the byte-stream routine at $E1E0 and the script walker at $DC39. * **The maze geometry.** `MazeColPtrLoTable`/`MazeColPtrHiTable` give the base of each of the 13 maze columns in the RAM buffer at $0200 (32 bytes per column, $0200–$039F, stride $20 = 13 × 32 = 416 bytes, which ends exactly where the object arrays at $03A0 begin). The two pointer tables are the only route into that buffer, and every reader of them (`$C943`, `$CAEB`, `$CC73`, …) is a routine that asks "what is in maze cell (column, row)?". * **The note table.** The values at $E7E2 fall by a factor of 2^(−1/12) per step (differences 114, 108, 100, 96, 91, 84, …), which is the period progression of a chromatic scale, and the table is indexed by 2 × the note number at $E5B8. * **Pass-1 residue.** Three spans listed above hold 6502 code that the tracer reached no instruction into; $D207–$D245 and $D25E–$D5F1 disassemble cleanly into the enemy-behaviour routines, and $E5F6–$E641 into the sound driver. They are still emitted as `.byte` data, so the build is unchanged, but they are code and should be re-classified in a later pass. --- ## 12. Summary of Execution Flow ``` Power On / Reset │ ▼ Reset ($C000) ───────────────────────────────────────┐ │ sei, disable PPU, cld, stack=$FF │ │ wait 2× VBlank │ ▼ │ jmp InitializeSystemAndRunMainLoop ($C30C) │ │ │ ├─ clear RAM ($0000–$07FF) │ ├─ InitializeVideoSystem ($C1F2) │ │ ├─ DisableRendering │ │ ├─ HideAllSprites │ │ ├─ UploadSystemPalette ($3F00–$3F1F) │ │ └─ EnableNmiInPpuControl │ ├─ InitializeApu ($E642) — silence all channels │ ├─ ComposeStatusScreen ($DE83) │ ├─ EnableNmiInPpuControl │ └─ MainGameLoop ($C368) ◄─────────────┘ │ MainGameLoop ($C368) ─────────────────────────────────┤ │ │ ├─ wait for controller button press │ ├─ handle SELECT / delay counters ($70,$71) │ ├─ increment game mode ($59) │ ├─ RunMenuSelectionScreen ($DA8E) [title] │ ├─ gameplay init ($C3B6–$C5FF) │ ├─ GameplayUpdate ($C79D) │ │ │ 80 object slots ($03E6 state array) │ │ ├─ object position ($0436/$0486) │ │ ├─ object timer ($0526) │ │ └─ object dispatch via ObjectTileBlockTable │ └─ eventually → back to MainGameLoop │ │ NMI ($C01A) [every frame] │ │ │ ├─ OAM setup + DMA ($4014) │ ├─ VRAM transfer from $0600 table │ ├─ screen state transfer ($D9C6–$D9C9 pointers) │ ├─ controller read & debounce ($C1BF) │ ├─ Gameplay_ReadZpB2_E4C7 ($E4C7) │ ├─ SoundUpdate ($E65F) │ ├─ DMC start ($4010–$4013) │ ├─ PPU state & scroll commit ($C296) │ ├─ frame parity toggle ($DB) │ └─ rti │ │ Sound Engine ($E65F) [from NMI] │ ├─ per-channel delay countdown ($E1–$E3) │ ├─ SoundCommandHandlerTable ($E6AD) │ ├─ pulse 2, noise, DMC channel control │ └─ RandomStateAlt ($E4) decrement │ ``` --- ## 13. Notes - **Build is byte-identical:** The apparent one-byte difference (`dec $E4` vs `dec $54` at CPU $E71B) was resolved by recognizing zero-page reuse — `$54` is `RandomState0` used by most code, while `$E4` is a distinct byte (`RandomStateAlt`) decremented at that one location. Both are now separately named in `locals.inc`. - **Source annotation:** `prg.s` uses the canonical `; CPU:$C123 PRG:0123 AD 21 03` audit comment format on every instruction, enabling direct comparison with the binary. - **Audit trail repaired 2026-10-21:** the last 515 emitting lines of `prg.s` (from $E6C9 to the vectors at $FFFA) carried `??` byte lists and PRG offsets that were up to $1A too low. They were re-anchored from the ca65 listing and validated against `prg.bin`; every audit comment in the file now names the bytes that are actually at the address it gives. No instruction and no byte value changed. - **Code organization:** Code is organized as reachable routines traced from the NMI/RESET/IRQ control-flow graph; unreachable bytes are classified as explicit data regions. - **Legacy file:** `main.s` (8,070 lines) — a pre-standardization duplicate carrying the inline iNES header and the old `; ROM $xxxxxx` comment format — was **removed 2026-10-20**; it remains recoverable in git history. The build uses (and always used) `prg.s` + `header.s` + `chr.s`. - **Vectors confirm** NMI at $C01A and RESET at $C000, matching the reset handler location. - **Known misnomer:** `Main_WriteSoundDispatchIndex_C317` ($C317) is actually the RAM-clear loop (writes $00 to $0100–$07FF via `IndirectPtrLo/Hi`), not a sound-dispatch write. The name reflects a prior pass's assumption; the operands now correctly use `IndirectPtrLo/Hi`. Renaming the label is a follow-up task. - **Four "data regions" are really code** — see section 11.1. The tracer reached no instruction into $D207–$D245, $D25E–$D5F1 or $E5F6–$E641, so they were emitted as `.byte` data; they disassemble cleanly as 6502 code and should be re-classified in a later pass. - **The maze lives in RAM.** The 13×11 playfield is a column-major bitmap in $0200–$039F (32 bytes per column). `MazeColPtrLoTable`/`MazeColPtrHiTable` are the only pointers into it; the cell value is read with `lda ($34),y` and compared against $08 at $CC83. ## 14. Open work - **Re-classify the four code spans** ($D207–$D245, $D25E–$D5F1, $E5F6–$E641 and the $D246–$D5F1 label's tail) from data to code. The bytes are already correct in the ROM; only the `prg.s` classification is wrong. - **Seven blocks are read by nothing in this dump** and their meaning is recorded as unestablished in section 11: `UnreferencedStepPatternData` ($CC04), `UnreferencedSingleByteData` ($CE0F), `UnreferencedBoundaryData` ($CFE3), `EnemyStepOpCodeTable` ($D124), `UnreferencedEnemyStepData` ($D1A6), `UnreferencedTrailingBytes` ($E1B0) and `SoundSequencePatternData` ($E8D4). The last of these is the music script the sound engine plays; its record format is the largest open question in the project. - **`SoundSequencePatternData` / `SoundScriptData`** — the sound engine reads the script through the pointer in $AC/$AD, so the exact record grammar is still to be worked out. - **`MazeLayoutTemplate`** — 96 flag bytes; only the first 33 are copied into the maze buffer by the routine at $CABA, and the rest is unexplained. - **Routine names.** Several labels still carry their Pass-2 `Gameplay_*` / `Menu_*` / `Object_*` prefixes (`Main_WriteSoundDispatchIndex_C317`, `Stage_WriteObjectWorkX_D8FC` — which is really the "draw one 2×2 tile block" routine — and others). The data-table pass did not touch routine names. ## Power-on flow analysis Derived from the disassembly by `_tools/derive_poweron.py`; the call graph is walked from the reset entry.