This project documents the complete reverse engineering of Wizard of Wor for the Atari 800, from an ATR disk image to a byte-identical reassemblable disassembly.
COMPLETE The game has been fully disassembled and verified to produce byte-identical output when reassembled with ca65/ld65.
Title Screen
High Scores
Monster Guide & Point Values
Gameplay - The Dungeon Maze
The original ATR disk image (92,176 bytes) contains a custom boot loader and the game binary:
The boot loader at $0600 performs the following operations:
| Address | Size | Description |
|---|---|---|
| $0600-$077F | 384 bytes | Boot loader code (overwritten after boot) |
| $1000-$167F | 1,664 bytes | Graphics / character data |
| $1680-$6A7F | 21,504 bytes | Main game code |
| $7800-$79FF | 512 bytes | Custom character sets |
| $7F15-$7F26 | 18 bytes | Score display buffer |
| Address | Name | Purpose |
|---|---|---|
| $B9 | busy | Busy/wait flag |
| $E0 | prev_state | Previous game state |
| $E1 | game_active | Game active flag |
| $E3 | game_phase | Current game phase |
| $81 | lives | Lives remaining |
| Address | Name | Purpose |
|---|---|---|
| $067C | game_state | Main state machine value |
| $069A | dungeon | Current dungeon level (1-N) |
| $069E-$06A3 | p1_score | Player 1 score (6 BCD bytes) |
| $06A4-$06A9 | p2_score | Player 2 score (6 BCD bytes) |
| $06AB | p2_flag | Two-player mode flag |
| $06DA | disp_mode | Display mode (1=title, 2=game, etc) |
Data extracted from the monster table at $3C20:
The score system uses BCD (Binary Coded Decimal) arithmetic for proper decimal handling on the 6502 processor.
| Address | Purpose |
|---|---|
| $069E-$06A3 | Player 1 score (6 BCD bytes = 6 digits max 999,999) |
| $06A4-$06A9 | Player 2 score (6 BCD bytes) |
| $7F15-$7F1A | Player 1 score display buffer |
| $7F1B-$7F20 | Player 2 score display buffer |
; add_score: Add points to player score using BCD mode ; Entry: Points to add stored at $069E-$06A3 L3489: LDA #$00 STA $06A4 ; Clear carry accumulator SED ; *** SET DECIMAL MODE (BCD) *** LDA $06AB ; Check two-player flag BEQ L3498 ; Branch if player 1 LDX #$11 ; Player 2: display offset = 17 BNE L349A ; Always branch L3498: LDX #$05 ; Player 1: display offset = 5 L349A: LDY #$05 ; Y = 5 (process 6 digits) ; Main addition loop (6 iterations for 6-digit score) L349C: CLC LDA $7F15,X ; Load current display digit AND #$0F ; Mask to get BCD value (0-9) ADC $06A4 ; Add carry from previous digit ADC $069E,Y ; Add points digit PHA ; Save result AND #$10 ; Check for BCD overflow BEQ L34AF ; No overflow, skip LDA #$01 ; Overflow: set carry for next L34AF: STA $06A4 ; Store carry PLA ; Restore result ORA #$10 ; Set display attribute (visible) STA $7F15,X ; Store to score display DEX ; Next display position DEY ; Next points digit BPL L349C ; Loop for all 6 digits CLD ; *** CLEAR DECIMAL MODE ***
Key Observations:
The entry point at $1AF2 implements a state machine based on the value in $067C:
; main_loop: Entry point - game state machine ; Called after boot loader transfers control L1AF2: BEQ L1AF5 ; If Z flag set, continue L1AF4: RTS ; Otherwise return L1AF5: LDA $B9 ; Load busy flag BNE L1AF4 ; If busy, return (wait) LDA $067C ; Load current game state CMP $E0 ; Compare with previous state BEQ L1B29 ; Same state, skip transition ; State transition code... STA $E0 ; Update previous state ; ... dispatch to state handlers
| Value | State | Description |
|---|---|---|
| $00 | TITLE | Title screen / attract mode |
| $01 | READY | "Get Ready" / level intro |
| $02 | PLAY | Active gameplay |
| $03 | DEATH | Player death sequence |
| $04 | CLEAR | Level cleared |
| $05 | GAMEOVER | Game over sequence |
| Address | String |
|---|---|
| $4CE9 | "DOUBLE" |
| $4E6B | "DOUBLESCORE" |
| $4E77 | "GAME" |
| $4E7B | "OVER" |
| $4CF5 | "DUNGEON" |
Monster names found in data: BURWOR, GARWOR, THORWOR
Available space for modifications:
| Address | Size | Contents |
|---|---|---|
| $13DD-$1407 | 43 bytes | $00 fill (in graphics area) |
| $67CB-$67FE | 52 bytes | $00 fill |
| $6800-$6913 | 276 bytes | $00 fill |
Total usable free space: ~328 bytes at $67CB-$6913
Sector 4 is unused (128 bytes, $DA/$86 filler). The boot loader skips it.
Sector 720 High scores are saved here! The game has disk I/O routines at $1F29-$1F71 that read/write sector 720 via DSKINV ($E453).
| Tool | Purpose |
|---|---|
| Python 3 | Custom extraction and disassembly scripts |
| ca65 (cc65 suite) | 6502 assembler |
| ld65 (cc65 suite) | 6502 linker |
| atari800 | Emulator for testing |
| hexdump / dd | Binary analysis |
| Tool | Issue |
|---|---|
| atrcopy | numpy.alen removed in newer numpy |
| mkatr | Only handles DOS filesystems, not boot disks |
| atari_8bit_utils disasm | macOS compatibility issues, be16toh undefined |
.byte directives with disassembly in comments to guarantee identical reassemblymd5 checksum comparison is essential# With Altirra OS (no real ROMs needed) atari800 -ai -xl -xl-rev altirra -nobasic wowor_rebuilt.atr # With real Atari OS ROMs atari800 -xl wowor.xex
The disassembly has been verified byte-identical to the original:
$ md5 wowor_full.bin wowor_rebuild_trimmed.bin MD5 (wowor_full.bin) = f17911c9a28021c946a129696c073176 MD5 (wowor_rebuild_trimmed.bin) = f17911c9a28021c946a129696c073176 MATCH - Byte-identical!
VERIFIED Both files produce identical MD5 checksums, confirming a perfect disassembly.