Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,191 @@
<!-- markdownlint-disable MD024 -->
# 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<TState, TMove>` 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<TState extends object>` 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<TState, TMove>` 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)