Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 33 additions & 23 deletions docs/adr/0003-fixed-timestep-simulation.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# ADR 0003: Fixed Timestep Simulation Model
# ADR 0003: Fixed Timestep Simulation Model (Tick-Driven)

## Status
Accepted
Expand All @@ -7,36 +7,43 @@ Accepted
Technical

## Context
Variable delta time (frame-rate-dependent simulation) causes non-determinism: the same inputs produce different outcomes depending on how fast the frame loop runs. This makes replay verification impossible, testing unreliable, and gameplay inconsistent across hardware.
Variable delta time (frame-rate-dependent simulation) causes non-determinism and makes replay verification and testing unreliable. The Simulation Core must advance independently of frame rate and wall-clock time.

To achieve determinism (ADR-0002), the simulation must advance in predictable, fixed-size increments independent of frame rate or wall-clock time.
A per-step `dt_seconds` argument is a determinism footgun: even if intended constant, it invites accidental variation and weakens the Simulation Core boundary.

## Decision
The Simulation Core (DM-0014, defined in ADR-0001) MUST advance in **fixed discrete ticks**. Each tick represents a fixed duration of simulated time.
The Simulation Core (DM-0014) MUST advance in **fixed discrete ticks**. Tick duration is a match-level constant derived from match configuration and MUST NOT vary within a match.

**Requirements:**
- Simulation stepping function: `advance(state, inputs, dt_seconds)` where `dt_seconds` is constant for a match
- Tick rate is a **match-level constant** (declared in protocol handshake, e.g., 60 Hz → dt = 16.67ms)
- All time-based rules MUST be expressed in seconds (cooldowns, durations, velocities), not tick counts
- Game logic and physics MUST NOT depend on:
- Frame rate or frames-per-second
- Wall-clock time or variable delta time
- Rendering vsync or monitor refresh rate
- Simulation state transitions occur ONLY on tick boundaries
The Simulation Core stepping API MUST be tick-driven and MUST NOT accept per-call delta time.

**Initial supported tick rate:** 60 Hz (may expand to 30/120 Hz in future)
### Normative Requirements
- Match start configuration MUST declare `tick_rate_hz`; `dt_seconds = 1.0 / tick_rate_hz` is implied and constant for the match.
- Tick rate MUST be stored on the Simulation Core instance (e.g., World) for the match; it MUST NOT be sourced from global/process state.
- Simulation stepping MUST be `advance(tick: Tick, step_inputs: &[StepInput]) -> Snapshot` (or equivalent). The tick parameter is the explicit boundary tick per INV-0005; tick duration is implicit to the Simulation Core instance (configured at construction).
- The Simulation Core MUST assert that the provided tick matches its internal state (e.g., `tick == world.tick()`) to prevent misalignment.
- StepInput (DM-0027) is the simulation-plane input type; the Server Edge MUST convert AppliedInput (DM-0024) to StepInput before invoking `advance()`.
- State transitions MUST occur only on tick boundaries.
- Simulation logic MUST NOT depend on:
- frame rate / render cadence
- wall-clock time
- variable delta time
- Time-based gameplay rules SHOULD be authored in seconds (cooldowns, durations, speeds). Implementations MAY convert these to tick-domain constants at match initialization, provided the conversion is deterministic and derived solely from match configuration.

### Initial Supported Tick Rate
- v0: 60 Hz (see [docs/networking/v0-parameters.md](../networking/v0-parameters.md))
- Additional discrete tick rates (e.g., 30/120 Hz) require validation before adoption.

## Rationale
**Why fixed timestep:**
- **Prerequisite for determinism:** Same inputs + same dt → same outputs (ADR-0002)
- **Prerequisite for determinism:** Same inputs + same tick rate → same outputs (ADR-0002)
- **Consistent gameplay:** Movement/physics behave identically on slow and fast hardware
- **Replayability:** Recorded inputs can be replayed at any speed without changing outcomes
- **Networking:** Clients and server can synchronize on tick numbers

