127 lines
4.8 KiB
Markdown
127 lines
4.8 KiB
Markdown
# 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
|