Tournaments
What wraps around the games: create, register, rounds, standings, completion. Each paired game itself — gate, moves, result — plays exactly as in the previous chapters, just with tournamentId set.
The whole flow
- Platform→EnginePOST /v1/tournaments (format, game type, config)req
- Client→PlatformUsers register in your lobby UI
- Platform→EnginePOST /v1/tournaments/{id}/join — per user, rating in the bodyreq
- Platform→EnginePOST /v1/tournaments/{id}/start — close registrationreq
- Engine→EngineFormat strategy generates round pairings (busy-checked; bye if odd)
- Engine→Platformmatch_found per pair (tournamentId set) — notify absenteescb
- Client→EngineConnected entrants get game_state pushed → playws
- Engine→Platformgame_result per game → standings updatecb
- Engine→EngineRound done → next pairings · arena: re-pair free players until time up
- Engine→Platformtournament_complete (final standings)cb
Phase by phase
- 1
Create
Platform → Engine ·
POST /v1/tournaments- 🏗️platform calls
POST /v1/tournaments - ⚙️engine creates the tournament
- 🆔returns
tournamentId
Field Type Required Default Description formatstring yes — arena · swiss · round_robin · knockout variantstring — "standard" one of the variants from GET /v1/game-types timeControlstring yes — a time-control id from GET /v1/game-types, e.g. "blitz_3_2" ratedboolean — true whether tournament games update Glicko-2 roundsnumber if swiss ceil(log2(n)) number of Swiss rounds roundDeadlineSecnumber — 0 per-round deadline in seconds; 0 = no deadline allowMatchmakingDuringTournamentboolean — false false = participants can't queue matchmaking while entered (exclusive) arenaobject if arena — { durationMin } — arena duration in minutes, capped 1..1440 httpPOST /v1/tournaments X-Service-Auth: <secret> { "format": "arena", // arena | swiss | round_robin | knockout "variant": "standard", "timeControl": "blitz_3_2", "rated": true, "roundDeadlineSec": 0, "allowMatchmakingDuringTournament": false, "arena": { "durationMin": 60 }, // arena only "rounds": 7 // swiss only } # 201 { "success": true, "response": "success", "responseCode": 201, "data": { "tournamentId": "trn_44" } } # Unsupported format → 422. The presence gate for tournament games is set by # the engine (grace_period), not by this request. - 🏗️platform calls
- 2
Registration
Platform → Engine ·
/join·/withdraw·GET /v1/tournaments- 📝User registers in the lobby
- ➕platform calls
POST /tournaments/{id}/join (rating in body) - 📋engine adds them to the field
Tournament join carries the rating in the body — unlike matchmaking — because it is a server-to-server call the client never touches. Entering is exclusive by default: while entered, a
queuemessage is rejected.Field Type Required Default Description userIdstring yes — participant ratingobject if rated — { r, rd, vol } for the tournament's (variant, speed) — no floor / lastGameAt here httpPOST /v1/tournaments/{id}/join { "userId": "user_123", "rating": { "r": 1512.3, "rd": 84.1, "vol": 0.0598 } } # 200 { data: null } · 409 if the user is already in an active tournament # withdraw and start take the same shape and also reply 200 { data: null }: POST /v1/tournaments/{id}/withdraw { "userId": "user_123" } // forfeits remaining games POST /v1/tournaments/{id}/start // close registration + begin # GET /v1/tournaments — lobby list: tournaments still open for registration { "tournaments": [ { "tournamentId": "trn_44", "format": "arena", "variant": "standard", "timeControl": "blitz_3_2", "rated": true, "players": 12, "durationMin": 60 } ] } - 3
Start & pairing
Platform → Engine (
/start) · then Engine (internal)- ▶️platform calls
POST /tournaments/{id}/start - 🧮format strategy pairs entrants (bye if odd)
- 🏠one room per pair
/startcloses registration. The format strategy (arena·swiss·round_robin·knockout) generates pairings, verifies each player against the busy-set, and creates a room per pair. Odd count → one player gets a bye. - ▶️platform calls
- 4
Rounds — play each game
Engine → Client (socket) · Engine → Platform (callback)
- 📡match_found per pair (tournamentId set)
- 📨engine POSTs webhook → notify absentees
- 📥connected entrants get game_state → play
jsonc{ "eventId": "evt_101", "type": "match_found", "matchId": "match_t1a", "variant": "standard", "timeControl": "blitz_3_2", "tournamentId": "trn_44", // ← set (null for plain matchmaking) "timeControlDetail": { "base": 180, "inc": 2 }, "players": [ { "userId": "user_123", "username": "magnus_fan", "color": "white" }, { "userId": "user_456", "username": "the_turk", "color": "black" } ] } // This is the one case where the match_found CALLBACK does real work: an // entrant may be OFFLINE when their round opens (they joined over REST, not // from a socket) — notify them out of band. The room waits at the grace_period // gate; when they log in and connect, the engine finds it by userId and pushes // the game state. // Optional round hint (own callback, safe to ignore): { "eventId": "evt_100", "type": "tournament_round_started", "tournamentId": "trn_44", "round": 3 } - 5
Results & standings
Engine → Platform (callback) ·
GET /v1/tournaments/{id}- 🏁each game → game_result → standings update
- 📥platform polls
GET /v1/tournaments/{id} - 🔁round done → next pairings · arena re-pairs free players
Every finished game sends its own
game_result(with rating delta when rated) and updates the standings. Round-based formats wait for the whole round before pairing the next; arena re-pairs a player as soon as they are free, until time runs out.httpGET /v1/tournaments/{id} # 200 — config + standings + current round { "success": true, "response": "success", "responseCode": 200, "data": { "standings": [ { "userId": "user_456", "rank": 1, "score": 21, "games": 14 }, { "userId": "user_123", "rank": 2, "score": 19, "games": 14 } ] } } - ✓
Complete
Engine → Platform (callback)
- 🧾no games left → final standings
- 📨engine POSTs
tournament_complete (retries forever) - ✅platform saves standings
When no games remain, the engine sends
tournament_complete— likegame_result, it retries indefinitely: no tournament outcome is ever lost.json{ "eventId": "evt_004", "type": "tournament_complete", "tournamentId": "trn_44", "standings": [ { "userId": "user_456", "rank": 1, "score": 21, "games": 14 }, { "userId": "user_123", "rank": 2, "score": 19, "games": 14 } ] }
What can go wrong
| Situation | Behaviour & handling |
|---|---|
| 409 on /join | The user is already in an active tournament — one at a time. Cleared on finish or withdraw. |
| Entrant can't queue matchmaking | By design: tournaments are exclusive by default (queue is rejected with already_in_game). Set allowMatchmakingDuringTournament:true at create time to permit it. |
| Entrant offline when the round opens | Expected — that's what grace_period is for. Use the match_found callback to notify them; the room waits. Never arriving by the deadline forfeits that game (the engine does not requeue, unlike matchmaking). |
| Entrant offline between rounds | Fine. Tournament membership is independent of the session — the session reaper never touches membership or standings. |
| Withdraw mid-tournament | POST /withdraw forfeits the player's remaining games and frees their membership. |