# Capability Spec Delta: `game-server` (Wave A — core foundation) **Package**: `@suaveplan/game-server` **Status**: PROPOSED > Every requirement below carries a `**Verified by:**` line at authoring time. ## ADDED Requirements ### Requirement: `IGameLogic` strategy contract The package SHALL export an `IGameLogic` interface declaring `validateMove`, `processMove`, `getGameState`, `isGameOver`, `getGameResult`, `initializeGame`, and `dispose`, fully typed without `any`, plus a runtime conformance harness `runIGameLogicConformance(factory, fixtures)` that exercises every method and returns a `ConformanceCheckResult[]`. #### Scenario: a compliant implementation passes every harness scenario - **WHEN** a caller passes a factory returning a complete implementation (correct return shapes, validation truthy + falsy branches, terminal state) - **THEN** every entry in the returned list has `passed === true` **Verified by:** `src/core/interfaces/i-game-logic/i-game-logic.test.ts::"passes every scenario for a compliant implementation"` AND `"reports failure when an impl violates the contract"` --- ### Requirement: `IPlayerManager` lifecycle contract The package SHALL export an `IPlayerManager` interface with the eleven methods `connect`, `disconnect`, `getConnection`, `getSessionPlayers`, `getAllConnections`, `heartbeat`, `checkTimeouts`, `updateMetadata`, `getPlayer`, `startHeartbeatMonitor`, `stopHeartbeatMonitor`, plus a conformance harness `runIPlayerManagerConformance(factory)` that exercises each. #### Scenario: a compliant implementation passes every harness scenario - **WHEN** the conformance harness runs against a Map-backed reference impl - **THEN** every entry in the result list has `passed === true` **Verified by:** `src/core/interfaces/i-player-manager/i-player-manager.test.ts::"passes every scenario for a compliant implementation"` AND `"reports session/all/getPlayer failures for an empty-returning impl"` --- ### Requirement: `ISessionManager` hierarchical contract The package SHALL export an `ISessionManager` interface supporting unlimited parent/child nesting with O(1) lookup by id, cascading destroy, partial- metadata update, and async `persist` / `restore`, plus an async conformance harness `runISessionManagerConformance(factory)`. #### Scenario: destroySession cascades to descendants - **WHEN** a `venue` containing a `table` is destroyed - **THEN** the harness reports `destroySession removes the node and its descendants` as passed; an implementation that fails to cascade reports the scenario as failed **Verified by:** `src/core/interfaces/i-session-manager/i-session-manager.test.ts::"passes every scenario for a compliant implementation"` AND `"reports failure when destroySession does not cascade to children"` --- ### Requirement: `IStateManager` observable-state contract The package SHALL export an `IStateManager` interface declaring `getState`, `update`, `updateMany`, `setState`, `subscribe`, `reset`, `getHistory`, and `clearHistory`, plus a conformance harness `runIStateManagerConformance(factory, fixtures)`. #### Scenario: subscribers receive events for every mutation - **WHEN** the harness calls `update`, `updateMany`, and `setState` against a compliant manager - **THEN** the harness records at least three observed events; the unsubscribe handle stops further events **Verified by:** `src/core/interfaces/i-state-manager/i-state-manager.test.ts::"passes every scenario for a compliant implementation"` AND `"flags failure when unsubscribe does not stop events"` --- ### Requirement: `GameEvents` map and `GAME_EVENT_KEYS` registry The package SHALL export a typed `GameEvents` map covering player, session, game, move, and error event families, plus a frozen `GAME_EVENT_KEYS` array enumerating every well-known key and an `isGameEventKey(value)` predicate. #### Scenario: predicate narrows arbitrary strings - **WHEN** `isGameEventKey("player:connected")` is called - **THEN** it returns `true`; for `"not:a:real:event"` it returns `false` **Verified by:** `src/core/events/game-events/game-events.test.ts::"returns true for every registered key"` AND `"returns false for unknown keys"` --- ### Requirement: `GameEventEmitter` typed pub/sub The package SHALL export a `GameEventEmitter` class supporting `on`/`once`/ `off`/`emit`, ergonomic helpers `safeEmit` / `waitFor(timeoutMs?)` / `onN` / `onMultiple`, a configurable per-key listener cap (default 100), and `removeAllListeners` / `listenerCount` introspection. #### Scenario: safeEmit routes listener errors to the error channel - **WHEN** a listener subscribed to `game:started` throws - **THEN** `safeEmit` returns `false` (no successful listener), re-emits the error as `error` payload on the `"error"` channel, and does not throw **Verified by:** `src/core/events/game-event-emitter/game-event-emitter.test.ts::"safeEmit routes listener errors to the error channel"` AND `"safeEmit wraps non-Error throws into Error"` #### Scenario: listener cap is enforced - **WHEN** more than `maxListenersPerKey` listeners are registered on one key - **THEN** `on(...)` throws `GameServerError(LISTENER_LIMIT_EXCEEDED)` **Verified by:** `src/core/events/game-event-emitter/game-event-emitter.test.ts::"throws GameServerError(LISTENER_LIMIT_EXCEEDED) past the cap"` #### Scenario: waitFor rejects on timeout - **WHEN** `waitFor("game:started", 5)` is called and no emission occurs - **THEN** the returned promise rejects with `GameServerError(EVENT_WAIT_TIMEOUT)` **Verified by:** `src/core/events/game-event-emitter/game-event-emitter.test.ts::"waitFor rejects with GameServerError on timeout"` --- ### Requirement: `AbstractGameLogic` Template-Method base The package SHALL export an `AbstractGameLogic` abstract class implementing `IGameLogic`, accepting `{ initialState, clock?, idGenerator?, meterName? }`, orchestrating `processMove` as validate → before hook → apply → install → after hook, managing a player roster with typed-error duplicate / missing handling, and emitting OTel instruments `game_server.logic.tick_duration_ms` (Histogram), `game_server.logic.moves_total` (Counter), and `game_server.logic.rejections_total` (Counter). #### Scenario: processMove on a valid move records duration + total - **WHEN** a subclass processes a valid move with an injected fake clock - **THEN** the histogram and `moves_total` counter are recorded; the state installs from `applyMove` **Verified by:** `src/core/abstract-game-logic/abstract-game-logic.test.ts::"processMove records OTel instruments"` AND `"processMove validates, applies, mutates state, runs hooks"` #### Scenario: processMove on an invalid move throws and increments rejections - **WHEN** `validateMove` returns `{ valid: false }` - **THEN** `processMove` throws `GameServerError(INVALID_MOVE)` with the reason in `error.context`; `rejections_total` increments **Verified by:** `src/core/abstract-game-logic/abstract-game-logic.test.ts::"processMove throws GameServerError(INVALID_MOVE) on rejection"` #### Scenario: roster mutations throw typed errors - **WHEN** `addPlayer` is called twice with the same id, or `removePlayer` is called on an unknown id - **THEN** `GameServerError(PLAYER_ALREADY_EXISTS)` / `(PLAYER_NOT_FOUND)` is thrown respectively **Verified by:** `src/core/abstract-game-logic/abstract-game-logic.test.ts::"addPlayer throws GameServerError(PLAYER_ALREADY_EXISTS) on duplicate"` AND `"removePlayer throws GameServerError(PLAYER_NOT_FOUND) for unknown id"` --- ### Requirement: error-code registry extended for Wave A The package SHALL extend `GAME_SERVER_CODES` with `INVALID_MOVE`, `PLAYER_ALREADY_EXISTS`, `PLAYER_NOT_FOUND`, `LISTENER_LIMIT_EXCEEDED`, `INVALID_LISTENER_COUNT`, and `EVENT_WAIT_TIMEOUT`. Every new code MUST be thrown by at least one production path inside `src/`. #### Scenario: every new code is reachable from production - **WHEN** the unit suites for `AbstractGameLogic` and `GameEventEmitter` run - **THEN** each of the six new codes is observed thrown by at least one test scenario **Verified by:** `src/errors/game-server-error.test.ts::"registers all expected codes under GAME_SERVER namespace"` AND `src/core/abstract-game-logic/abstract-game-logic.test.ts::"processMove throws GameServerError(INVALID_MOVE) on rejection"` AND `src/core/events/game-event-emitter/game-event-emitter.test.ts::"onN throws GameServerError(INVALID_LISTENER_COUNT) for non-positive counts"` --- ### Requirement: subpath exports per feature module The package SHALL declare a `package.json` `exports` subpath for every Wave A feature module so consumers can tree-shake at module-level granularity. #### Scenario: each new feature dir has its own export - **WHEN** a consumer imports `@suaveplan/game-server/core/events/game-event-emitter` or any other Wave A subpath - **THEN** the import resolves to the feature module's compiled `index.js` **Verified by:** `bunx turbo build --filter=@suaveplan/game-server` (vite-plugin-dts emits matching `.d.ts` per subpath)