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,105 @@
# Wave A — `@suaveplan/game-server` core foundation
**Package**: `@suaveplan/game-server`
**Status**: PROPOSED
**Type**: extends existing genesis package (0.1.0)
**Design**: ./design.md (inline below)
**Spec delta**: ./specs/game-server/spec.md
## Why
Wave A ships the core foundation of `@suaveplan/game-server` that every
later wave (engine, session, state, matchmaking, transport) depends on:
the four core interfaces (`IGameLogic`, `IPlayerManager`,
`ISessionManager`, `IStateManager`) with runtime conformance harnesses,
the `GameEvents` typed payload map plus its `GAME_EVENT_KEYS` registry,
the `GameEventEmitter` typed pub/sub with ergonomic helpers
(`safeEmit`, `waitFor`, `onN`, `onMultiple`), and the
`AbstractGameLogic` Template-Method base class for concrete games. The
package already shipped `DIContainer`, the `GameServerError` registry,
and the Zod-validated core types in the prior change
`enterprise-game-server-core`; Wave A is an additive extension that
fills the interfaces / events / abstract-base layer behind a single
0.1.0 publish.
Without this Wave the downstream waves (B: session manager + player
manager, C: state manager + replay, D: matchmaking + transport plugins)
have no contract to implement against. Shipping it as one isolated wave
keeps the surface reviewable, the coverage gate enforceable on the
whole package, and the dependent slices unblocked.
## What changes
- ADDED: `core/types/core.types.ts` — `ConnectionStatus`,
`PlayerConnection`, `SessionNode`, `SessionData`, `StateChangeEvent`,
`GameEvents` map.
- ADDED: `core/interfaces/i-game-logic/` — `IGameLogic<TState, TMove>`
interface and `runIGameLogicConformance` harness.
- ADDED: `core/interfaces/i-player-manager/` — `IPlayerManager` and
`runIPlayerManagerConformance`.
- ADDED: `core/interfaces/i-session-manager/` — `ISessionManager` and
`runISessionManagerConformance`.
- ADDED: `core/interfaces/i-state-manager/` — `IStateManager<TState>`
and `runIStateManagerConformance`.
- ADDED: `core/events/game-events/` — `GAME_EVENT_KEYS` array +
`isGameEventKey` predicate.
- ADDED: `core/events/game-event-emitter/` — `GameEventEmitter` typed
pub/sub with `safeEmit` / `waitFor` / `onN` / `onMultiple` / per-key
listener cap.
- ADDED: `core/abstract-game-logic/` — `AbstractGameLogic<TState, TMove>`
Template-Method base class with injected `Clock` + `IdGenerator` and
OTel instrumentation.
- EXTENDED: `errors/game-server-error.ts` — six new codes
(`INVALID_MOVE`, `PLAYER_ALREADY_EXISTS`, `PLAYER_NOT_FOUND`,
`LISTENER_LIMIT_EXCEEDED`, `INVALID_LISTENER_COUNT`,
`EVENT_WAIT_TIMEOUT`) raised by the new core surface.
- EXTENDED: root `src/index.ts` — additive named re-exports for the new
Wave A surface.
- EXTENDED: `package.json` exports map — 8 new subpath entries, one per
feature module.
## Impact
### Affected dependents
`bunx turbo ls --filter=...@suaveplan/game-server` enumerates dependents
of this package. As of Wave A landing, no genesis package consumes
`@suaveplan/game-server` yet — this is a foundation wave for
`@suaveplan/game-loop`, `@suaveplan/game-state-schemas`, and the
`api-plugin-game-server` family that will land in Waves B–D.
- `@suaveplan/game-server` (self) — [x] tests run / [x] docs reviewed /
[ ] no action needed.
- No external dependents — verified by `bunx turbo ls
--filter=...@suaveplan/game-server`.
## Parity Checklist
| Capability | Upstream | Migrated | Notes |
| --- | --- | --- | --- |
| `IGameLogic` interface | YES | YES | Genesis tightens `any` → generic `TState`/`TMove`. |
| Inline `AbstractGameLogic` in `IGameLogic.ts` | YES | UP | Genesis splits to its own feature dir. |
| `IPlayerManager` interface | YES | YES | Returns `ReadonlyArray<PlayerConnection>`. |
| `ISessionManager` interface | YES | YES | Returns `ReadonlyArray<SessionNode>`. |
| `IStateManager` interface | YES | YES | `Readonly<TState>` instead of `DeepReadonly`. |
| `GameEvents` map | YES | UP | Variadic tuples collapsed to single-payload objects. |
| `GameEventEmitter` extending Node `EventEmitter` | YES | UP | Self-contained typed emitter; same surface +typed errors. |
| Wildcard `[key: string]: any[]` index signature | YES | NO | Scoped out: closed map keeps lint/type safety; downstream needing wildcard analytics can subscribe to `GAME_EVENT_KEYS` directly. |
| `AbstractGameLogic` Template Method | YES | UP | Adds injected `Clock` + `IdGenerator`, typed errors, OTel instruments. |
| `cloneState` JSON round-trip | YES | YES | |
| Hook methods (`beforeMoveProcessing`, etc.) | YES | YES | |
| Player roster (`addPlayer` / `removePlayer`) | YES | UP | Throws `GameServerError` not plain `Error`. |
| Conformance harnesses | NO | UP | Genesis-original — makes interfaces runtime-testable. |
Rationale for the single scope-out (`wildcard analytics:*`): the
upstream signature `[key: symbol]: any[]` defeats type safety and is
unused inside the `core/` slice; downstream analytics packages can
extend `GameEvents` via module augmentation if needed.
## Migration
Wave A is additive — no existing public-surface item is renamed or
removed. Consumers of the prior `0.1.0` surface (`DIContainer`,
`GameServerError`, Zod parsers, types) keep working unchanged. The new
error codes extend the registry; pattern-match consumers should not
care because none of the new codes were thrown previously.

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)

