Context
createGame() is already independent of DOM and canvas, which is the strongest boundary in the codebase. The presentation path is less clear: main.js owns a live game, stage.js mirrors a board state and handles all scenes, and juice.js interprets settled events for animation. stage.js is 1,526 lines, main.js uses dense inline lifecycle handlers, and the engine repeatedly filters or searches objects during each movement step. The designer and world authors work in parallel; their owned files and authored behavior must remain stable.
Goals / Non-Goals
Goals:
- Keep one authoritative simulation state in the game session; treat stage board data as a presentation projection.
- Route stage commands through a single action vocabulary regardless of device.
- Extract focused modules at existing seams while preserving
createStage(),createGame(), mod, share, and browser debug APIs. - Reduce repeated object/rule scans in turn hot paths without changing deterministic order.
- Make data-only copying semantics consistent for manifests, levels, and share payloads.
- Establish measured gates for future stateful mechanics.
Non-Goals:
- New gameplay vocabulary or gates, switches, and crumble behavior in this change.
- A new rendering framework, rewrite of the stage design, or edits to designer-owned visuals.
- A different automata conflict policy; current turn ordering remains until its own spec change.
- Running untrusted remote CommonJS code. Local module execution remains explicitly trusted.
Decisions
Preserve the public façades
createGame() and createStage() remain the entry points. Internals can move into src/, src/, and src/ without forcing all callers or mods to migrate. A large public API rewrite would increase regression risk without helping near-term world authoring.
Separate intent, simulation, and presentation
Inputs produce named actions. A stage dispatcher maps an action in the current scene to one callback; the application session accepts game commands and updates the single engine instance; rendering consumes snapshots plus animation frames. This removes three similar but drifting keyboard, pointer, and programmatic command paths. Renderer-returned hit regions are the eventual source for pointer targets.
Keep turn semantics stable while indexing
The current player → rule refresh → automata → rule refresh → contacts pipeline and event shape are the compatibility contract. Turn-local maps by ID/cell/subject can replace repeated scans, but indexes must be invalidated or rebuilt after movement, transformation, and destruction. Simultaneous automata resolution is a separate behavior decision and requires a dedicated OpenSpec change.
Treat snapshots as the extension boundary
All future stateful mechanics need normalized data, deterministic turn phase, snapshot/restore fields, solver state keys, and presentation events before campaign use. The existing automata phase shows why this must be planned before adding gates or one-step tiles. A versioned mechanics envelope is a future design step, not a silent addition in this refactor.
Make cleanup incremental and reviewable
Each commit owns one seam: architecture contract, stage dispatch, app session, engine indexing, and data copy semantics. Existing browser and Node gates verify behavior between seams. A single source-tree reshuffle would obscure behavior changes and collide with parallel level/design work.
Risks / Trade-offs
- [Risk] Moving input routing changes a key or pointer action twice or not at all. → Conformance tests and browser flows check one action per accepted input, Pause/Index navigation, and mobile controls.
- [Risk] Cached engine indexes become stale after a push or transformation. → Keep indexes turn-local with explicit rebuild points and compare settled state/events against existing fixtures.
- [Risk] Pure JSON clones can alter values outside the data contract. → Limit the shared helper to already validated level/manifest/share data and keep existing error behavior.
- [Risk] Another team changes visual files during refactor. → This change does not edit
docs/, world files, or their active OpenSpec changes.design.* - [Risk] The first extraction does not make every large file small. → Track remaining hotspots, line counts, owners, and migration order in the architecture review.
Migration Plan
- Record baseline module sizes, ownership, dependencies, and behavior gates.
- Extract action dispatch and app/session seams behind existing façades.
- Introduce engine lookup helpers and shared data cloning without changing serialized state.
- Run Node, build, and browser controls/rules/mod/Undo checks; inspect rendered frames and console.
- Follow with separate OpenSpec changes for scene renderers, snapshot schema/mechanics, and automata conflict policy as needed.
Open Questions
- What simultaneous automata conflict policy should apply before larger world packs?
- Should passive pushing turn every directional actor, or only actors configured for that behavior?
- Which gates/switch/crumble effects run before contact resolution? World-authored scenarios must answer this before schema changes.