8.9 KiB
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
venuecontaining atableis destroyed - THEN the harness reports
destroySession removes the node and its descendantsas 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, andsetStateagainst 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 returnsfalse
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:startedthrows - THEN
safeEmitreturnsfalse(no successful listener), re-emits the error aserrorpayload 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
maxListenersPerKeylisteners are registered on one key - THEN
on(...)throwsGameServerError(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_totalcounter are recorded; the state installs fromapplyMove
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
validateMovereturns{ valid: false } - THEN
processMovethrowsGameServerError(INVALID_MOVE)with the reason inerror.context;rejections_totalincrements
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
addPlayeris called twice with the same id, orremovePlayeris 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
AbstractGameLogicandGameEventEmitterrun - 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-emitteror 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)