# Capability Spec Delta: game-server (game-server-enhanced-infra) **Package**: `@suaveplan/game-server` **Status**: PROPOSED ## ADDED Requirements ### Requirement: Game instance registry with TTL eviction and load-balancing The package SHALL expose a `GameInstanceRegistry` class that registers live game-server instances, evicts them when their last heartbeat is older than `instanceTTL` milliseconds (as reported by an injected `Clock`), supports filter-based discovery, and selects one healthy instance per request using one of four load-balancing strategies (`least-players`, `random`, `region-based`, `round-robin`). All clock and randomness reads SHALL flow through injected `Clock` and `Rng` dependencies. #### Scenario: register / discover / deregister round-trip - **WHEN** the caller registers an instance, then calls `discover()` with no filter - **THEN** the registered instance appears in the result, defaulted to `HEALTHY` - **WHEN** the caller deregisters - **THEN** the next `discover()` returns an empty array #### Scenario: heartbeat expiry triggers eviction - **WHEN** the injected clock advances past `instanceTTL * 2` since the last heartbeat - **THEN** the registry transitions the instance to `UNHEALTHY` first, then deregisters it on the second sweep - **AND** the corresponding `GameInstanceEvent.HEALTH_CHANGED` and `GameInstanceEvent.DEREGISTERED` events fire #### Scenario: least-players strategy picks lowest player count - **WHEN** two healthy instances exist with `playerCount = 5` and `playerCount = 2` - **THEN** `selectInstance(..., 'least-players')` returns the second one with `score = 2` #### Scenario: region-based strategy honours preferred region - **WHEN** the caller passes `filter.region = "eu-west"` and at least one EU instance exists - **THEN** the selected instance has `metadata.region === "eu-west"` #### Scenario: round-robin rotates across calls - **WHEN** three instances are available and `selectInstance(..., 'round-robin')` is called three times - **THEN** each call returns a different instance, then the rotation repeats **Verified by:** `src/enhanced/service-discovery/game-instance-registry/game-instance-registry.test.ts::"registers and discovers an instance"` AND `src/enhanced/service-discovery/game-instance-registry/game-instance-registry.test.ts::"evicts after TTL expiry"` AND `src/enhanced/service-discovery/game-instance-registry/game-instance-registry.test.ts::"least-players selects lowest count"` AND `src/enhanced/service-discovery/game-instance-registry/game-instance-registry.test.ts::"region-based prefers configured region"` AND `src/enhanced/service-discovery/game-instance-registry/game-instance-registry.test.ts::"round-robin rotates across calls"` --- ### Requirement: BitPacker-backed binary state synchronisation The package SHALL expose a `BitPackerStateSync` class that packs `TState` snapshots to `Uint8Array` via `@suaveplan/bitpacker`, supports client-side prediction with a bounded history window, supports server reconciliation against a tick-tagged authoritative state, supports linear interpolation between two history snapshots, and aggregates wire-byte counters into a `SyncStats` view. All timestamps SHALL flow through an injected `Clock`. Pack/unpack failures SHALL throw `GameServerError` with `BITPACK_ENCODE_FAILED` / `BITPACK_DECODE_FAILED`. #### Scenario: pack then unpack round-trip preserves state - **WHEN** the caller packs a `TState` value and unpacks the resulting `Uint8Array` - **THEN** the unpacked value equals the original #### Scenario: prediction records a history frame - **WHEN** `predict(state, input, simulate)` is called - **THEN** the returned state equals `simulate(state, input.data)` - **AND** `getStats().predictionsMade` increments by one #### Scenario: reconciliation re-applies pending inputs above server tick - **WHEN** the client has predicted three frames at ticks 11, 12, 13 and the server delivers a state at tick 10 - **AND** the client's snapshot at tick 10 differs from the server state beyond the threshold - **THEN** `reconcile(...)` returns `needed: true` with `framesResimulated = 3` and a state derived from re-applying the three pending inputs #### Scenario: interpolation lerps between snapshots - **WHEN** the caller passes two snapshots at timestamps 100 and 200 and a render time bracketing the targetTime - **THEN** the returned state equals `lerp(before, after, t)` for some `t ∈ [0, 1]` #### Scenario: bitpack failure routes to typed error - **WHEN** `BitPacker.pack` throws - **THEN** `packState` re-throws a `GameServerError` carrying code `GAME_SERVER.BITPACK_ENCODE_FAILED` **Verified by:** `src/enhanced/state-sync/bitpacker-state-sync/bitpacker-state-sync.test.ts::"packs and unpacks state"` AND `src/enhanced/state-sync/bitpacker-state-sync/bitpacker-state-sync.test.ts::"prediction records a history frame"` AND `src/enhanced/state-sync/bitpacker-state-sync/bitpacker-state-sync.test.ts::"reconciliation re-applies pending inputs above the server tick"` AND `src/enhanced/state-sync/bitpacker-state-sync/bitpacker-state-sync.test.ts::"interpolation lerps between two snapshots"` AND `src/enhanced/state-sync/bitpacker-state-sync/bitpacker-state-sync.test.ts::"throws BITPACK_ENCODE_FAILED when bitpacker throws"` --- ### Requirement: Vector-clock distributed action ordering The package SHALL expose a `VectorClockSync` class that records game actions per session, tags each action with a vector-clock snapshot, detects concurrent actions, resolves conflicts via one of three strategies (`last-write-wins`, `merge`, `custom`), produces a causality chain in happens-before order, and exposes request/response sync round-trips between nodes. The internal vector clock SHALL be the `createVectorClock` primitive from `@suaveplan/timing`. Action IDs SHALL be minted through an injected `IdGenerator`; timestamps SHALL flow through an injected `Clock`. #### Scenario: recordAction tags with current vector clock - **WHEN** `recordAction(sessionId, "move", data, tick)` is called - **THEN** the returned `GameAction.vectorClock` carries this node's counter at >= 1 - **AND** `getStats().totalActions` equals the count of recordings #### Scenario: receiveAction detects concurrent conflict - **WHEN** two actions are recorded by different nodes with vector clocks that are mutually concurrent (`compare === "concurrent"`) - **THEN** `receiveAction(remote)` returns a `ConflictResolution` with `conflictDetected: true` - **AND** the chosen winner under `last-write-wins` is the one with the larger timestamp #### Scenario: custom resolver overrides default strategy - **WHEN** the registry is constructed with a `conflictResolver` - **THEN** the resolver function is called with each (winner, candidate) pair and its return value becomes the resolved action - **AND** `ConflictResolution.strategy === "custom"` #### Scenario: causality chain sorts by happens-before - **WHEN** action A is recorded, then action B is recorded on the same node - **THEN** `getCausalityChain(sessionId).actions[0]` is A and `[1]` is B #### Scenario: sync request returns missing actions - **WHEN** node X has recorded actions [a1, a2] and node Y has recorded none - **AND** Y issues `requestSync(X, sessionId)` with its current (empty) vector clock - **THEN** X's `handleSyncRequest(request)` returns a response carrying both `a1` and `a2` **Verified by:** `src/enhanced/timing/vector-clock-sync/vector-clock-sync.test.ts::"recordAction tags with the current vector clock"` AND `src/enhanced/timing/vector-clock-sync/vector-clock-sync.test.ts::"receiveAction detects concurrent conflict and resolves last-write-wins"` AND `src/enhanced/timing/vector-clock-sync/vector-clock-sync.test.ts::"custom resolver overrides default strategy"` AND `src/enhanced/timing/vector-clock-sync/vector-clock-sync.test.ts::"causality chain sorts by happens-before"` AND `src/enhanced/timing/vector-clock-sync/vector-clock-sync.test.ts::"sync request returns missing actions"`