Files
daedalian/README.md
2026-10-31 18:12:15 -05:00

81 lines
3.9 KiB
Markdown

# Daedalian Opus, ported to Rust
A rewrite of the Game Boy game as ordinary Rust, translated by hand from the
disassembly in `../disassembly/`. Unlike `../rust/` (a static recompilation that
still runs the original instructions against emulated registers and memory),
this is the game itself: the screens, the puzzle rules, the map, the story and
the music sequencer are Rust functions over named state, so they can be read
and changed.
```
cargo run --release # looks for "Daedalian Opus (USA).gb" here or in ..
cargo run --release -- --rom PATH
```
Controls: arrows = D-pad, Z = A, X = B, Enter = Start, Right Shift / Backspace = Select, Esc = quit.
## What comes from where
The program contains no game content. At startup `rom.rs` parses the ROM
into typed data: levels (`Level`: starting pieces, board layout, cursor
start, music, pieces needed, which piece drops on the map), piece shapes,
screen layouts (tilemap draw scripts), story text, passwords, tile graphics
and the music byte code. Small numbers that are really code (coordinates,
tile numbers for digits, timings) live in the Rust source as named constants.
## Layout
| file | what |
|---|---|
| `src/game/mod.rs` | `Game` state, the top-level flow (title → story → map → puzzle → ...), the per-frame tasks, the clear-time clock |
| `src/game/puzzle.rs` | the puzzle: picking up, moving, rotating, flipping and placing pieces; the win check; reset; the level-clear screen |
| `src/game/map.rs` | the map between levels: walking, castle doors, the bridge, the password bird, the piece that drops from the sky |
| `src/game/menus.rs` | title, how to play, password entry, puzzle list |
| `src/game/story.rs` | story screens and the ending |
| `src/sound.rs` | the game's sound driver: an 8-channel byte-code sequencer (format documented at the top) |
| `src/apu.rs` | the Game Boy sound chip, as the instrument the driver plays |
| `src/video.rs` | tile graphics, two background maps, 40 sprites, palettes, and the renderer |
| `src/rom.rs` | ROM loading and parsing |
| `src/input.rs` | buttons, with the game's d-pad auto-repeat |
| `src/main.rs` | window (`winit` + `pixels`), audio (`cpal`), frame pacing |
| `src/headless.rs` | scripted runs for testing |
The game code is written the way the original is structured: straight-line
code that calls `wait_frame()` whenever it needs the next frame (to animate,
or to wait for a button). It runs on its own thread; `wait_frame` sends the
finished picture and sound to the host and blocks until the host sends the
next frame's buttons.
The display model (tiles, maps, sprites) is kept because it's what the art is
made of, so the game still draws by placing tiles and sprites. Map addresses
like `0x9800` remain the way cells are named, since the screen layout scripts
in the ROM use them.
## Faithfulness
It's checked against the recompiled build in `../rust/` with the headless
runner. Same inputs give the same screens (title, every menu, story pages,
map walks, levels 1-3 and 36 solved, the ending), and the sound driver's chip
register writes match frame for frame. Known differences:
- Loading a screen takes no time here, where the original spent a few frames
copying graphics with the display off, so the port runs a few frames ahead.
- The original queued tile updates to the next vblank; here they happen
immediately, so some changes appear one frame sooner.
- The original's unused "resume music" sound (id $96) points at garbage; it
just resumes.
Original quirks are kept on purpose and marked `(sic)` in the source. For
example, undoing a Select flip that doesn't fit uses Start's rule.
## Headless testing
```
cargo run --release -- --headless OUT_DIR 120 start:2 90 shot:title probe goto:44:54
```
Tokens: `N` run N frames; `BUTTON:N` hold a button for N frames (up down left
right a b start select); `shot:NAME` save a PNG; `probe` print the level and
cursor; `goto:YY:XX` steer the puzzle cursor (hex sprite coordinates). Audio
goes to `OUT_DIR/audio.wav`.