Skip to content

Split engine.rs into modules (pure move, stacked on #31) - #32

Merged
shpwrck merged 2 commits into
mainfrom
claude/engine-module-split
Sep 25, 2026
Merged

shpwrck merged 2 commits into
mainfrom
claude/engine-module-split

Conversation

@shpwrck

@shpwrck shpwrck commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Stacked on #31. Merge this after #31 lands; its base is #31's branch (claude/codequest-opus-improvements-0069d2 at 9a3bd58, which includes the review-3 fixes).

What this does

src-tauri/src/engine.rs (14,714 lines) becomes the directory module src-tauri/src/engine/, one file per concern. This is a pure move with no behavior change:

  • Every item keeps its body, token for token. The only added tokens are mod/use lines, visibility qualifiers, and one cfg attribute on runtime.rs's test module (see Platform-gated tests). Private items and fields that now cross a file boundary become pub(super), or pub(in crate::engine) under render/. Each file starts with use super::*.
  • The only changed tokens are the include_bytes!/include_str! paths, which gain one ../ per directory level.
  • engine/mod.rs re-exports everything the rest of the crate uses (GameEngine, EngineRuntime, Button, CartridgeSpec, QuizQuestion, ...), so no crate::engine:: path outside the engine changes. engine/transcript.rs and engine/bench.rs are byte-identical.
  • The three node contracts that read engine.rs now read all the engine files through the new tests/engine-source.mjs (the production-only check still stops each file at its #[cfg(test)] mod tests).
  • The README module map, and in a separate commit docs/architecture.md, now point at engine/.

Module table

Module Lines Tests Contents
mod.rs 101 0 imports, frame constants, module list, re-exports
text.rs 91 2 wrap_text, text_width, truncate, title_lines
palette.rs 28 0 Color and the named colors
assets.rs 115 1 embedded sprite and scene-plate bytes
framebuffer.rs 492 2 UiBox, Framebuffer drawing primitives, the plate cache
cartridge.rs 123 1 CartridgeSpec, QuizQuestion, QuestSpec, loader types
progress.rs 160 1 thresholds, InsightStage, PresentationTier, streak scoring
oracle.rs 351 4 datafall drops, Oracle line, recall, failure reasons
lesson.rs 446 2 lesson card layout and copy
state.rs 1033 11 Screen, OpeningBeat, GameState and its core impl
codex.rs 165 2 GameState's journal, mastery and Codex page methods
quiz.rs 1510 25 QuizRun, RunLedger, retry scheduling, answer commit
deck.rs 828 16 question batches, prefetch, rebatching
commands.rs 595 11 EngineCommand, EngineEffect, apply_commands
input.rs 562 8 Button, handle_press, hero setup controls
advance.rs 472 9 advance_game, the per-tick step
audio_snapshot.rs 474 9 audio_snapshot, direct_audio
quest_process.rs 125 0 quest shell process and polling
runtime.rs 365 2 GameEngine, EngineRuntime (Bevy app), handle_effect and its quest-process tests
render/mod.rs 784 2 the render system and the draw_screen dispatch
render/widgets.rs 196 1 runes, meters, wards, focus frames
render/sprites.rs 265 1 hero, crab, boss, sigils, drops
render/opening.rs 435 4 boot, chronicle, awakening, title, fanfare
render/menu.rs 213 2 quiz menu, gateway menu
render/atelier.rs 230 2 character creation
render/datafall.rs 788 7 Oracle sanctum and datafall HUD
render/trial.rs 792 6 quiz trial screen and feedback banners
render/ascension.rs 340 3 level-up screen
render/aftermath.rs 576 4 game over and debrief rows
render/codex.rs 1497 10 Codex lens, lesson and self-test pages
render/quest.rs 65 0 quest select, battle, result
test_support.rs 891 0 shared test fixtures and drivers (cfg(test))
transcript.rs 2084 23 unchanged
bench.rs 715 4 unchanged (the release tick benchmark, cfg(test))

How it was made and verified

The split was generated by a deterministic script, not written by hand. The script moves top-level items, inherent-impl methods, and the #[test] functions of mod tests by name from explicit placement lists. It fails on a listed name that is missing, and on any item it does not know. It was first proven on 320ed4e, re-run on ea032da, and has now been re-run on 9a3bd58. None of the output was edited by hand. The script is not committed: once this merges, engine.rs no longer exists, so there is nothing left for it to re-run on.

For 9a3bd58, the placement lists gained the items the later merges added: 3 constants, the 6 plate-cache items and draw_screen from the engine-performance stream, and 20 tests. The two quest-process tests also moved from quest_process.rs to runtime.rs (see below). The script's logic needed four small changes:

  • The scanner treats an item-position macro invocation (thread_local! { ... }) as one item that ends at its closing brace. Before this change it classified the item by the static inside and read past the brace.
  • The usage analysis reads every existing child module of the engine, not only transcript.rs. The new engine/bench.rs is declared #[cfg(test)], so everything it uses counts as test use.
  • render/mod.rs's own items are imported into engine/mod.rs by name only where something outside render/ uses them: plainly for production use, and under #[cfg(test)] for test-only use. That applies to draw_screen, which outside render/ only bench.rs calls.
  • A test module whose tests all carry their own #[cfg(..)] gets the union of those predicates on the module itself.

With the ea032da placement lists, the changed script writes the same output on ea032da as before, except for one line: the #[cfg(unix)] that the last change adds to quest_process.rs's test module. So the ea032da split had the same Windows problem.

  • Token multiset. The script checks that the original token multiset (comments included) is preserved, apart from the added tokens listed above and the re-rooted include paths. A separate checker that shares no code with the script agrees, both on the raw output and after cargo fmt: no token was lost apart from the 25 include paths, which reappear with one more ../.
  • Name inventory. The same checker finds 808 declared names before and none lost after. 148 #[test] functions before and 148 after.
  • Identical test count. cargo test --lib -- --list lists 411 tests before and after, with identical leaf names (only the module paths change). The results are 400 passed and 11 ignored, the same as the unsplit tree at 9a3bd58.
  • Determinism. Two fresh runs write identical trees, and rustfmt of a fresh run matches the committed files.
  • CI. Run on the committed tree in the WSL mirror: cargo fmt --check, cargo clippy --all-targets -- -D warnings, cargo test, npm test (node contracts), and the CODEQUEST.toml validator for both manifests all pass.

Platform-gated tests

The previous head of this PR failed Rust on windows-latest with two unused imports: use super::*; in quest_process.rs's test module, and #[cfg(test)] use self::runtime::*; in engine/mod.rs. Both were used only by the two quest-process tests, and Windows compiles both tests out.

The engine's only platform-gated items are those two tests: aborting_a_quest_stops_its_whole_process_tree_and_frees_the_slot (#[cfg(unix)]) and a_surviving_quest_helper_forwards_nothing_after_the_quest_is_done (#[cfg(target_os = "linux")]). I checked this by searching engine.rs at 9a3bd58, bench.rs, and transcript.rs for every cfg(, cfg_attr( and cfg!(. The only other hit is a cfg!(debug_assertions) expression in bench.rs, which compiles on every target.

Both tests call handle_effect directly, so they now live in runtime.rs next to it. That module's test module is #[cfg(test)] #[cfg(any(unix, target_os = "linux"))]. With the tests there, no other module needs runtime's private names, so engine/mod.rs has no use self::runtime::*;.

Every import the script emits, checked against the platform gates:

  • The root production globs (use self::<module>::*;), the explicit use self::render::render;, and each module's file-level use super::*; are all production imports. No engine production code is platform-gated, so each one is used on every target. No module has a test-only file-level use super::*;.
  • The root's two test-only imports are #[cfg(test)] use self::render::draw_screen; and #[cfg(test)] use self::test_support::*;. bench.rs uses draw_screen in ungated code, and every module's tests use test_support.
  • Each test module's use super::*; is used by at least one ungated test in that module. The exception is runtime.rs, whose test module is compiled only where one of its tests is.

I can't compile for Windows locally, so I simulated the Windows and macOS builds on Linux. In a scratch export of 9a3bd58, I turned the gated tests off with #[cfg(any())] before splitting: both tests for Windows, and only the Linux one for macOS. I then ran the script and cargo clippy --all-targets -- -D warnings. Both simulations pass.

Review tips

The move commit deletes one file, adds 33 (32 engine files and tests/engine-source.mjs), and edits four (README and three node contracts). To check a module, open it next to the same items in engine.rs at 9a3bd58. Only visibility and imports should differ, plus the one cfg attribute on runtime.rs's test module. The native Windows/macOS jobs (verify-native) are the check to watch on this PR.

🤖 Generated with Claude Code

@shpwrck
shpwrck force-pushed the claude/engine-module-split branch 2 times, most recently from 095a9bf to 3e0eedc Compare September 25, 2026 17:44
shpwrck and others added 2 commits September 25, 2026 15:30
src-tauri/src/engine.rs had grown to 14.7k lines. It is now the directory
module src-tauri/src/engine/, one file per concern. engine/mod.rs keeps the
shared imports, WIDTH/HEIGHT/FRAME_BYTES/NATIVE_RGB_BYTES, the module list
and the crate-facing re-exports, so every path the rest of the crate uses
(crate::engine::GameEngine, EngineRuntime, Button, CartridgeSpec, ...) is
unchanged. engine/transcript.rs and engine/bench.rs are untouched.

The split was generated by a script (split_engine.py), not by hand. It moves
top-level items and the tests of `mod tests` by name, fails on any item it
does not know, and checks that the token multiset is preserved except for
the use/mod/visibility tokens it adds and the include_bytes!/include_str!
paths that gain one `../` per directory level. An independent checker agreed:
no token lost apart from those 25 paths, no declared name lost, 148 #[test]
functions before and after. Private items and fields that now cross a file
boundary are pub(super) (pub(in crate::engine) under render/), and each
file starts with `use super::*`, so name resolution is what it was. The
plate cache's thread_local! stays private to framebuffer.rs, and draw_screen,
which outside render/ only the test-only engine/bench.rs calls, is imported
into engine/mod.rs under cfg(test).

The engine's only platform-gated items are two quest-process tests
(cfg(unix) and cfg(target_os = "linux")). They sit in runtime.rs next to
handle_effect, which they drive, and runtime.rs's test module carries
cfg(any(unix, target_os = "linux")), the one added token that is not an
import or a visibility. Every other emitted import is used by code that
compiles on every target, so none goes unused on Windows or macOS.

The node contracts that read engine.rs now read every engine file through
tests/engine-source.mjs, and the README module map points at engine/.

| Module | Lines | Tests | Contents |
|---|---:|---:|---|
| mod.rs | 101 | 0 | imports, frame constants, module list, re-exports |
| text.rs | 91 | 2 | wrap_text, text_width, truncate, title_lines |
| palette.rs | 28 | 0 | Color and the named colors |
| assets.rs | 115 | 1 | embedded sprite and scene-plate bytes |
| framebuffer.rs | 492 | 2 | UiBox, Framebuffer drawing primitives, the plate cache |
| cartridge.rs | 123 | 1 | CartridgeSpec, QuizQuestion, QuestSpec, loader types |
| progress.rs | 160 | 1 | thresholds, InsightStage, PresentationTier, streak scoring |
| oracle.rs | 351 | 4 | datafall drops, oracle line, recall, failure reasons |
| lesson.rs | 446 | 2 | lesson card layout and copy |
| state.rs | 1033 | 11 | Screen, OpeningBeat, GameState and its core impl |
| codex.rs | 165 | 2 | GameState's journal, mastery and Codex page methods |
| quiz.rs | 1510 | 25 | QuizRun, RunLedger, retry scheduling, answer commit |
| deck.rs | 828 | 16 | question batches, prefetch, rebatching |
| commands.rs | 595 | 11 | EngineCommand, EngineEffect, apply_commands |
| input.rs | 562 | 8 | Button, handle_press, hero setup controls |
| advance.rs | 472 | 9 | advance_game, the per-tick step |
| audio_snapshot.rs | 474 | 9 | audio_snapshot, direct_audio |
| quest_process.rs | 125 | 0 | quest shell process and polling |
| runtime.rs | 365 | 2 | GameEngine, EngineRuntime, handle_effect and its quest tests |
| render/mod.rs | 784 | 2 | render system and draw_screen dispatch |
| render/widgets.rs | 196 | 1 | runes, meters, wards, focus frames |
| render/sprites.rs | 265 | 1 | hero, crab, boss, sigils, drops |
| render/opening.rs | 435 | 4 | boot, chronicle, awakening, title, fanfare |
| render/menu.rs | 213 | 2 | quiz menu, gateway menu |
| render/atelier.rs | 230 | 2 | character creation |
| render/datafall.rs | 788 | 7 | Oracle sanctum and datafall HUD |
| render/trial.rs | 792 | 6 | quiz trial screen and feedback banners |
| render/ascension.rs | 340 | 3 | level-up screen |
| render/aftermath.rs | 576 | 4 | game over and debrief rows |
| render/codex.rs | 1497 | 10 | Codex lens, lesson and self-test pages |
| render/quest.rs | 65 | 0 | quest select, battle, result |
| test_support.rs | 891 | 0 | shared test fixtures and drivers (cfg(test)) |

Verified on the WSL mirror: cargo fmt --check, clippy --all-targets -D
warnings, cargo test (411 lib tests listed before and after with identical
leaf names; 400 passed, 11 ignored, as on the unsplit tree), npm test, and
both CODEQUEST.toml validations all pass. No behavior change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The guide's module map, its note on line numbers, and its list of where the
Rust tests live still named engine.rs. They now name engine/ and the files
that own each part.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@shpwrck
shpwrck force-pushed the claude/engine-module-split branch from 3e0eedc to 8e66049 Compare September 25, 2026 19:31
@shpwrck
shpwrck changed the base branch from claude/codequest-opus-improvements-0069d2 to main September 25, 2026 19:31
@shpwrck shpwrck closed this Sep 25, 2026
@shpwrck shpwrck reopened this Sep 25, 2026
@shpwrck
shpwrck marked this pull request as ready for review September 25, 2026 19:51
@shpwrck
shpwrck merged commit 5bb5c91 into main Sep 25, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant