Files
OpenFrontIO/docs/API.md
T
3a5fba2e12 Add 2v2 ranked matchmaking (#4596)
## Description:

Implements 2v2 ranked matchmaking end-to-end against the matchmaking
API's 2v2 queues (API PR #419): core team pinning, the server's second
checkin loop and game creation, and the client UI.

## Core — deterministic team pinning

The matcher's assignment specifies exactly who plays with whom (`teams:
[[a,d],[b,c]]`), but team assignment previously only did clan/friend
balancing and could scramble the ELO-balanced split.

- `PlayerSchema`/`PlayerInfo` gain an optional **`teamIndex`** — a
server-stamped index into the game's team list, part of `GameStartInfo`
so it's identical on every client (same category as `clanTag`/`friends`,
which already feed deterministic team assignment).
- `assignTeams` honors pins **unconditionally** — before clan/friend
grouping and past `maxTeamSize` (the matcher's balancing is
authoritative) — and seeds the counts that balancing of any unpinned
players sees. Pinned players still participate in the friend graph, so
an unpinned friend is pulled toward a pinned player's team.
- publicIds never enter core: the game server resolves publicId →
teamIndex per client at game start.

## Server

- **One checkin long-poll per mode.** Both loops send `mode` explicitly
(the API deployed ahead of the client, so no omit-for-back-compat
needed).
- **`get2v2Config()`**: Team mode, `playerTeams: 2`, `maxPlayers: 4`,
always-compact map, donations enabled (matching public team games),
`rankedType: "2v2"` (the API's 2v2 ingestion has shipped; `RankedType`
gains `TwoVTwo`).
- **The assignment payload is now used** (it was previously discarded):
`players` → `allowedPublicIds` so only the matched accounts can take the
slots (also hardens 1v1), and `teams` → `teamIndex` stamps at game
start. A malformed assignment logs a warning and falls back to creating
the game without pins rather than stranding matched players.
- The 3-clients-per-IP cap on public games applies to matchmade games
too (an allowlist doesn't stop one person multi-tabbing multiple
accounts). It is now skipped in dev, where local testing (multi-tab, the
4-player e2e) is inherently same-IP — matching the existing dev/prod
gating of Turnstile and the duplicate-account kick.

## Client

- Ranked modal's 2v2 card is enabled; it passes the mode through
`open-matchmaking` (dispatchers without a detail — homepage button,
requeue URL — still mean 1v1).
- Matchmaking modal joins with `&mode=1v1`/`&mode=2v2`, shows a 2v2
title (`matchmaking_modal.title_2v2` in en.json), and shows the real 2v2
ELO from the new `leaderboard.twoVtwo` field in `/users/@me` (the ranked
modal's 2v2 card does too).
- WinModal shows requeue for any ranked game and carries the mode back
into the right queue (`/?requeue=2v2`).
- 2v2 ranked stats surface in the player stats tree (labeled via
`player_stats_tree.ranked_2v2`).

## Harnesses (`tests/matchmaking/`)

- Contained: the fake server captures the `mode` query param; asserts
each queue sends its mode explicitly. **10/10.**
- E2E: `MM_MODE=2v2` runs four real browser players through the real
local worker's 2v2 queue and rides the flow into the started game.
Asserts same gameId for all four, the 2v2 config, allowlist admission,
and a **deterministic 2 vs 2 in-game split read from each client's
GameView** (the software-WebGL gate is spoofed in test pages only).
**8/8.** 1v1 e2e still **6/6.**

## Verification

- `npm test`: 2,053 tests pass, including 7 new (6 `assignTeams` pinning
unit tests + a full-game pinned-split test through `setup()`).
- `npx tsc --noEmit`, ESLint clean.
- Live e2e against a local `wrangler dev` API worker: 1v1 (6/6) and 2v2
(8/8) as above.

🤖 Generated with [Claude Code](https://claude.com/claude-code)


## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory

(UI changes — the ranked modal's 1v1/2v2 cards — were verified with
before/after screenshots in the live app during development.)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 15:22:52 -07:00

7.0 KiB
Raw Blame History

API Usage

Warning: Rate limits are very strict. Join the Discord to request higher rate limits.

Games

List Game Metadata

Get game IDs and basic metadata for games that started within a specified time range. Results are sorted by start time and paginated.

Constraints:

  • Maximum time range: 2 days
  • Maximum limit per request: 1000 games

Endpoint:

GET https://api.openfront.io/public/games

Query Parameters:

  • start (required): ISO 8601 timestamp
  • end (required): ISO 8601 timestamp
  • type (optional): Game type, must be one of [Private, Public, Singleplayer]
  • mode (optional): Game mode, must be one of [Free For All, Team]
  • rankedType (optional): Ranked type, must be one of [unranked, 1v1, 2v2]
  • playerTeams (optional): Player team configuration (e.g. Duos)
  • limit (optional): Number of results (max 1000, default 50)
  • offset (optional): Pagination offset

Example Request:

curl "https://api.openfront.io/public/games?start=2025-10-25T00:00:00Z&end=2025-10-26T23:59:59Z&type=Public&mode=Team&rankedType=unranked&limit=10&offset=5"

Response:

[
  {
    "game": "ABSgwin6",
    "start": "2025-10-25T00:00:10.526Z",
    "end": "2025-10-25T00:19:45.187Z",
    "type": "Public",
    "mode": "Team",
    "difficulty": "Medium",
    "numPlayers": 6,
    "maxPlayers": 8,
    "lobbyFillTime": 45000,
    "playerTeams": "Duos",
    "rankedType": "unranked"
  }
]

The response includes a Content-Range header indicating pagination (e.g., games 5-15/399).


Get Game Info

Retrieve detailed information about a specific game.

Endpoint:

GET https://api.openfront.io/public/game/:gameId

Query Parameters:

  • turns (optional): Set to false to exclude turn data and reduce response size

Examples:

# Full game data
curl "https://api.openfront.io/public/game/ABSgwin6"

# Without turn data
curl "https://api.openfront.io/public/game/ABSgwin6?turns=false"

Note: Public player IDs are stripped from game records for privacy.

Players

Get Player Info

Retrieve information and stats for a specific player.

Endpoint:

GET https://api.openfront.io/public/player/:playerId

Example:

curl "https://api.openfront.io/public/player/HabCsQYR"

Get Player Sessions

Retrieve a list of games & client ids (session ids) for a specific player.

Endpoint:

GET https://api.openfront.io/public/player/:playerId/sessions

Example:

curl "https://api.openfront.io/public/player/HabCsQYR/sessions"

Get Player Games

Retrieve a player's personal game history, newest first. Uses keyset (cursor) pagination rather than the page/limit scheme used elsewhere.

Endpoint:

GET https://api.openfront.io/public/player/:playerId/games

Query Parameters:

  • filter (optional): Mode bucket, one of [ffa, team, hvn, ranked]. Omit for all modes.
  • type (optional): Game type, one of [public, private, singleplayer]. Omit for all types. filter and type are orthogonal and may be combined.
  • cursor (optional): Opaque continuation token. Pass the nextCursor value from the previous response verbatim to fetch the next page — do not construct or parse it.

Response:

{
  "results": [
    {
      "gameId": "abc123",
      "start": "2026-05-17T21:04:00.000Z",
      "durationSeconds": 1234,
      "map": "World",
      "mode": "Team",
      "type": "Public",
      "playerTeams": "Duos",
      "rankedType": "unranked",
      "result": "victory",
      "totalPlayers": 8,
      "username": "alice",
      "clanTag": "ABC"
    }
  ],
  "nextCursor": "opaque-token"
}
  • result is one of [victory, defeat, incomplete] (incomplete = no recorded winner).
  • playerTeams, totalPlayers, and clanTag may be null.
  • nextCursor is null when there are no more games.
  • username/clanTag reflect the identity the player used in that specific game.

Example:

curl "https://api.openfront.io/public/player/HabCsQYR/games?filter=team&type=public"

Clans

Clan Leaderboard

Shows the top 100 clans by weighted wins.

Endpoint:

GET https://api.openfront.io/public/clans/leaderboard

Weighted wins have a half-life of 30 days to favor recent wins.

Weighted wins are calculated using the following formula:

FUNCTION calculateScore(session: ClanSession, decay: NUMBER = 1) → NUMBER
    // 1. Calculate average team size
    avgTeamSize ← session.totalPlayerCount ÷ session.numTeams

    // 2. Determine how much the clan contributed to their team
    //    (clan players divided by average players per team)
    clanMemberRatio ← session.clanPlayerCount ÷ avgTeamSize

    // 3. Apply decay factor (e.g., for older sessions)
    weightedValue ← clanMemberRatio × decay

    // 4. Calculate match difficulty based on number of teams
    //    More teams → harder to win → higher reward for victory
    //    Uses square root to avoid extreme scaling
    difficulty ← MAX(1, √(session.numTeams - 1))

    // 5. Return final score:
    //    - Win:  reward is multiplied by difficulty
    //    - Loss: penalty is divided by difficulty (less punishment in harder matches)
    IF session.hasWon THEN
        RETURN weightedValue × difficulty
    ELSE
        RETURN weightedValue ÷ difficulty
    END IF
END FUNCTION

Clan stats

Displays comprehensive clan performance statistics for a specified clan over a chosen time range. If no time range is provided, it shows lifetime stats (starting from early November 2025).

Key metrics include:

  • Total games, wins, losses, and win rate
  • Win/loss ratio and weighted win/loss ratio* broken down by:
    • Team type (e.g., 2 teams, 3 teams, duos, trios, etc)
    • Number of teams in the game (2 teams, 5 teams, 20 teams, etc)

Note: No decay is used, so weighted wins will be different from in the leaderboard.

Endpoint

GET https://openfront.io/public/clan/:clanTag

Query Parameters:

  • start (optional): ISO 8601 timestamp
  • end (optional): ISO 8601 timestamp

Example

curl https://api.openfront.io/public/clan/UN?start=2025-11-15T00:00:00Z &
end=2025-11-18T23:59:59Z

Clan Sessions

A clan session is created any time a player with that clan tag is in a public team game. If no start or end query parameter is provided, lifetime sessions (starting early November 2025) are shown.

Endpoint

GET https://api.openfront.io/public/clan/:clanTag/sessions

Query Parameters:

  • start (optional): ISO 8601 timestamp
  • end (optional): ISO 8601 timestamp
  • page (optional): Page number, 1-200 (default: 1)
  • limit (optional): Results per page, 1-50 (default: 20)

Response:

{
  "results": [ ... ],
  "total": 150,
  "page": 1,
  "limit": 20
}

Results are ordered by game start time, newest first.

Example

curl "https://api.openfront.io/public/clan/UN/sessions?start=2025-11-15T00:00:00Z&end=2025-11-18T23:59:59Z&limit=10&page=1"