# 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