11 KiB
Duck Hunt (JUE) — Promoted to Top Level (2026-10-19)
ROM: Duck Hunt (JUE)/Duck Hunt (JUE).nes (24,592 bytes, iNES). Status: Pass 1 complete — 37.5% code; Pass 2 complete; Pass 3 complete; Promoted to top level 2026-10-19. copilot/prg.s is a real disassembly: 3,058 instructions (6,152 bytes as code), 472 label definitions all named — 30 semantic routine names + 428 SubC<addr> entry points + 293 <Routine>_Block[_n] internal branch targets. locals.inc holds 174 zero-page slots with 18 semantic aliases added. All 18 multi-alias ZP operands substituted in prg.s. 20 data tables/structures named in the 10,232-byte data region. It builds byte-identical at the whole-ROM level (cmp reconstructed.nes rom.nes).
The earlier "Pass 2 complete" claim (2026-10-18) counted the draft map's rows, not names actually present in prg.s. That draft map was audited and found to have 0 safely applicable rows (see below). Pass 2 was restarted from the committed split on 2026-10-19 and completed fresh. Pass 3 (data table naming + full ZP substitution) completed same day. Promotion to top level completed 2026-10-19.
ROM Facts
| Property | Value |
|---|---|
| Mapper | 0 (NROM) — from the iNES header |
| PRG | 16 KB (header claims map at CPU $C000–$FFFF) |
| CHR | 8 KB |
| Mirroring | Vertical |
| Battery-backed flag | clear |
| ROM SHA-256 | ca0d31e6f3aa70204fa5af67803c6533851a28fa48a133e15d772d3667fa675a |
| PRG SHA-256 | bcdd22fbe343ebd33c7fd19309510079150d1e4dc052f65382b64d197d6ba080 |
Interrupt Vectors (taken from PRG $3FFA–$3FFF)
| Vector | Target | Note |
|---|---|---|
| NMI | $C086 |
per-frame heartbeat |
| RESET | $C000 |
cold start at $C000 — the usual pattern in this repo |
| IRQ | $C000 |
redirects to RESET — unused placeholder |
copilot/ Contents
| File | Purpose |
|---|---|
Duck Hunt (JUE).nes |
original ROM image (reference) |
prg.bin / header.bin / chr.bin |
split image, produced by _tools/splitnes.py |
rom.nes |
symlink to ../Duck Hunt (JUE).nes (space-free reference for cmp) |
prg.s |
Pass 1/2/3: classified code/data split, 3,058 instructions, 472 labels all named (30 semantic + 428 SubC<addr> + 293 _Block), canonical audit comments |
symbols.inc |
shared NES hardware constants (PPU/APU/controller) |
locals.inc |
174 zero-page slots, 18 semantic aliases (e.g., PtrLo/PtrHi, Ptr2Lo, GameState, SndTemp, SndChannel, SndNote, SprData, Temp, CtrlState, CtrlTimer, VramPtrLo/Hi, DataPtrLo, SndChan, SndPtrLo/Hi, FrameCounter) |
duckhunt_label_map.tsv / duckhunt_full_map.tsv |
Pass 2 draft maps — stale and multi-candidate, see the audit below; kept for provenance only |
header.s / chr.s |
one-line .incbin wrappers for header.bin / chr.bin |
linker.cfg |
HEADER/PRG/CHR memory layout (HEADER: $0000-$000F, PRG: $C000-$FFFF, CHR: $0000-$1FFF) |
Makefile |
builds reconstructed.nes and cmps it against rom.nes |
reconstructed.nes |
build output from the last verified build |
Header and CHR are now linked: make verifies whole-ROM byte identity against rom.nes.
Build
cd "Duck Hunt (JUE)/copilot"
make # → cmp reconstructed.nes rom.nes → "Byte-identical ROM verified."
Pass Status
| Pass | Status |
|---|---|
| Pass 1 | Complete — 3,058 instructions, 6,152 bytes as code (37.5%), 10,232 bytes as data, byte accounting exact (16,384) |
| Pass 2 | Complete — 472 label definitions all named (30 semantic + 428 SubC<addr> + 293 _Block); 174 zero-page slots with 18 semantic aliases in locals.inc |
| Pass 3 | Complete — 18 multi-alias ZP operands fully substituted in prg.s; 20 data tables/structures named across all data regions; hardware register operands (68) substituted via apply_symbols.py |
| Promotion | Complete (2026-10-19) — added header.s/chr.s + HEADER/PRG/CHR linker.cfg, switched make to whole-ROM cmp, moved _tools paths |
Pass 2 Status and Map Audit (corrected 2026-10-19)
The earlier "Pass 2 complete" text was wrong on every number it gave, because it read the draft map instead of the source:
- "48 semantic labels" was the number of distinct old names in the map (
duckhunt_full_map.tsvhas 272labelrows covering only 48 distinctLabel_names).prg.sactually named 31 definitions; the other 441 were stillLabel_. - "174 zero-page semantic aliases" was the count of neutral
ZpXXentries inlocals.inc. There were zero semantic zero-page aliases. - Several listed "key renamed routines" (
InitPPU,MainGameLoop,GameModeCheck,TitleScreenMode,UpdateSprites,InitSound) did not exist in the file under those names.
Draft-map audit (duckhunt_full_map.tsv, 272 label rows):
| Outcome | Rows | Meaning |
|---|---|---|
| already applied | 31 | the proposed name already sits at that address |
| name conflict | 210 | a different candidate name already sits there — the map is a ranked candidate list (one Label_C2BF carries 205 competing names) |
| stale address | 2 | the address is not an instruction boundary in the current prg.s |
| unlabelled, unreferenced | 29 | an instruction boundary with no label and no operand referencing it |
| safely applicable | 0 | no row is both insertable and corroborated by control flow |
One row in the file is malformed (label_Label_C2BF, missing tab). The map is therefore kept only as provenance. Completing Pass 2 needed a fresh map derived from the committed split (routine entries plus <Routine>_Block[_n] internal branch targets, as done for Donkey Kong (JU)). This was done on 2026-10-19.
Pass 2 Naming Summary (2026-10-19)
-
30 semantic routine names preserved from Pass 1:
Reset,WaitVBlank,ClearZeropage,InitGameState,InitVariables,MainGameLoop_1,MainGameLoop_3,NmiHandler,NmiFrameEnd,GameModeDispatcher,TitleScreenLoop,InitializeRoundData,InitializeDuckDataLoop,UpdateZp09_2,UpdateGameState,UpdateGameState2,UpdateGameState3_1,UpdateGameState6_3,UpdateSprites_4,UpdateSprites2_1,UpdateSprites2_3,UpdateSprites3,UpdateController,UpdateController2,UpdateController3,UpdateController4_206,GameModeCheck_3,GameplayFrame,UpdateGameState3_3,UpdateGameState5. -
428
SubC<addr>entry points for all remainingjsr/jmptargets that lacked semantic names (e.g.,SubCC14Eat $C14E,SubCC303at $C303, etc.). -
293
<Routine>_Block[_n]internal branch targets derived from control flow within each routine (e.g.,Reset_Block,GameModeDispatcher_Block…GameModeDispatcher_Block_5, etc.).
Zero-Page Semantic Aliases (18, added 2026-10-19)
| Alias | ZpXX | Address | Role |
|---|---|---|---|
PtrLo |
Zp00 |
$00 | general-purpose indirect pointer low byte |
PtrHi |
Zp01 |
$01 | general-purpose indirect pointer high byte |
Ptr2Lo |
Zp0C |
$0C | secondary indirect pointer low byte |
GameState |
Zp09 |
$09 | game state/mode (InitGameState, InitializeDuckDataLoop, etc.) |
SndTemp |
Zp0F |
$0F | sound engine temporary |
SndChannel |
Zp14 |
$14 | sound channel index |
SndNote |
Zp15 |
$15 | sound note/frequency |
SprData |
Zp23 |
$23 | sprite data pointer |
Temp |
Zp25 |
$25 | general-purpose temporary |
CtrlState |
Zp31 |
$31 | controller state |
CtrlTimer |
Zp32 |
$32 | controller timer/repeat counter |
VramPtrLo |
Zp96 |
$96 | VRAM pointer low byte |
VramPtrHi |
Zp97 |
$97 | VRAM pointer high byte |
DataPtrLo |
ZpB4 |
$B4 | data pointer low byte |
SndChan |
ZpEB |
$EB | sound channel |
SndPtrLo |
ZpEC |
$EC | sound pointer low |
SndPtrHi |
ZpED |
$ED | sound pointer high |
FrameCounter |
ZpF3 |
$FF | frame counter |
Pass 3 Data Table Naming (2026-10-19)
20 data tables/structures named across all data regions:
| Label | Address Range | Size | Description |
|---|---|---|---|
GameState3_1_Data |
$C12C–$C14B | 32 bytes | UpdateGameState3_1 data |
SpriteAnimTable |
$C511–$C580 | 112 bytes | Sprite animation table (7 entries × 16 bytes) |
SpriteAnimTable2 |
$C5AA–$C5C9 | 32 bytes | Sprite animation table 2 (2 entries) |
SpriteDataTable |
$C72C–$C7EB | 196 bytes | Sprite data table (13 entries) |
DuckDataTable |
$D2A4–$D3C3 | 192 bytes | Duck data table (12 entries) |
SpriteDataTable2 |
$D56A–$D5C9 | 96 bytes | Sprite data table 2 (6 entries) |
LevelData |
$DAC5–$F355 | 6,289 bytes | Level/background data |
SpritePtrTable |
$DCC0–$DCCF | 16 bytes | Sprite pointer table (8 pointers) |
SpriteOffsetTable |
$E532–$E541 | 16 bytes | Sprite offset table |
GameInitData |
$E5DF–$E5EA | 12 bytes | Game initialization data |
SpritePosTable |
$E5DB–$E5E2 | 8 bytes | Sprite position table |
ControllerTable |
$E60B–$E612 | 8 bytes | Controller state table |
SpritesTable |
$E661–$E66E | 14 bytes | Sprites table |
SpriteDataTable3 |
$E73C–$E74B | 16 bytes | Sprite data table 3 |
SpriteDataTable4 |
$E75A–$E765 | 12 bytes | Sprite data table 4 |
SpriteDataTable5 |
$E766–$E775 | 16 bytes | Sprite data table 5 |
SpriteAnimData |
$EB30–$EB3F | 16 bytes | Sprite animation data |
SpriteAnimData2 |
$EBFA–$EBF9 | 10 bytes | Sprite animation data 2 |
SoundData |
$EC1F–$EC2E | 16 bytes | Sound data |
SpritePtrTable |
$DCC0–$DCCF | 16 bytes | Sprite pointer table (8 pointers) |
SpriteDataTable6 |
$E92A–$E939 | 16 bytes | Sprite data table 6 |
SpriteDataTable7 |
$E944–$E953 | 16 bytes | Sprite data table 7 |
SpriteDataTable8 |
$E968–$E977 | 16 bytes | Sprite data table 8 |
SpriteDataTable9 |
$E9C8–$E9D7 | 16 bytes | Sprite data table 9 |
SpriteDataTable10 |
$EA30–$EA3F | 16 bytes | Sprite data table 10 |
FrameData |
$F300–$F34F | 80 bytes | Frame data |
FontData |
$F350–$F35F | 16 bytes | Font/glyph data |
BackgroundTileData |
$F847–$FFF6 | 1,968 bytes | Background tile data |
All data regions now have descriptive labels; the byte-identity audit trail is preserved.
Power-On Flow Analysis
Reset ($C000) → sei/cld → PPU init (sta PPUCTRL #$00) → stack init (ldx #$FF/txs) → WaitVBlank (poll PPUSTATUS for vblank) → ClearZeropage (clear $0000–$00FF via (Zp00),y) → InitGameState → InitVariables → MainGameLoop_1 → NMI synchronization → GameModeDispatcher dispatch loop.
NMI ($C086) → saves registers → PPU status read → sprite DMA via OAMDMA → scroll update (PPUSCROLL) → PPU mask/ctrl updates → GameModeDispatcher dispatch → restores registers → rti.
IRQ ($C000) → redirects to RESET (unused placeholder).
Main Game Loop → MainGameLoop_1 / MainGameLoop_3 → calls GameModeDispatcher which dispatches to game mode handlers → loops.
Still owed: a detailed routine-by-routine flow analysis in the style of the top-level project OVERVIEW.mds.