Rewrite groundwork as ExecPlan

This commit is contained in:
2026-04-16 10:29:09 +02:00
parent 0b53ab9133
commit 6f35cba659

View File

@@ -1,136 +1,275 @@
# Side Scroller Shooter Groundwork # Build the deterministic simulation foundation for the side scroller shooter
## Goal This ExecPlan is a living document. The sections `Progress`, `Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must be kept up to date as work proceeds.
This document defines the technical foundation for a classic 2D side scroller shooter built with Godot 4 and C#. The framework must support the whole genre family, not a single game design: player movement, weapons, enemies, bosses, pickups, scrolling levels, scripted encounters, layered music, overlapping sound effects, and strong debugging tools. Final game-specific rules and content will be built later on top of this groundwork.
The non-negotiable constraint is that **all authoritative gameplay simulation lives in one pure .NET project with no Godot dependency**. Godot is the host for rendering, input capture, authoring UX, audio playback, and debug tooling. The same simulation assembly is referenced by both xUnit tests and the Godot host so all gameplay logic can be verified without engine or multimedia interference. `PLANS.md` is checked into the repository root at `PLANS.md`. This document must be maintained in accordance with `PLANS.md`.
## Hard Constraints ## Purpose / Big Picture
1. **Single pure simulation project**. Do not split runtime gameplay and content definitions into separate assemblies unless a real dependency problem appears later.
2. **Deterministic fixed-step simulation**. The same content, seed, and input action stream must produce the same state and hashes every run.
3. **Serializable input actions**. The simulation consumes serializable action batches so recorded runs can be replayed exactly.
4. **Serializable full state**. The entire simulation state must be serializable for save/load, replay checkpoints, and desync verification.
5. **Thin Godot host**. Godot must not become gameplay authority for physics, damage, AI, triggers, or progression.
6. **xUnit-first verification**. The simulation project is expected to reach 100% gameplay coverage; exclusions are acceptable only for clearly non-logic code such as generated tables or serializer boilerplate.
## Repository Layout After this work, the repository will support a classic 2D side scroller shooter whose gameplay rules live in a pure .NET simulation instead of inside Godot scene logic. A developer will be able to run deterministic simulation tests without starting Godot, record and replay exact input streams, save and reload full simulation state, and use Godot only as the host for rendering, audio, input capture, and authoring tools. The change is visible when `dotnet test` can prove gameplay behavior, and when the Godot host can step, pause, fast-forward, and replay the same simulation state hashes for the same seed and inputs.
The Godot project should **not** live at repository root. With SDK-style .NET projects, a root-level Godot `.csproj` will glob in files from subfolders, which makes it too easy to accidentally compile tests and simulation sources into the Godot assembly.
Recommended layout: The user-visible outcome is not merely “new projects were added.” The outcome is that the same content, seed, and action stream produce the same gameplay results in both automated tests and the running Godot host. A novice should be able to follow this plan from an empty understanding of the repository and reach that observable result.
```text ## Progress
/SideScrollerGame.sln
/src/SideScrollerGame.Sim/
SideScrollerGame.Sim.csproj
/FixPoint/ # preferred final home
/Definitions/
/Runtime/
/Systems/
/Serialization/
/Replay/
/Verification/
/tests/SideScrollerGame.Sim.Tests/
SideScrollerGame.Sim.Tests.csproj
/godot/
project.godot
SideScrollerGame.Godot.csproj
/.godot/
/scenes/
/scripts/host/
/assets/
/addons/
/content/compiled/
/tools/
```
Current repository note: - [x] (2026-04-16 08:24Z) Reviewed `PLANS.md`, the original `groundwork.md`, `SideScrollerGame.sln`, and `godot/SideScrollerGame.Godot.csproj` to anchor this ExecPlan to the current repository state.
- The existing root `FixPoint/` folder is acceptable during bootstrap. - [x] (2026-04-16 08:32Z) Rewrote `groundwork.md` as a self-contained ExecPlan with milestones, repository orientation, exact commands, validation guidance, and living-document bookkeeping.
- The preferred end state is to move it under `src/SideScrollerGame.Sim/FixPoint/` and adjust namespaces once the simulation project is created. - [ ] Create `src/SideScrollerGame.Sim/SideScrollerGame.Sim.csproj` and `tests/SideScrollerGame.Sim.Tests/SideScrollerGame.Sim.Tests.csproj`, add them to `SideScrollerGame.sln`, and wire both projects to the simulation assembly.
- The Godot project should be moved under `/godot` before the real solution structure grows. - [ ] Copy the current root `FixPoint/*.cs` files into `src/SideScrollerGame.Sim/FixPoint/`, update namespaces, and make the simulation project the only assembly that compiles deterministic math code.
- [ ] Implement the first runnable simulation spine: `Simulation`, `SimulationState`, `WorldSnapshot`, `TickActionBatch`, deterministic random number generation, and per-tick hashes.
- [ ] Implement versioned save/load, replay recording, replay playback, and the optional round-trip verification modes.
- [ ] Implement deterministic movement, collision, damage resolution, triggers, and the fixed tick pipeline with exhaustive simulation tests.
- [ ] Implement data-driven definitions for heroes, enemies, weapons, projectiles, pickups, modifiers, squads, encounters, and level runtime data.
- [ ] Implement Godot host adapters for input translation, fixed-step execution, interpolation, presentation mapping, sound playback, music transitions, and debug transport controls.
- [ ] Add compile-time authoring flow from Godot scenes into engine-agnostic runtime content under `content/compiled/`.
- [ ] Run the full validation set, update this plan with final outcomes, and keep the plan aligned with the implementation state at every stopping point.
Dependency rules: ## Surprises & Discoveries
```text - Observation: the repository has already moved the Godot project under `godot/`, so the restructuring work should preserve that host location instead of planning another move.
Godot Host -> SideScrollerGame.Sim Evidence: `SideScrollerGame.sln` currently references `godot/SideScrollerGame.Godot.csproj`.
Tests -> SideScrollerGame.Sim - Observation: the only deterministic math code in the repository today is the root `FixPoint/` directory, and there is no simulation project yet.
Sim -> no Godot references Evidence: `rg --files` lists `FixPoint/*.cs`, `godot/SideScrollerGame.Godot.csproj`, `SideScrollerGame.sln`, `PLANS.md`, and `groundwork.md`, but no `src/` or `tests/` projects.
``` - Observation: the repository root contains `godot.cmd`, so all Godot commands in this plan should call that wrapper with `--path .\godot`.
Evidence: `rg --files` lists both `godot.cmd` and `godot/project.godot`.
Within the simulation project, separate concerns by namespaces and folders, not by extra assemblies: ## Decision Log
- `SideScrollerGame.Sim.Math`
- `SideScrollerGame.Sim.Definitions`
- `SideScrollerGame.Sim.Runtime`
- `SideScrollerGame.Sim.Systems`
- `SideScrollerGame.Sim.Serialization`
- `SideScrollerGame.Sim.Replay`
- `SideScrollerGame.Sim.Verification`
## Assembly Responsibilities - Decision: preserve the original seven-phase technical direction and translate it into milestone-based execution rather than replacing the architecture outright.
### `SideScrollerGame.Sim` Rationale: the original groundwork already captured the correct constraints and domain boundaries; rewriting it as an ExecPlan reduces design drift while making the work executable.
- fixed-step world simulation Date/Author: 2026-04-16 / Codex
- immutable gameplay and level definitions - Decision: migrate deterministic math into the simulation project by copying `FixPoint/*.cs` into `src/SideScrollerGame.Sim/FixPoint/` first, then retire the root copies only when safe.
- deterministic numeric types and RNG Rationale: additive migration is safer and more idempotent in this Windows environment, where deletion is restricted and recovery should not depend on rollback commands.
- physics and collision Date/Author: 2026-04-16 / Codex
- AI and behavior execution - Decision: keep all authoritative gameplay logic and gameplay definitions inside one pure project named `SideScrollerGame.Sim` until a concrete dependency problem appears.
- input action ingestion Rationale: one simulation assembly is the clearest way to guarantee deterministic tests, shared replay behavior, and a thin Godot host.
- save/load serialization Date/Author: 2026-04-16 / Codex
- replay support - Decision: use `godot.cmd` in all concrete steps instead of the shorthand `.\godot`.
- debug hashes and verification hooks Rationale: `godot.cmd` is the wrapper that actually exists in this checkout, so the plan must describe commands that a novice can run without guessing aliases.
Date/Author: 2026-04-16 / Codex
### `SideScrollerGame.Godot` ## Outcomes & Retrospective
- editor bootstrapping
- converting physical input into simulation actions
- fixed-step runner and interpolation
- rendering and presentation node lifecycle
- animation mapping
- audio playback, music queue, and cross-fading
- authoring and validation tools
- development-time timeline controls
## Simulation Boundaries At the moment, the outcome is a corrected planning artifact rather than finished gameplay infrastructure. The original groundwork note has been converted into a proper ExecPlan that a future contributor can execute step by step. No simulation code, tests, or host adapters have been implemented yet, so the gap between purpose and delivered software remains the actual implementation work described below.
The simulation owns:
- world state
- movement and collision
- weapon firing and ammo
- enemy behavior
- spawn logic
- damage, death, score, drops, checkpoints
- scripted triggers
- camera gameplay rules
- music state requests
The Godot host owns: The main lesson from this rewrite is that the repository already contains enough concrete structure to support a precise plan. The plan therefore names real files, current commands, and migration-safe steps instead of speaking in abstract architecture terms.
- drawing sprites, meshes, parallax, particles, and UI
- collecting OS input and converting it to actions
- audio stream playback and mixing
- interpolation between snapshots
- editor tooling and authoring UX
- developer overlays and transport controls
Godot physics, animation trees, collision layers, and timers must never become authoritative for gameplay. ## Context and Orientation
## Deterministic Simulation Model This repository is currently a small Godot 4 .NET project with one solution file, one Godot C# project, and a root folder of fixed-point math helpers. `SideScrollerGame.sln` exists at the repository root and currently includes only `godot/SideScrollerGame.Godot.csproj`. `godot/project.godot` is the Godot project entry point. `godot/SideScrollerGame.Godot.csproj` targets .NET 8 and uses `Godot.NET.Sdk/4.5.1`. The root `FixPoint/` directory contains deterministic numeric helper types such as `FixPoint16.cs`, `FixPointVector2.cs`, and `IntRandom.cs`, but those files are not yet owned by a simulation project. There is no `src/` directory, no `tests/` directory, and no pure gameplay assembly.
Use a fixed rate of **60 simulation ticks per second**. This is appropriate for classic side scroller shooters and is easy to reason about. Rendering can run faster or slower; simulation must not.
Determinism rules: Several terms matter in this plan and are defined here in plain language. “Authoritative gameplay simulation” means the code that decides what is true in the game world: positions, collisions, damage, enemy behavior, triggers, checkpoints, and progression. “Deterministic” means that if the simulation receives the same content, starting state, random seed, and input actions, it must produce the same results every time, including the same debug hash. A “fixed step” is a simulation update that always advances exactly one sixtieth of a second of game time, regardless of render frame rate. A “replay” is a saved stream of input actions, plus enough metadata to rebuild the same simulation run later. “Compiled runtime content” means engine-agnostic data files produced from Godot-authored scenes so that tests and the Godot host load the same content without depending on live scene trees.
- no floating-point math in simulation logic
- no use of `DateTime`, wall clock, threads, async work, GUIDs, or nondeterministic iteration order
- stable entity iteration order
- immutable definitions after load
- deterministic RNG with explicit persisted state
- all simulation outputs derived only from definitions, current state, seed, and action stream
Numerics: The desired end state is a repository where `src/SideScrollerGame.Sim/` contains the pure gameplay assembly, `tests/SideScrollerGame.Sim.Tests/` contains xUnit tests for engine-light gameplay behavior, `godot/` remains the presentation and authoring host, and `content/compiled/` holds deterministic runtime data produced from Godot-authored scenes. The dependency direction must be one-way: the Godot host references the simulation project, the tests reference the simulation project, and the simulation project references no Godot packages or Godot namespaces.
- use fixed-point math throughout the simulation
- represent position, velocity, and acceleration in deterministic units
- keep map sizes and tile sizes as integer values
The current `FixPoint/` folder should be treated as simulation infrastructure, not Godot host code. ## Milestones
## Simulation API ### Milestone 1: Create the simulation and test projects
The simulation should expose a narrow runner API that is safe for tests, replays, and Godot hosting:
This milestone establishes the repository shape needed for every later change. At the end of the milestone, the solution contains a pure gameplay project and a test project, the Godot host references the simulation assembly, and the deterministic math code lives under the simulation project path. Run the following commands from `D:\Code\SideScrollerGame` as the milestone is implemented:
dotnet new classlib --language C# --framework net8.0 --name SideScrollerGame.Sim --output src/SideScrollerGame.Sim
dotnet new xunit --language C# --framework net8.0 --name SideScrollerGame.Sim.Tests --output tests/SideScrollerGame.Sim.Tests
dotnet sln SideScrollerGame.sln add src/SideScrollerGame.Sim/SideScrollerGame.Sim.csproj
dotnet sln SideScrollerGame.sln add tests/SideScrollerGame.Sim.Tests/SideScrollerGame.Sim.Tests.csproj
dotnet add godot/SideScrollerGame.Godot.csproj reference src/SideScrollerGame.Sim/SideScrollerGame.Sim.csproj
dotnet add tests/SideScrollerGame.Sim.Tests/SideScrollerGame.Sim.Tests.csproj reference src/SideScrollerGame.Sim/SideScrollerGame.Sim.csproj
dotnet build SideScrollerGame.sln
Acceptance for this milestone is that the solution builds, the test project runs, and the Godot project still builds after it references the new simulation assembly. A novice should be able to inspect the solution and immediately see where pure gameplay code belongs.
### Milestone 2: Build the deterministic simulation spine
This milestone creates the smallest useful simulation loop. At the end of it, a caller can construct a `Simulation`, feed a `TickActionBatch`, advance exactly one tick, and receive a `TickResult` containing events and a deterministic hash. Run the tests and host build from `D:\Code\SideScrollerGame`:
dotnet test tests/SideScrollerGame.Sim.Tests/SideScrollerGame.Sim.Tests.csproj
.\godot.cmd --headless --path .\godot --build-solutions
Acceptance for this milestone is behavioral. Two fresh `Simulation` instances created with the same game definition, configuration, seed, and empty action stream must produce the same tick numbers, the same state hash, and the same serialized state bytes for a sequence of no-op ticks.
### Milestone 3: Add save/load, replay, and verification
This milestone makes deterministic behavior provable instead of assumed. At the end of it, the simulation can serialize all runtime state, replay saved action batches, and optionally clone itself every tick to verify that save/load and re-stepping reproduce the same state. Run this milestones proof commands from `D:\Code\SideScrollerGame`:
dotnet test tests/SideScrollerGame.Sim.Tests/SideScrollerGame.Sim.Tests.csproj --filter Replay
dotnet test tests/SideScrollerGame.Sim.Tests/SideScrollerGame.Sim.Tests.csproj --filter Verification
Acceptance for this milestone is that a replay generated from a test scenario reproduces the same per-tick hashes on a second run, and that `RoundTripState` plus `RoundTripAndStepClone` fail loudly when a serialization bug is introduced and pass when the state is complete.
### Milestone 4: Implement movement, collision, and the fixed tick pipeline
This milestone makes the simulation look like a game instead of a timer. At the end of it, the simulation owns movement, collision, weapon intent resolution, damage resolution, triggers, and snapshot generation in a stable per-tick order. Run the verification commands from `D:\Code\SideScrollerGame`:
dotnet test tests/SideScrollerGame.Sim.Tests/SideScrollerGame.Sim.Tests.csproj --filter Movement
dotnet test tests/SideScrollerGame.Sim.Tests/SideScrollerGame.Sim.Tests.csproj --filter Collision
dotnet test tests/SideScrollerGame.Sim.Tests/SideScrollerGame.Sim.Tests.csproj --filter Trigger
Acceptance for this milestone is that deterministic tests cover grounded movement, jump buffering, coyote time, one-way platforms, hazards, projectile sweeps, and trigger ordering, and that those tests fail when system order changes incorrectly.
### Milestone 5: Implement combat systems, enemies, and coordinated behaviors
This milestone fills out the genres core gameplay. At the end of it, the simulation supports heroes, enemies, weapons, projectiles, pickups, modifiers, squads, flocking, scripted encounters, and boss phase data using engine-agnostic definitions. Run the proof commands from `D:\Code\SideScrollerGame`:
dotnet test tests/SideScrollerGame.Sim.Tests/SideScrollerGame.Sim.Tests.csproj --filter Combat
dotnet test tests/SideScrollerGame.Sim.Tests/SideScrollerGame.Sim.Tests.csproj --filter Behavior
dotnet test tests/SideScrollerGame.Sim.Tests/SideScrollerGame.Sim.Tests.csproj --filter Encounter
Acceptance for this milestone is that tests can demonstrate reusable enemy behaviors, squad coordination, damage and drop resolution, and deterministic boss phase transitions without running Godot.
### Milestone 6: Compile authored levels into runtime content
This milestone connects Godot authoring to simulation-safe content. At the end of it, a designer can author level markers in Godot, run a compile step, and produce deterministic runtime data under `content/compiled/` that both tests and the host can consume. Run the milestone commands from `D:\Code\SideScrollerGame`:
.\godot.cmd --headless --path .\godot --build-solutions
dotnet test tests/SideScrollerGame.Sim.Tests/SideScrollerGame.Sim.Tests.csproj --filter Content
Acceptance for this milestone is that a level scene with spawn markers, checkpoints, trigger zones, encounter data, and music zones compiles into engine-agnostic definitions, and tests can load that compiled data without touching a live Godot scene tree.
### Milestone 7: Finish the Godot host and debugging tools
This milestone exposes the simulation to humans. At the end of it, the Godot host translates live input into `TickActionBatch` objects, runs the fixed-step simulation, interpolates between snapshots for rendering, plays overlapping sound effects and cross-faded music, and offers debug transport controls for play, pause, restart, single-step, and fast-forward. Run the proof commands from `D:\Code\SideScrollerGame`:
.\godot.cmd --editor --path .\godot
.\godot.cmd --headless --path .\godot --build-solutions
dotnet test tests/SideScrollerGame.Sim.Tests/SideScrollerGame.Sim.Tests.csproj
Acceptance for this milestone is that a developer can start the Godot project, load a test level, pause the simulation, advance one step at a time, jump forward to a later tick using saved checkpoints, and observe the same state hash sequence that the automated replay tests produce.
## Plan of Work
Start by extending the repository structure instead of altering the Godot host in place. Create `src/SideScrollerGame.Sim/SideScrollerGame.Sim.csproj` and `tests/SideScrollerGame.Sim.Tests/SideScrollerGame.Sim.Tests.csproj`, then add both projects to `SideScrollerGame.sln`. Update `godot/SideScrollerGame.Godot.csproj` to reference `src/SideScrollerGame.Sim/SideScrollerGame.Sim.csproj`, and update the test project to reference the same simulation project. If common compiler settings are needed for nullable reference types or analyzers, add them through a shared file such as `Directory.Build.props` at the repository root so the Godot host, simulation project, and tests stay aligned.
Next, migrate deterministic math into the new simulation project. Copy the existing files from `FixPoint/` into `src/SideScrollerGame.Sim/FixPoint/` and change namespaces so they read as simulation infrastructure rather than host helpers. Do not make the Godot project authoritative for any gameplay behavior. If the root `FixPoint/` directory remains temporarily, ensure that only the simulation project compiles the copied files and treat the root copies as legacy bootstrap artifacts until a safe cleanup step is possible.
After the math layer is inside the simulation project, build the minimal runtime model under `src/SideScrollerGame.Sim/`. Create folders and namespaces that match the long-term architecture: `Definitions/`, `Runtime/`, `Systems/`, `Serialization/`, `Replay/`, and `Verification/`. Define immutable configuration and content types under `Definitions/`. Define mutable world state under `Runtime/`. Define the fixed tick pipeline under `Systems/`. Define deterministic serialization under `Serialization/`. Define replay records under `Replay/`. Define clone-step verification and debug hashes under `Verification/`.
Implement the first useful public API immediately instead of waiting for all systems to exist. `src/SideScrollerGame.Sim/Simulation.cs` should expose the constructor and methods described in the original groundwork: a constructor that takes `GameDefinition`, `SimulationConfig`, and a seed; `CurrentTick`; `CurrentState`; `PreviousSnapshot`; `CurrentSnapshot`; `Step(in TickActionBatch actions)`; `SaveState()`; and `LoadState(...)`. The early implementation can return empty event lists and minimal snapshots, but it must already be deterministic, serializable, and testable.
Once the simulation spine exists, implement replay and verification before building rich gameplay systems. Add explicit versioned serialization for `SimulationState` and replay files. Introduce `VerificationMode.None`, `VerificationMode.RoundTripState`, and `VerificationMode.RoundTripAndStepClone`. Write tests that intentionally prove determinism: same seed plus same actions equals same hashes, while different seeds or different actions change hashes. Keep test fixtures engine-light and executable from `dotnet test` alone.
With deterministic scaffolding proven, implement gameplay in stable layers. Add movement and collision first because every other system depends on positions and contacts. Then add damage, projectiles, pickups, and modifiers. Then add AI behaviors, squads, flocking, and scripted encounters. Keep all semantic animation and audio requests as plain simulation events, not Godot-specific clip names or audio player calls. When a new subsystem is added, add tests that prove its behavior in isolation and in scenario form.
In parallel with later gameplay systems, build the host-side adapters under `godot/scripts/host/`. Add a run controller that owns real-time accumulation and fixed-step execution. Add an input translator that converts keyboard and gamepad input into serializable simulation actions. Add presenter code that maps `EntityId` values to visual nodes and interpolates only between simulation snapshots. Add an audio layer that consumes logical sound and music requests. Add debug controls that operate on the simulation runner rather than on Godot timers or animation state.
Finally, add the authoring and content compile path. Godot scenes remain the editing surface, but the implementation must compile those scenes into deterministic runtime definitions under `content/compiled/`. Add validation tools for broken references, overlapping markers, and invalid encounters before those authored assets reach the simulation. The tests and the host must both load the compiled content so that authoring and gameplay are verified against the same data.
## Concrete Steps
Run all commands from `D:\Code\SideScrollerGame` unless a step states otherwise.
1. Create the new projects and add them to the solution.
dotnet new classlib --language C# --framework net8.0 --name SideScrollerGame.Sim --output src/SideScrollerGame.Sim
dotnet new xunit --language C# --framework net8.0 --name SideScrollerGame.Sim.Tests --output tests/SideScrollerGame.Sim.Tests
dotnet sln SideScrollerGame.sln add src/SideScrollerGame.Sim/SideScrollerGame.Sim.csproj
dotnet sln SideScrollerGame.sln add tests/SideScrollerGame.Sim.Tests/SideScrollerGame.Sim.Tests.csproj
dotnet add godot/SideScrollerGame.Godot.csproj reference src/SideScrollerGame.Sim/SideScrollerGame.Sim.csproj
dotnet add tests/SideScrollerGame.Sim.Tests/SideScrollerGame.Sim.Tests.csproj reference src/SideScrollerGame.Sim/SideScrollerGame.Sim.csproj
Expect `dotnet sln` to report that both projects were added, and expect the `dotnet add ... reference ...` commands to report a new project reference.
2. Copy the root `FixPoint/*.cs` files into `src/SideScrollerGame.Sim/FixPoint/`, then update the namespaces inside those files so the simulation project owns the deterministic numeric layer.
After the copy, build the solution:
dotnet build SideScrollerGame.sln
Expect build output to mention all three projects and to finish without duplicate type definitions. If duplicate types appear, the wrong project is compiling both copies of the math files.
3. Add the simulation spine and test it immediately. Create `src/SideScrollerGame.Sim/Simulation.cs`, `src/SideScrollerGame.Sim/Runtime/SimulationState.cs`, `src/SideScrollerGame.Sim/Runtime/WorldSnapshot.cs`, `src/SideScrollerGame.Sim/Input/SimulationAction.cs`, `src/SideScrollerGame.Sim/Input/TickActionBatch.cs`, `src/SideScrollerGame.Sim/Verification/VerificationMode.cs`, and matching tests under `tests/SideScrollerGame.Sim.Tests/`.
Run:
dotnet test tests/SideScrollerGame.Sim.Tests/SideScrollerGame.Sim.Tests.csproj
Expect early tests to prove that stepping the simulation increments the tick and yields stable hashes for identical inputs.
4. Add serialization, replay, and verification. Create files under `src/SideScrollerGame.Sim/Serialization/`, `src/SideScrollerGame.Sim/Replay/`, and `src/SideScrollerGame.Sim/Verification/`, then add tests for replay determinism and state round-tripping.
Run:
dotnet test tests/SideScrollerGame.Sim.Tests/SideScrollerGame.Sim.Tests.csproj --filter Replay
dotnet test tests/SideScrollerGame.Sim.Tests/SideScrollerGame.Sim.Tests.csproj --filter Verification
Expect replay tests to pass with the same hash sequence on repeated runs.
5. Implement movement, collision, combat, triggers, and scenario tests in the simulation project. Keep adding tests before wiring presentation details in Godot.
Run:
dotnet test tests/SideScrollerGame.Sim.Tests/SideScrollerGame.Sim.Tests.csproj /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura /p:Threshold=100 /p:ThresholdType=line,branch /p:ThresholdStat=total
Expect the command to fail if gameplay logic coverage drops below 100 percent.
6. Wire the Godot host to the simulation project by adding host-side runner, presenter, audio, and debug transport code under `godot/scripts/host/`, then confirm the host still compiles.
Run:
.\godot.cmd --headless --path .\godot --build-solutions
Expect Godot to regenerate and build C# project artifacts successfully.
7. Build the content compile path and manual host verification flow.
Run:
.\godot.cmd --editor --path .\godot
In the editor, validate that authored level scenes can be compiled into `content/compiled/`, then run the game and observe that pausing, single-stepping, and replaying preserve deterministic state hashes.
## Validation and Acceptance
The implementation is acceptable only when a human can see deterministic behavior, not merely when the code compiles. Start with automated proof. `dotnet test tests/SideScrollerGame.Sim.Tests/SideScrollerGame.Sim.Tests.csproj` must pass, and replay-oriented tests must demonstrate that the same seed and action stream yield the same per-tick debug hash sequence. The coverage command in `Concrete Steps` must enforce 100 percent line and branch coverage for gameplay logic inside `SideScrollerGame.Sim`, with only narrow exclusions for non-logic infrastructure such as generated serializers or lookup tables.
Then verify the host integration. `.\godot.cmd --headless --path .\godot --build-solutions` must succeed, proving that the Godot project can consume the simulation assembly. After that, start the editor with `.\godot.cmd --editor --path .\godot`, load a level built from compiled runtime content, and verify the following behaviors manually: play starts the simulation, pause freezes the simulation while leaving the editor responsive, advance-one-step increments the tick exactly once, fast-forward resumes from the nearest saved checkpoint and reaches the requested tick, and replaying the same recorded input reproduces the same state hash display.
The final acceptance scenario is simple and observable. Record a short run in which the player moves, fires, triggers an encounter, collects a pickup, and reaches a checkpoint. Save the replay. Restart the host with the same compiled content and seed, play the replay, and confirm that the tick count, key gameplay events, and final state hash all match the original run. If any of those differ, the implementation is incomplete.
## Idempotence and Recovery
Most steps in this plan are additive and safe to repeat. Re-running `dotnet build`, `dotnet test`, or `.\godot.cmd --headless --path .\godot --build-solutions` is safe and should produce the same result when the repository state has not changed. Re-running the host replay validation is also safe because replays and checkpoints are meant to be reproducible.
Project creation commands such as `dotnet new classlib` and `dotnet new xunit` are not naturally idempotent once the destination directories already exist. If those directories have already been created, do not delete them to start over. Instead, inspect the generated files, patch them into the required shape, and continue. The same rule applies to content compilation in this Windows environment: prefer overwriting generated runtime content in place instead of deleting directories.
When migrating the `FixPoint/` sources, do not remove the root copies until the simulation project is compiling and all references are proven correct. If a migration step goes wrong, recover by fixing project includes and namespaces rather than by using rollback commands such as `git restore` or `git reset`. The plan is intentionally written so that each milestone can be resumed from the current working tree without assuming a clean slate.
## Artifacts and Notes
Current repository snapshot, captured while writing this plan:
PS D:\Code\SideScrollerGame> rg --files
SideScrollerGame.sln
PLANS.md
groundwork.md
godot.cmd
godot\SideScrollerGame.Godot.csproj
godot\project.godot
FixPoint\IntRandom.cs
FixPoint\FixPoint16.cs
...
Current solution shape, which this plan will extend:
Project(...) = "SideScrollerGame.Godot", "godot/SideScrollerGame.Godot.csproj", "{75DE3F78-FF5C-4E58-8315-8AEF5BF95BBA}"
Current Godot project target framework:
<Project Sdk="Godot.NET.Sdk/4.5.1">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
</Project>
Use those facts as the starting point when implementing this plan. If the repository changes later, update this section and every other affected section of the plan so the document remains self-contained.
## Interfaces and Dependencies
Use .NET 8 for the simulation and test projects so they align with the existing Godot project target. Keep the Godot host on `Godot.NET.Sdk/4.5.1` unless a later, repository-wide upgrade is performed deliberately and documented in this plan. The simulation project may use built-in .NET libraries and deterministic-support libraries that do not pull in Godot. Avoid floating-point math, wall-clock time, thread scheduling, asynchronous gameplay work, or any API that can vary results between runs.
At the end of Milestone 2, the simulation project must define these public interfaces and types with stable names and repository-relative locations:
In `src/SideScrollerGame.Sim/Simulation.cs`, define:
```csharp
public sealed class Simulation public sealed class Simulation
{ {
public Simulation(GameDefinition gameDefinition, SimulationConfig config, int seed); public Simulation(GameDefinition gameDefinition, SimulationConfig config, int seed);
@@ -145,28 +284,9 @@ public sealed class Simulation
public byte[] SaveState(); public byte[] SaveState();
public static Simulation LoadState(byte[] data, GameDefinition gameDefinition, SimulationConfig config); public static Simulation LoadState(byte[] data, GameDefinition gameDefinition, SimulationConfig config);
} }
```
`TickResult` should contain: In `src/SideScrollerGame.Sim/Input/SimulationAction.cs`, define the serializable action hierarchy:
- gameplay events
- presentation events
- sound requests
- music requests
- spawn/despawn notifications
- per-tick debug hash
- optional verification report
## Input Model and Replay Format
The simulation should receive user intent through serializable actions, not direct polling state.
Recommended approach:
- Godot host converts raw keyboard/gamepad input into `SimulationAction` records
- each tick receives a `TickActionBatch`
- continuous controls are handled by edge-triggered actions plus persistent input state inside the simulation
Example action model:
```csharp
public abstract record SimulationAction; public abstract record SimulationAction;
public sealed record MoveAxisChanged(PlayerId PlayerId, sbyte X, sbyte Y) : SimulationAction; public sealed record MoveAxisChanged(PlayerId PlayerId, sbyte X, sbyte Y) : SimulationAction;
@@ -174,478 +294,25 @@ public sealed record AimAxisChanged(PlayerId PlayerId, short X, short Y) : Simul
public sealed record ButtonChanged(PlayerId PlayerId, InputButton Button, bool IsPressed) : SimulationAction; public sealed record ButtonChanged(PlayerId PlayerId, InputButton Button, bool IsPressed) : SimulationAction;
public sealed record WeaponSlotSelected(PlayerId PlayerId, int SlotIndex) : SimulationAction; public sealed record WeaponSlotSelected(PlayerId PlayerId, int SlotIndex) : SimulationAction;
In `src/SideScrollerGame.Sim/Input/TickActionBatch.cs`, define:
public sealed record TickActionBatch(int Tick, ImmutableArray<SimulationAction> Actions); public sealed record TickActionBatch(int Tick, ImmutableArray<SimulationAction> Actions);
```
Why this matters: In `src/SideScrollerGame.Sim/Verification/VerificationMode.cs`, define:
- the exact action stream can be recorded to disk
- bug reports can attach a replay instead of describing a scenario
- tests can load recorded runs and verify hashes
- stepping to tick `N` is just replay plus optional checkpoint restore
Replay file contents: public enum VerificationMode
- content hash or version id {
- simulation config None,
- seed RoundTripState,
- initial level/start condition RoundTripAndStepClone
- per-tick action batches }
- optional periodic serialized state checkpoints
## Full State Serialization In `src/SideScrollerGame.Sim/Runtime/`, define `SimulationState`, `WorldSnapshot`, `TickResult`, entity identifiers, runtime state groups such as `PlayerState`, `EnemyState`, `ProjectileState`, `PickupState`, `HazardState`, `PlatformState`, `TriggerState`, `CameraState`, `LevelRuntimeState`, and `SquadState`, and a stable per-tick pipeline that applies actions, advances timers, updates AI, resolves weapons, integrates movement, resolves collisions, applies damage, processes triggers, and then produces events, snapshots, and hashes.
The entire simulation state must be serializable. This is not just for player save games. It is also required for:
- replay checkpoints
- fast-forward and jump-to-tick tooling
- save/load correctness verification
- deterministic debugging
`SimulationState` should include: In `src/SideScrollerGame.Sim/Definitions/`, define immutable content types including `GameDefinition`, `LevelDefinition`, `HeroDefinition`, `EnemyDefinition`, `WeaponDefinition`, `ProjectileDefinition`, `PowerUpDefinition`, `SquadDefinition`, and related modifier and behavior definitions. These types must remain engine-agnostic and serializable.
- current tick
- active level and checkpoint
- all runtime entities and subsystem state
- timers and cooldowns
- RNG state
- squad and flock coordination state
- scripted trigger state
- camera gameplay state
- pending spawn queues
- music/gameplay progression state
- player input hold state
Serialization guidance: In `src/SideScrollerGame.Sim/Replay/` and `src/SideScrollerGame.Sim/Serialization/`, define versioned replay and state persistence types that can serialize the full simulation state, recorded action batches, content hash, seed, and optional checkpoints without relying on Godot serialization.
- use an explicit versioned serializer owned by the simulation project
- avoid relying on Godot serialization
- keep format deterministic and stable
- support both compact runtime format and optional debug-friendly dump format if useful
## Built-In Desync Verification In `godot/scripts/host/`, define host-side adapters with clear responsibilities: a `RunController` that owns real-time accumulation and fixed-step stepping, an input translator that turns Godot input into simulation actions, a presentation registry keyed by `EntityId`, interpolation code that renders between `PreviousSnapshot` and `CurrentSnapshot`, and audio services that consume simulation sound and music requests without becoming gameplay authorities.
During development, each step should optionally verify save/load correctness and step determinism by round-tripping a clone.
Recommended `VerificationMode`: Revision Note: 2026-04-16, Codex. Replaced the original groundwork note with an ExecPlan that follows `PLANS.md`, because the old document captured architectural intent but did not give a novice enough concrete guidance to implement, validate, and maintain the work.
- `None`
- `RoundTripState`
- `RoundTripAndStepClone`
`RoundTripAndStepClone` algorithm for each tick:
1. serialize the live simulation
2. deserialize a clone from those bytes
3. compare live and clone state hashes or normalized snapshots
4. apply the same `TickActionBatch` to both instances
5. compare resulting states again
6. report any mismatch with tick number, subsystem, and serialized artifacts
This catches two different problems:
- **save/load desync**: serialization fails to reconstruct the same state
- **step desync**: cloned and live instances diverge when stepping the same input
This mode is expensive and should be optional, but it is exactly the right tool for early framework development.
## Runtime Debug Time Controls
Development UX must support stepping and replay inspection from day one.
Required transport controls:
- `Restart`
- `Play/Pause`
- `Advance One Step`
- `Fast Forward To Step X`
Recommended supporting features:
- current tick display
- current replay seed
- state hash display
- verification on/off toggle
- playback speed multiplier
- jump to checkpoint
- scrub to nearest saved checkpoint, then replay to requested tick
Implementation guidance:
- maintain a `RunController` in the Godot host
- cache serialized checkpoints every `N` ticks to make fast-forward practical
- treat restart as reloading the initial seed, content, and replay
- do not let presentation-only pause state alter simulation state
## World Representation
Avoid a generic ECS. A domain-specific, data-oriented model is a better fit for this genre.
Recommended runtime state groups:
- `PlayerState`
- `EnemyState`
- `ProjectileState`
- `PickupState`
- `HazardState`
- `PlatformState`
- `TriggerState`
- `CameraState`
- `LevelRuntimeState`
- `SquadState`
Cross-cutting concepts:
- `EntityId`
- faction/team
- transform
- velocity
- health and armor
- hurtboxes and hitboxes
- timers and modifiers
- presentation state keys
Keep definitions immutable and runtime state mutable.
## Fixed Tick Pipeline
Each tick should run in a fixed order:
1. apply incoming actions to player input state
2. advance timers, cooldowns, modifiers, and scripted sequences
3. update AI, squad logic, and flocking decisions
4. resolve weapon intents and spawn projectiles/effects
5. integrate movement
6. resolve map, platform, and environment collision
7. resolve projectile, melee, and contact hits
8. apply damage, knockback, deaths, drops, score, and checkpoint changes
9. process triggers, camera rules, wave progression, and music state transitions
10. build events, snapshots, and hashes
This order must remain explicit and stable. New systems should be inserted deliberately, not opportunistically.
## Physics and Collision
The simulation owns all gameplay physics.
Recommended model:
- 2D kinematic actors
- deterministic AABB-based collision for actors
- tile-grid collision for level geometry
- one-way platforms
- ladders and climb volumes
- hazard and water zones
- deterministic moving platforms
- sweep or raycast projectile movement to avoid tunneling
Movement design targets:
- grounded movement
- coyote time and jump buffering
- crouch and stance-dependent hitboxes
- knockback and stun
- moving platform carry behavior
- optional slopes later if needed
Do not use Godot rigid bodies as gameplay authority.
## Frame Interpolation
Rendering should interpolate between two authoritative simulation snapshots.
Godot host loop:
1. accumulate real frame time
2. run zero or more fixed simulation steps
3. compute interpolation alpha
4. render between `PreviousSnapshot` and `CurrentSnapshot`
Interpolate:
- position
- aim direction
- camera anchor
- recoil offsets
Do not interpolate:
- deaths
- pickups
- state machine transitions
- damage flashes
- spawn and despawn boundaries
Presentation nodes should be mapped by `EntityId` and recreated only on tick boundaries.
## Animation Model
The simulation should emit semantic animation state, not Godot clip names.
Example semantic outputs:
- locomotion: `Idle`, `Run`, `JumpRise`, `JumpFall`, `Land`, `Crouch`, `Climb`
- combat: `Fire`, `Reload`, `Charge`, `Melee`, `Overheat`
- damage: `Hurt`, `Invulnerable`, `Dead`
- modifiers: facing, aim sector, grounded, wet, frozen, heavy_weapon
The Godot host maps semantic state to `AnimationTree`, sprite animations, blend spaces, or custom timelines.
## Audio Architecture
The simulation emits logical audio requests; the Godot host performs actual playback.
### Overlapping Sound Effects
Simulation should emit `SoundEvent` data:
- cue id
- source entity or world position
- category/bus
- priority
- optional pitch/variant seed
The Godot host should:
- pool multiple audio players
- allow overlapping playback for repeated weapons and explosions
- cap voices per cue and bus
- support both positional and UI sounds
### Cross-Fading Music Queue
The simulation should emit logical music requests, not manipulate players directly.
Recommended requests:
- `Queue(trackId)`
- `Replace(trackId, fadeOutTicks, fadeInTicks)`
- `PushPriority(trackId)`
- `PopPriority(trackId)`
- `Stop(fadeOutTicks)`
The Godot host should implement a `MusicDirector` with:
- at least two music players for cross-fading
- support for intro, loop, and optional outro segments
- deterministic transitions triggered from simulation ticks
- queue inspection in the debug UI
## Gameplay Definitions
Definitions live inside the simulation project under `Definitions/` and are loaded from compiled engine-agnostic data.
### HeroDefinition
Include:
- id, tags, display info
- collider and stance shapes
- health, armor, lives, invulnerability ticks
- movement profile
- weapon loadout
- pickup interaction rules
- animation profile id
- sound profile id
### EnemyDefinition
Include:
- id, archetype, faction, tags
- collider and hurtboxes
- health and contact damage
- movement profile
- behavior definition id
- squad role support
- weapon set
- score reward and drop table
- boss phase data when needed
### WeaponDefinition
Include:
- slot type
- fire mode
- cadence and burst settings
- ammo and reload rules
- muzzle offsets
- projectile or hitscan profile
- recoil, spread, and status effects
- animation and audio ids
### ProjectileDefinition
Include:
- speed and lifetime
- gravity scale
- collider
- collision mask
- pierce behavior
- hit response
- damage payload
- explosion or follow-up spawn ids
### PowerUpDefinition
Include:
- pickup type
- effect payload
- duration
- stacking policy
- despawn rules
- magnet behavior
- audio and presentation ids
### Modifier Model
Use a shared modifier system for buffs and debuffs:
- additive and multiplicative stat changes
- capability flags
- timed effects
- stack limits
- refresh behavior
## Behavior Framework
Use a data-driven hierarchical state machine model with reusable sensors and actions. Do not rely on Godot scripts for enemy logic.
Reusable sensors:
- target in range
- line of sight
- ground ahead
- wall ahead
- recently damaged
- timer elapsed
- squad signal received
Reusable actions:
- move
- stop
- jump
- fire
- dodge
- retreat
- strafe
- hover
- call squad action
### Squadrons and Flocking
The behavior framework must also support coordinated enemies, not just isolated actors.
Required concepts:
- `SquadDefinition`
- `SquadState`
- leader/follower roles
- formation anchor and slots
- shared target selection
- squad orders such as attack, regroup, retreat, flank
- separation, cohesion, and alignment style flocking for flyers or swarms
Use flocking as a tunable subsystem, not a hard-coded enemy type. The simulation should support:
- loose flying swarms
- tight escort formations
- attack waves that break formation temporarily
- rejoin logic after disruption
## Level Data Structure
Levels must support both authored geometry and deterministic encounter scripting.
Recommended `LevelDefinition`:
- metadata and dimensions
- tile layers and collision/material map
- spawn markers
- pickups and hazards
- moving platforms and paths
- checkpoints
- trigger zones
- encounter and wave definitions
- camera zones and locks
- music zones and scripted transitions
- scripted event graph
Keep authored Godot scenes as input to a compile/export step. The simulation and tests should run on compiled runtime data, not raw scene trees.
## Level Editing UX in Godot
Godot should be used as the authoring front-end, but authored scenes must compile into engine-agnostic runtime definitions.
Recommended workflow:
1. author level scenes in Godot
2. place custom marker nodes for spawns, triggers, checkpoints, patrol paths, squad anchors, and music zones
3. validate the authoring scene
4. compile to runtime content under `content/compiled/`
5. run game and tests against compiled data
Required authoring UX:
- custom inspector editors for definitions and references
- gizmos for triggers, camera bounds, paths, and squad formations
- validation panel for bad ids, overlapping markers, broken references, and invalid encounter setups
- buttons for `Validate`, `Compile`, and `Play From Here`
- preview of wave timing, camera rules, and music transitions where practical
## Content Pipeline
There is still a distinction between authoring content and runtime content, but it does not justify a separate assembly.
Authoring content:
- Godot scenes and resources
- marker nodes and editor-only metadata
Compiled runtime content:
- engine-agnostic serialized definitions
- loaded by the simulation project
- used identically by tests and the Godot host
Compilation should:
- resolve references
- validate identifiers and links
- flatten authored data into deterministic DTOs
- assign stable content hashes
- produce clear compile errors
## Save/Load, Replay, and Debugging
These three systems should be designed together.
### Save/Load
Save files should contain:
- content hash/version
- serialized `SimulationState`
- optional metadata such as timestamp or user-visible slot info
### Replay
Replay files should contain:
- content hash/version
- simulation config
- seed
- initial start state or entry point
- action batches per tick
- optional periodic state checkpoints
### Debugging
A bug report should ideally include:
- replay file
- final tick number
- expected vs actual state hash
- verification failure artifacts if present
This allows deterministic reproduction without stepping through engine code.
## Testing Strategy
Use **xUnit** for all automated simulation tests.
Test categories:
- unit tests for movement, collision, damage, weapons, modifiers, and trigger logic
- scenario tests for encounters and level scripts
- replay tests using recorded action streams
- serialization roundtrip tests for full simulation state
- desync verification tests for clone-step comparison
- property or fuzz tests for invariants where useful
Coverage expectations:
- target 100% line and branch coverage for gameplay logic in `SideScrollerGame.Sim`
- allow narrow exclusions only for generated lookup tables, source-generated serializers, or similar non-logic infrastructure
- do not use exclusions to hide untested gameplay systems
Recommended tooling:
- xUnit
- coverlet
- deterministic test fixtures and builders
## Implementation Phases
### Phase 1: Project Restructure
- move the Godot project under `/godot`
- create `src/SideScrollerGame.Sim`
- create `tests/SideScrollerGame.Sim.Tests`
- temporarily link the existing root `FixPoint/` sources into the simulation project if needed
### Phase 2: Simulation Foundation
- integrate fixed-point numerics and deterministic RNG
- define `SimulationState`, snapshots, ids, events, and core step runner
- define serialization contracts
### Phase 3: Replay and Verification
- define serializable action model
- implement replay recording and playback
- implement per-step roundtrip and clone-step verification
- add debug hashes
### Phase 4: Movement and Collision
- implement actor movement, platforms, ladders, hazards, and projectile sweeps
- build exhaustive xUnit coverage for collision edge cases
### Phase 5: Combat and Behaviors
- add hero, enemy, weapon, projectile, powerup, and modifier definitions
- implement behavior graphs, squads, flocking, and boss phase support
### Phase 6: Levels and Authoring
- implement level definitions, triggers, checkpoints, and scripted graph
- build Godot authoring nodes, validators, and compile/export flow
### Phase 7: Presentation Host
- add interpolation, presenter registry, animation mapping, audio playback, and music director
- add runtime transport controls and replay inspection UI
## Key Decisions to Hold
- keep all gameplay authority in the pure simulation project
- keep content definitions in that same project unless a real dependency issue emerges
- keep the Godot host in its own subfolder and project
- keep replay, save/load, and verification as first-class features
- keep fixed-point determinism as the baseline
- keep the debug transport controls available throughout development
If these constraints are maintained, the framework will remain flexible enough for later game design work without collapsing into engine-driven logic or untestable behavior.