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,126 @@
# game-server — matchmaking spec delta (Wave B-5a)
## ADDED Requirements
### Requirement: QueueManager primary-priority queue
`QueueManager` SHALL maintain an in-memory priority queue keyed by an
arbitrary string (typically a game mode or queue type) and prioritise
entries by a documented mix of premium bonus, wait-time bonus, and an
optional caller-supplied base offset.
#### Scenario: addToQueue accepts a valid request
- **WHEN** a caller invokes `addToQueue("ranked", {...validRequest})`
- **THEN** the request is inserted into the queue and the queue's
size grows by one
- **AND** the `game_server.matchmaking.queue_enqueue_total` counter is
incremented
#### Scenario: addToQueue rejects when the queue is at capacity
- **GIVEN** a `QueueManager` configured with `maxQueueSize: 1`
- **AND** one request has been accepted
- **WHEN** a second request is submitted
- **THEN** `addToQueue` throws
`GameServerError(GAME_SERVER.MATCHMAKING_QUEUE_FULL)`
- **AND** the `game_server.matchmaking.queue_reject_total{reason=full}`
counter is incremented
#### Scenario: findMatch atomically pops the top N entries
- **GIVEN** three queued players
- **WHEN** the caller invokes `findMatch("ranked", 2)`
- **THEN** the top two entries (by priority) are returned in order
- **AND** the queue's size shrinks by two
- **AND** the wait-time histogram is sampled twice
### Requirement: SkillMatcher Elo-style closest-rating selection
`SkillMatcher` SHALL select the candidate closest in rating to the
seeker, widening the acceptance band proportional to the seeker's
waiting time and respecting a configurable hard ceiling.
#### Scenario: closest opponent is returned
- **GIVEN** a seeker rated 1500 and candidates rated 1400, 1510, 1700
- **AND** the configured `ratingRange` is 500
- **WHEN** the caller invokes `findMatch(seeker, candidates)`
- **THEN** the matcher returns the 1510-rated candidate
#### Scenario: rejection on a non-finite rating
- **WHEN** the caller submits a `SkillEntry` whose rating is `NaN`
- **THEN** `findMatch` throws
`GameServerError(GAME_SERVER.INVALID_SKILL_SCORE)`
### Requirement: SpectatorManager per-session cap
`SpectatorManager` SHALL enforce a configurable per-session spectator
cap and surface typed-error rejections on cap violation or duplicate
spectator registration.
#### Scenario: cap rejection
- **GIVEN** a `SpectatorManager` configured with
`maxSpectatorsPerSession: 1`
- **AND** one spectator has been added to session "s1"
- **WHEN** a second spectator is added to the same session
- **THEN** `addSpectator` throws
`GameServerError(GAME_SERVER.SPECTATOR_LIMIT_REACHED)`
#### Scenario: duplicate spectator rejection
- **GIVEN** a spectator "alice" already attached to session "s1"
- **WHEN** `addSpectator("s1", "alice", perms)` is invoked again
- **THEN** the call throws
`GameServerError(GAME_SERVER.INVALID_SPECTATOR_REQUEST)`
### Requirement: WinnerStaysOnManager streak tracking
`WinnerStaysOnManager` SHALL track the current champion, their
consecutive win streak, and a FIFO challenger queue, and expose an
`isChampionRotationDue` flag once the streak meets `maxWinStreak`.
#### Scenario: throne transfer
- **GIVEN** "alice" recorded as winner against "bob"
- **WHEN** the manager records "bob" as winner against "alice"
- **THEN** the champion becomes "bob" with `winStreak === 1`
- **AND** "alice" is enqueued at the back of the challenger list
### Requirement: MatchmakingEngine façade
`MatchmakingEngine` SHALL expose `createMatch(players, criteria,
algorithm)` and `createMatchFromQueue(queueKey, criteria, algorithm)`
that pick the algorithm, validate inputs via Zod, mint a stable
`matchId` via an injected `IdGenerator`, and notify any registered
`onMatchFound` listener.
#### Scenario: quick algorithm matches FIFO
- **GIVEN** three queued players and `MatchCriteria` of
`{ minPlayers: 2, maxPlayers: 4 }`
- **WHEN** `createMatch(players, criteria, "quick")` is invoked
- **THEN** the engine returns a `Match` containing all three players
#### Scenario: skill-based algorithm uses the largest viable group
- **GIVEN** four players with ratings 1000, 1100, 1200, 2500
- **AND** `MatchCriteria` of `{ minPlayers: 2, maxPlayers: 4, skillRange: 250 }`
- **WHEN** `createMatch(players, criteria, "skill-based")` is invoked
- **THEN** the engine returns the three rated 1000/1100/1200
#### Scenario: unknown algorithm rejection
- **WHEN** the caller passes `algorithm: "nuke"`
- **THEN** `createMatch` throws
`GameServerError(GAME_SERVER.UNKNOWN_MATCHING_ALGORITHM)`
#### Scenario: createMatchFromQueue restores on shortfall
- **GIVEN** a `QueueManager` with one queued player and
`MatchCriteria` of `{ minPlayers: 2, maxPlayers: 4 }`
- **WHEN** `createMatchFromQueue("ranked", criteria)` is invoked
- **THEN** the call returns `null`
- **AND** the queue's size remains 1