**Why dt-parameterized (not hardcoded tick count):**
- Time-based rules (e.g., "3 second cooldown") are intuitive and tunable
- Allows testing at different tick rates without rewriting logic
- Scales to variable tick rates (30/60/120 Hz) if needed in future
**Why tick-driven (no per-call dt):**
- Eliminates a major determinism footgun (accidental dt variation)
- Clarifies Simulation Core boundary (tick rate is configuration, not runtime parameter)
- "Author in seconds" still works; deterministic init-time conversion enables multi-rate support

**Why match-level constant (not global):**
- Different game modes may benefit from different tick rates
Expand All @@ -52,6 +59,8 @@ The Simulation Core (DM-0014, defined in ADR-0001) MUST advance in **fixed discr
- Constitution IDs:
- INV-0002 (Fixed Timestep) — canonical definition
- DM-0001 (Tick) — atomic unit of game time
- DM-0024 (AppliedInput) — Server Edge input selection
- DM-0027 (StepInput) — Simulation Core input type
- Canonical Constitution docs:
- [docs/constitution/invariants.md](../constitution/invariants.md)
- [docs/constitution/domain-model.md](../constitution/domain-model.md)
Expand All @@ -61,18 +70,19 @@ The Simulation Core (DM-0014, defined in ADR-0001) MUST advance in **fixed discr

## Alternatives Considered
- **Variable delta time** — `advance(state, inputs, dt_variable)` where dt changes per frame. Rejected: Non-deterministic; same inputs produce different outcomes at different frame rates.
- **Per-call `dt_seconds` constant-in-practice** — `advance(inputs, dt_seconds)` where dt is "supposed to be" constant. Rejected: Footgun; boundary leak; invites accidental variation.
- **Frame-locked simulation** — Couple simulation to rendering frame rate. Rejected: Determinism requires independence from frame rate.
- **Hardcoded tick rate (no dt parameter)** — Simulation logic uses tick counts instead of seconds. Rejected: Less intuitive; harder to tune; cannot test at different tick rates.
- **Hardcoded tick rate (no configurability)** — Single tick rate, no match-level configuration. Rejected: Less flexible for future game modes; testing at different rates is useful.
- **Fully variable tick rate** — Allow tick rate to change mid-match. Rejected: Numerical integration differences cause subtle gameplay changes; validation surface area explodes.

## Implications
- **Enables:** Determinism (ADR-0002), replay verification, tick-synchronized networking, consistent gameplay across hardware
- **Constrains:** Simulation cannot adapt to low frame rates; must use fixed-timestep integration techniques
- **Constrains:** Simulation cannot adapt to low frame rates; must use fixed-timestep integration techniques; tick rate is chosen at match start
- **Migration costs:** None (greenfield project)
- **Contributor impact:** All simulation logic must be written in terms of `dt_seconds`, not frame count or wall-clock time
- **Contributor impact:** Contributors MUST keep wall-clock time out of the Simulation Core; any time progression is tick-indexed and configuration-derived. Replay artifacts MUST record tick configuration sufficient to reproduce the match's tick duration.

## Follow-ups
- Define simulation stepping API: `advance(state, inputs, dt_seconds)`
- Define concrete `World::new(seed, tick_rate_hz)` and `World::advance(tick, step_inputs)` API in simulation crate
- Document integration patterns for movement/physics at fixed timestep
- Validate behavior at 30/60/120 Hz before expanding supported tick rates
- Add CI gate: verify same outcomes at same tick rate across multiple runs
7 changes: 4 additions & 3 deletions docs/adr/0005-v0-networking-architecture.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# ADR 0005: v0 Networking Architecture

## Status
Proposed
Accepted

