-
Notifications
You must be signed in to change notification settings - Fork 602
Open
Milestone
Description
Ranked Matchmaking and MMR: Comprehensive Design and Integration Plan
Goal
- Introduce a ranked matchmaking system with seasons, MMR, and a queue-based matchmaker, fully integrated with the existing client and server.
- Prioritize correctness, fairness, speed of delivery, and a clean migration path from MVP to scale.
High-level architecture
- Ranked Matchmaking Service (RMS): Accepts queue tickets, widens search over time, creates balanced matches, coordinates accept/ready checks, hands off to game creation.
- Rating Service: Maintains player MMR per season, applies updates post-match, persists history, exposes leaderboards.
- Lobby Coordinator: Spins up the game instance/lobby with assigned players/mode, returns server connection details, enforces accept/ready windows.
- Data Store: Relational DB for durability/auditability; Redis optional for fast, ephemeral queue state.
- Observability: Metrics (wait times, fairness deltas), structured logs, SLOs.
Recommended initial deployment
- Phase 1: In-process ranked module in the existing server, single worker.
- Phase 2: Extract to a separate process sharing DB; optional Redis for queue state and accept windows.
- Phase 3: Horizontal scale, sharded by region/mode.
MMR system
- Default: Glicko-2 per season
- Store per player per season: rating r, rating deviation RD, volatility σ.
- Init: r=1500, RD=350, σ=0.06; provisional for first 10 matches.
- Team rating: team_rating = mean(player_ratings), team_RD = sqrt(sum(RD^2))/team_size.
- Expected score E uses g(RD) dampening.
- Update per standard Glicko-2: compute v, Δ, a=ln(σ^2), solve for a’, then r’ and RD’; apply time-based RD inflation between matches.
- Placement: higher RD floor until N matches.
- Decay: increase RD after inactivity; do not forcibly lower r.
- Parties: update each member separately with their own r/RD and team result.
- Ranking tiers
- Map rating to tiers (e.g., Bronze 0–1199, Silver 1200–1399, Gold 1400–1599, etc.) with 100-point sub-divisions.
- Soft season reset: r’ = mean + α*(r - mean), α≈0.75; RD bumped to 200.
- Anti-abuse
- Dodge penalties: escalating queue timeouts; no rating change if match not started.
- Leaver penalties: ranked loss on early leave and confidence constraints on the remaining team’s MMR updates.
- Smurf detection: large deltas and rapid rating changes flagged.
Alternative MMR choices
- TrueSkill 2: Bayesian skill with teams/draws natively; superior for team games; more complex.
- Elo with dynamic K: Simple and fast; add K scaling by uncertainty/opponent delta.
- Glicko-1: Simpler than Glicko-2; slightly less adaptive.
Matchmaking algorithm
- Queue tickets
- Attributes: player_id/party_id, mode_id, region, mmr_snapshot, ping, team_size, constraints (crossplay, map pool).
- Search window: (MMR_min, MMR_max), ping_max, party size compatibility.
- Widening search over time
- T0–30s: ±100 MMR, ping ≤50ms.
- 30–90s: expand ±50 MMR and +25ms every 15s.
- 90–180s: expand up to ±400 MMR and ping ≤120ms.
- Cap by mode’s fairness targets and regional population.
- Candidate matching
- Partition queue by region → mode → party size bins.
- Find candidate sets that satisfy team size/constraints.
- Balance teams by minimizing |team_avg_mmr - global_avg_mmr|.
- Ready/accept check
- Send MatchFound with accept tokens; 10–15s accept window.
- Decline/timeout: apply dodge penalty; re-queue remaining or cancel all.
- Hand-off to game
- Create lobby, return endpoint to clients, record
ranked_matches
.
- Create lobby, return endpoint to clients, record
- Post-match updates
- Ingest result, compute ratings, write
ranked_match_participants
andranked_rating_history
, updateplayer_ranked_ratings
.
- Ingest result, compute ratings, write
Alternative matchmaking strategies
- Strict banded queues: Fixed ±MMR brackets; lower fairness drift; longer waits.
- Global pool with region weighting: Cross-region fallback with ping caps; better low-population handling.
- Role-based MMR: Track role-specific ratings; higher fairness at increased complexity.
Client integration (files to touch and flows)
- UI/UX
- Ranked panel with queue button and mode/region selectors in
src/client/graphics/layers/PlayerPanel.ts
orsrc/client/graphics/layers/OptionsMenu.ts
. - Queue state widget in
src/client/graphics/layers/GameRightSidebar.ts
: status, elapsed time, estimated wait. - Match accept modal with countdown; display accept/decline states in
src/client/graphics/layers/ExitConfirmModal.ts
or a newAcceptMatchModal.ts
. - Post-game: rating delta, tier, streak in
src/client/graphics/layers/WinModal.ts
. - Settings: ranked toggle and region lock in
src/client/graphics/layers/SettingsModal.ts
.
- Ranked panel with queue button and mode/region selectors in
- Networking messages (client → server)
- RankedQueueJoin: { modeId, region, partyId?, constraints? }
- RankedQueueLeave: { ticketId }
- RankedAcceptMatch: { ticketId, matchId, acceptToken }
- RankedDeclineMatch: { ticketId, matchId, acceptToken }
- Notifications (server → client)
- RankedQueued: { ticketId, estWait, searchWindow }
- RankedSearchUpdate: { searchWindow, position?, estWait }
- RankedMatchFound: { matchId, acceptDeadline, opponentsSummary }
- RankedMatchCancelled: { reason }
- RankedLobbyReady: { matchId, connect }
- RankedRatingUpdate: { matchId, delta, newRating, newTier }
- Server handlers to extend
- Add message handlers in
src/server/worker/websocket/handler/message/PreJoinHandler.ts
andsrc/server/worker/websocket/handler/message/PostJoinHandler.ts
for join/leave/accept/decline. - Introduce
RankedQueueHandler.ts
consolidating matchmaking commands/events. - Wire lobby creation through
src/core/GameRunner.ts
with a ranked flag and metadata.
- Add message handlers in
Alternative client integration approaches
- Minimal MVP UI: Single Ranked button and one accept modal; expand later.
- Out-of-game queue screen: Dedicated waiting room with tips/leaderboards.
- Background queue: Allow casual play while queued; prompt accept on match found.
Server integration (modules and flow)
- New modules
- RankedQueueService: ticket lifecycle, search widening, candidate selection.
- AcceptCoordinator: match found → ready check → finalization.
- RatingService: Glicko-2 updates, placement logic, history write.
- RankedController: websocket message endpoints.
- Game lifecycle hooks
- On lobby ready: persist
ranked_matches
with roster and mmr snapshot. - On game end: compute outcome, rating updates, record history, notify clients.
- On lobby ready: persist
- Persistence and idempotency
- State transitions transactional; dedupe by natural keys (match_id, player_id).
- Rating updates isolated and retriable.
Alternative service architecture
- Separate service process communicating via HTTP/gRPC and Redis streams; scales independently.
- Monolithic module in the existing server process; fastest initial delivery.
- Managed service (e.g., PlayFab/Steam): fastest to validate; least control.
Database schema (core tables)
- ranked_seasons
- id (pk), name, start_at, end_at, soft_reset_factor, created_at
- idx: (start_at), (end_at)
- player_ranked_ratings
- player_id (pk fk), season_id (pk fk), rating, rd, volatility, matches_played, wins, losses, streak, last_active_at, last_match_id
- idx: (season_id, rating desc), (player_id)
- ranked_matches
- id (pk), season_id (fk), mode_id, region, map_id, created_at, started_at, finished_at, state, average_mmr, team_size
- idx: (season_id, mode_id, created_at), (region, state)
- ranked_match_participants
- match_id (pk fk), player_id (pk fk), team, rating_before, rd_before, volatility_before, rating_after, rd_after, volatility_after, outcome, dodged, left_early, duration_seconds
- idx: (player_id, match_id), (match_id, team)
- ranked_rating_history
- id (pk), player_id (fk), season_id (fk), match_id (fk), delta, rating_after, rd_after, volatility_after, reason, created_at
- idx: (player_id, season_id, created_at desc)
- ranked_parties
- id (pk), leader_id (fk), created_at, disbanded_at
- idx: (leader_id)
- ranked_party_members
- party_id (pk fk), player_id (pk fk), joined_at, left_at
- idx: (player_id)
- ranked_queue_tickets
- id (pk), player_id or party_id, is_party, season_id, mode_id, region, mmr_snapshot, ping_ms, created_at, state, search_json
- idx: (state, region, mode_id, created_at), (player_id), (party_id)
- optional: ranked_leaderboards
- Materialized view or cached denormalization for fast top-N.
Alternative persistence approaches
- Event-sourced: Append-only rating events, materialize projections.
- Key-value + periodic compaction: Redis primary, DB as sink.
- Analytics sidecar: Columnar store (e.g., ClickHouse) for telemetry.
API contracts (examples)
Client → Server
{
"type": "RankedQueueJoin",
"modeId": "1v1",
"region": "eu-west",
"partyId": null,
"constraints": {"maxPingMs": 80}
}
Server → Client
{
"type": "RankedMatchFound",
"matchId": "m_abc",
"acceptDeadline": 1712345678,
"opponentsSummary": {"avgMmr": 1520, "teamSizes": [1,1]}
}
Post-match rating update
{
"type": "RankedRatingUpdate",
"matchId": "m_abc",
"delta": 18,
"newRating": 1538,
"newTier": "Gold-1"
}
Ready/accept and penalties
- Accept window: 10–15s; all must accept.
- Declines/timeouts: Decliner flagged, escalating queue lock (e.g., 2m, 5m, 10m).
- No rating changes: If match never started; invalidate match.
- AFK early leave: Mark loss; apply reduced rating protection to teammates.
Seasons and leaderboards
- Season roll: Soft reset, RD inflation; optionally maintain all-time rating separately.
- Leaderboards: Per season; top N with tier filters; weekly highlights.
- Rewards: Snapshot tiers at end_of_season for cosmetics.
Testing and rollout
- Unit tests
- Rating math: golden tests vs reference Glicko-2 vectors.
- Matchmaking selection under synthetic queues; fairness and time-to-match.
- Simulation
- Agent-based simulation of populations and ping/MMR distributions; track fairness/wait time distributions.
- Load tests
- Burst queue and accept storm; DB contention and Redis TTL churn.
- Observability
- Metrics: p50/p90 queue times, fairness delta by mode, accept rate, dodge rate, rating error drift.
- Feature flags
- Enable by mode/region; shadow compute MMR and compare to control.
- Rollback
- Gate rating writes; ability to invalidate a season quickly.
Implementation sequence
- Step 1: Schema migrations, read models, admin tools for seasons.
- Step 2: Queue join/leave plumbing and simple same-MMR matches end-to-end (dev region).
- Step 3: Ready/accept workflow; lobby creation and back-out safety.
- Step 4: Post-match ingestion and rating updates; emit deltas to client.
- Step 5: Search widening and fairness balancing.
- Step 6: Placement, tiers, and leaderboards; inactivity RD inflation.
- Step 7: Scaling: move queue state to Redis; optional service split.
- Step 8: Telemetry SLOs and alerting; anti-dodge and anti-smurf policies.
Specific changes in this codebase
- Server
- Add
RankedQueueService
,AcceptCoordinator
,RatingService
modules. - Extend websocket message handling in
src/server/worker/websocket/handler/message/PreJoinHandler.ts
andsrc/server/worker/websocket/handler/message/PostJoinHandler.ts
with ranked join/leave/accept. - Add ranked-aware lobby creation path in
src/core/GameRunner.ts
. - Add post-game hook to emit result into RatingService.
- Add
- Client
- Add Ranked UI in
src/client/graphics/layers/PlayerPanel.ts
andsrc/client/graphics/layers/OptionsMenu.ts
. - Add queue status in
src/client/graphics/layers/GameRightSidebar.ts
. - Add accept modal; wire to
src/client/graphics/layers/ExitConfirmModal.ts
or a new layer. - Post-game rating in
src/client/graphics/layers/WinModal.ts
.
- Add Ranked UI in
- Tooling
- Add DB migrations for ranked tables.
- Add endpoint schemas in
src/core/ExpressSchemas.ts
orsrc/core/WorkerSchemas.ts
. - Add analytics events for queue lifecycle.
Operations and safeguards
- Idempotent rating writes keyed by (player_id, match_id).
- Ticket TTLs for stale entries.
- Strict state machine: ticket → candidate → matchFound → accepted → lobbyReady → started → finished/cancelled.
- Privacy: Only expose public ranking data.
Concrete configuration defaults
- Glicko-2 init: r=1500, RD=350, σ=0.06, RD floor 50, RD inflation 1.5/month inactivity.
- Queue widening: ±100 MMR, +50 every 30s up to ±400; ping cap 50→120ms.
- Accept window: 12s, dodge lockouts: 2m, 5m, 10m; reset daily.
- Placement matches: First 10 with higher RD floor.
Fastest MVP cut
- Scope: Single mode (1v1) and single region.
- State: In-process queue with array-based candidates.
- MMR: Elo with dynamic K; upgrade to Glicko-2 in Phase 2.
- Constraints: No parties initially; no cross-region fallback.
Alternatives to major decisions
- Architecture
- In-process module first, split later.
- Separate service with HTTP/gRPC from day one.
- Managed matchmaking platform integration for rapid validation.
- Queue state
- In-memory plus periodic checkpoints.
- Redis primary with consumer groups for accept windows.
- Database-only with careful indexing.
- Ready checks
- Instant start without accept for low ranks.
- Double-confirm accept to reduce dodges.
- Pre-commit ready (soft match) then finalize after lobby allocation.
- Season management
- Hard reset each season.
- Soft reset with tier floors.
- Rolling seasons per mode.
- Leaderboards
- Live computed from
player_ranked_ratings
. - Materialized leaderboard refreshed every minute.
- Cached in Redis with write-through on updates.
- Live computed from
Risks and mitigations
- Low-population queues: Gradual cross-region with ping caps and clear UX.
- Party fairness: Avoid large parties in small modes; optionally split parties.
- Rating inflation/deflation: Monitor drift; tune σ and RD floors per season.
- Cheating/abuse: Escalate penalties; detect anomaly deltas.
mucahitdev and Elliatom
Metadata
Metadata
Assignees
Labels
No labels
Type
Projects
Status
Triage