Initial Commit
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user