## Type
Technical
Expand Down Expand Up @@ -56,9 +56,10 @@ For v0, we use:
- **60 Hz tick rate**, **unreliable snapshots** @ 60 Hz, **unreliable inputs** @ 60 Hz
- See [docs/networking/v0-parameters.md](../networking/v0-parameters.md) for tunables
- Justification: Simple 1:1 tick-to-snapshot-to-input mapping for v0
- **Tier-0 input validation:** magnitude limit, tick window (±120 ticks), rate limit (120/sec)
- **Tier-0 input validation:** magnitude limit, tick window, rate limit
- Tick acceptance window is defined in [docs/networking/v0-parameters.md](../networking/v0-parameters.md)
- Tick targeting semantics (TargetTickFloor, InputSeq) are defined in ADR-0006
- Justification: Permissive for LAN/dev; prevents crashes from malformed inputs
- Values in [docs/networking/v0-parameters.md](../networking/v0-parameters.md)

### Determinism Scope

Expand Down
105 changes: 105 additions & 0 deletions docs/adr/0006-input-tick-targeting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# ADR 0006: Input Tick Targeting & Server Tick Guidance

## Status
Accepted

## Type
Technical

## Context
Clients must tag InputCmd messages with a target Tick. If clients choose target ticks solely based on the most recently observed server tick (from Welcome/Snapshots), that observation is inherently delayed by network latency. This produces late-tagged inputs that arrive after the server has already processed the targeted tick(s), causing dropped inputs and confusing startup behavior.

Additionally, when multiple InputCmd messages target the same (PlayerId, Tick), the Server Edge must select one deterministically. Relying on packet arrival order is non-deterministic and transport-dependent.

We need a deterministic, protocol-level mechanism that:
- makes the input-targeting rule explicit and testable,
- avoids relying on wall-clock timing inside the Simulation Core,
- tolerates reordering/duplication without "arrival order" semantics,
- and preserves replay verification by ensuring the Simulation Core consumes only StepInput (derived from AppliedInput).

## Decision
The Server Edge MUST provide tick guidance to clients, and clients MUST use that guidance when selecting InputCmd target ticks.

### Normative Requirements

**Server-Guided Targeting (TargetTickFloor):**
- The Server Edge MUST emit TargetTickFloor (DM-0025) in ServerWelcome and in each Snapshot (DM-0007).
- TargetTickFloor MUST be computed as: `server.current_tick + INPUT_LEAD_TICKS` (see [docs/networking/v0-parameters.md](../networking/v0-parameters.md) for v0 value).
- TargetTickFloor MUST be monotonic non-decreasing per Session (DM-0008); resets on session re-establishment or new MatchId (DM-0021).
- Game Clients MUST target InputCmd.tick values >= TargetTickFloor (clients MUST clamp upward).
- Game Clients MUST NOT target InputCmd.tick values earlier than TargetTickFloor.

**Deterministic Input Selection (InputSeq):**
- InputCmd MUST carry an InputSeq (DM-0026) that is monotonically increasing per Session.
- When multiple InputCmd messages are received for the same (PlayerId, Tick), the Server Edge MUST deterministically select the winning InputCmd using InputSeq (greatest wins) or an equivalent deterministic mechanism.
- InputSeq selection MUST NOT depend on packet arrival order or other runtime artifacts.

**Input Pipeline:**
- The Server Edge MUST buffer inputs by (PlayerId, Tick) within the InputTickWindow (DM-0022).
- For each Tick T processed, the Server Edge MUST produce exactly one AppliedInput (DM-0024) per participating player, using LastKnownIntent (DM-0023) fallback when no valid input exists for T.
- The Server Edge MUST convert AppliedInput to StepInput (DM-0027) before invoking the Simulation Core.
- The Server Edge MUST invoke `advance(tick, step_inputs)` with the explicit tick T per INV-0005.
- The Simulation Core MUST assert that the provided tick matches its internal state (e.g., `tick == world.tick()`).
- The Simulation Core MUST consume only StepInput and MUST NOT consume raw InputCmd or AppliedInput directly.

