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