View File

@@ -0,0 +1,55 @@
# Tasks — Wave A: `@suaveplan/game-server` core foundation
## 0. Pre-flight (non-negotiable)
- [x] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work
- [x] 0.2 Account for every `.claude/worktrees/agent-*` — operating inside `agent-a3cb7ed021fa6e13a` for this change
- [x] 0.3 `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count — verified unchanged (no new packages added; this is an in-package extension)
## 1. Spec authoring
- [x] 1.1 Read `../suaveplan/packages/universal/game-server/src/core/` upstream sources
- [x] 1.2 Read existing `openspec/changes/enterprise-game-server-core/proposal.md` to align with prior scope decisions
- [x] 1.3 Read `openspec/AGENTS.md` Sections 13 and 14
- [x] 1.4 Draft `proposal.md`
- [x] 1.5 Source mapping documented in the Parity Checklist
- [x] 1.6 Draft `specs/game-server/spec.md`
- [x] 1.7 Every `### Requirement:` carries a `**Verified by:**` line
## 2. Implementation
- [x] 2.1 `src/core/types/core.types.ts` — `ConnectionStatus`, `PlayerConnection`, `SessionNode`, `SessionData`, `StateChangeEvent`, `GameEvents`
- [x] 2.2 `src/core/interfaces/i-game-logic/` — interface + `runIGameLogicConformance`
- [x] 2.3 `src/core/interfaces/i-player-manager/` — interface + `runIPlayerManagerConformance`
- [x] 2.4 `src/core/interfaces/i-session-manager/` — interface + `runISessionManagerConformance`
- [x] 2.5 `src/core/interfaces/i-state-manager/` — interface + `runIStateManagerConformance`
- [x] 2.6 `src/core/events/game-events/` — `GAME_EVENT_KEYS` + `isGameEventKey`
- [x] 2.7 `src/core/events/game-event-emitter/` — `GameEventEmitter` with all helpers
- [x] 2.8 `src/core/abstract-game-logic/` — `AbstractGameLogic` with Clock + IdGenerator + OTel
- [x] 2.9 Extend `src/errors/game-server-error.ts` — 6 new codes
- [x] 2.10 Extend root `src/index.ts` and `package.json` exports
## 3. Tests
- [x] 3.1 Unit tests for every implementation file (one `.test.ts` per `.ts`)
- [x] 3.2 Conformance harness tests against compliant + non-compliant reference impls
- [x] 3.3 Every `it(...)` carries `{ timeout: 5000 }`
- [x] 3.4 OTel assertion via `flushTestMetrics()` from the existing polyfill
## 4. Documentation
- [x] 4.1 Sibling `.md` for every implementation `.ts`
- [x] 4.2 Existing `README.md` covers the package (Wave A surface auto-discoverable via re-exports; no README rewrite required for the core extension)
## 5. Pre-archive verification (the 5-gate triple)
- [ ] 5.1 `bun run check` — Biome 0 errors across the package
- [ ] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/game-server` — green
- [ ] 5.3 `bunx turbo test:coverage --filter=@suaveplan/game-server` — 100/100/100/100
- [ ] 5.4 Banned-pattern scan returns empty
- [ ] 5.5 `bunx turbo build --filter=@suaveplan/game-server` — green
## 6. Archive (NOT performed in this worktree)
- [ ] 6.1 Fold delta into `openspec/specs/game-server/spec.md` (orchestrator step)
- [ ] 6.2 Move folder to `openspec/archive/<yyyy-mm-dd>-game-server-core/` (orchestrator step)