### Startup Behavior

In v0, the first simulation step (tick 0 → 1) uses neutral LastKnownIntent for all players because clients cannot provide inputs before receiving ServerWelcome.

**Rationale:** ServerWelcome is sent when the match starts (after all required clients connect). At that moment, `server.current_tick = 0`. ServerWelcome.TargetTickFloor = `0 + INPUT_LEAD_TICKS = 1`. Clients target tick 1 or later. The server's first `advance()` call (processing tick 0) has no client inputs, so LastKnownIntent (zero-intent) is used for all players.

Tier-0 validation focuses on responsiveness starting with the first eligible tick (tick 1 with INPUT_LEAD_TICKS=1).

## Rationale
**Why server-provided tick guidance:**
- Avoids stale client tick observations being treated as authoritative targeting information
- Eliminates first-input ambiguity (clients know exactly which tick to target)
- Reduces late-input drops compared to pure snapshot-driven targeting

**Why sequence-based tie-breaker (InputSeq):**
- Makes "latest wins" deterministic and independent of packet arrival order
- On transports with guaranteed ordering (e.g., ENet sequenced channels), InputSeq is defense-in-depth
- On unordered transports (e.g., WebTransport datagrams), InputSeq becomes required

**Why separate StepInput from AppliedInput:**
- Preserves Simulation Core boundary (INV-0004); simulation code never sees protocol-plane types
- AppliedInput is what the Server Edge selected (protocol truth); StepInput is what the sim consumes
- Prevents implementers from accidentally passing raw client messages into the sim

## Constraints & References (no prose duplication)
- Constitution IDs:
- INV-0004 (Simulation Core Isolation)
- INV-0005 (Tick-Indexed I/O Contract)
- INV-0006 (Replay Verifiability)
- INV-0007 (Deterministic Ordering & Canonicalization)
- DM-0001 (Tick)
- DM-0006 (InputCmd)
- DM-0022 (InputTickWindow)
- DM-0023 (LastKnownIntent)
- DM-0024 (AppliedInput)
- DM-0025 (TargetTickFloor)
- DM-0026 (InputSeq)
- DM-0027 (StepInput)
- Related ADRs:
- ADR-0003 (Fixed Timestep) — defines tick-driven stepping API
- ADR-0005 (v0 Networking Architecture) — transport/channel architecture
- Parameters:
- [docs/networking/v0-parameters.md](../networking/v0-parameters.md) — INPUT_LEAD_TICKS value

## Alternatives Considered
- **Client-only snapshot-driven targeting (no server guidance)** — Rejected: Stale observation leads to dropped inputs at startup and under RTT variation. Requires clients to guess server state.
- **Start delay / first-input barrier** — Acceptable as auxiliary mitigation, but insufficient as the long-term targeting model. Adds latency to match start.
- **RTT / clock-based tick estimation** — Viable later for prediction/reconciliation, but adds complexity. Server guidance achieves robust behavior with less coupling for v0.
- **Arrival-order tie-breaking** — Rejected: Non-deterministic; depends on transport and network conditions. Breaks replay verification.

## Implications
- **Enables:** Deterministic input selection independent of transport; robust startup behavior; replay verification via AppliedInput
- **Constrains:** Protocol messages (Welcome/Snapshot/InputCmd) carry additional semantics (TargetTickFloor, InputSeq); clients must implement clamping
- **Migration costs:** None (greenfield protocol)
- **Contributor impact:** Contributors must understand the input pipeline (InputCmd → AppliedInput → StepInput) and respect plane boundaries

## Follow-ups
- Update v0 multiplayer slice spec to incorporate TargetTickFloor and InputSeq semantics
- Add Tier-0 tests validating deterministic selection under duplication/reordering
- Add Tier-0 tests validating "first movement" occurs without prolonged input drops
- Document input pipeline in contributor handbook
Loading