Files
OpenFrontIO/resources/lang
49c358d304 feat: cancel ranked 2v2 matches that don't fill or fully spawn (#4655)
## Problem

A ranked 2v2 that starts with a missing player, or where a matched
player idles through the spawn phase, plays out as a lopsided 2v1 — and
records a ranked result (ELO) for a match that was never fair.

## Solution

Void the match from inside the sim: `WinCheckExecution` runs a one-time
check on the first check tick after the spawn phase ends. For
`rankedType === "2v2"`, if fewer than `maxPlayers` humans have spawned,
the game ends immediately with **no winner**. A player who never joined
isn't in the game start info at all, so the single spawn-count check
covers both the missing-at-start and never-spawned cases.

- `setWinner` now accepts `null` and emits a `WinUpdate` with `winner:
undefined` — the wire schema (`WinnerSchema`) already allowed an absent
winner.
- `WinModal`'s previously-empty `winner === undefined` branch shows
"Match cancelled — not all players spawned" and still votes the
winnerless result to the server, so the winner-vote consensus is reached
and the record archives promptly.
- The archived record simply has no `winner`; the API voids winnerless
ranked records (same shape any ranked game already produces when the
winner vote never reaches consensus).
- Only server change: winner votes are keyed with `JSON.stringify(winner
?? null)`, since `JSON.stringify(undefined)` is not a string and would
break the `VoteRound` key contract.

The check counts via `allPlayers()` rather than `players()`, since the
latter filters out tile-less players — which is exactly what a
never-spawned player is.

Ranked 1v1 is deliberately untouched (the existing last-human-connected
walkover still applies).

## Tests

`tests/Ranked2v2Cancel.test.ts`:
- 3 of 4 players spawn → game ends with a winnerless `WinUpdate`
- a 4th player never joined (absent from start info) → same
- all 4 spawn → no cancellation
- unranked team game short a player → no cancellation

Full suite (2094 tests), tsc, ESLint, and Prettier all pass.

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

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 11:18:26 -07:00
..
2025-06-30 19:49:42 -07:00
2026-05-31 19:55:50 -07:00
2026-03-02 10:54:37 -08:00
2025-06-30 19:49:42 -07:00
2025-09-07 19:54:22 -07:00
2026-04-17 17:31:43 -07:00
2026-02-11 14:42:59 -08:00
2026-02-11 14:42:59 -08:00
2026-06-13 08:39:35 -07:00
2025-06-30 19:49:42 -07:00
2026-03-02 10:54:37 -08:00
2026-06-13 08:39:35 -07:00
2025-09-30 11:10:34 -07:00
2026-06-13 08:39:35 -07:00
2025-09-07 19:54:22 -07:00
2025-07-13 07:09:42 +00:00
2026-06-13 08:39:35 -07:00
2026-04-17 17:31:43 -07:00
2025-06-30 19:49:42 -07:00
2026-06-13 08:39:35 -07:00
2026-03-02 10:54:37 -08:00
2025-09-30 11:10:34 -07:00
2026-02-11 14:42:59 -08:00
2026-02-11 14:42:59 -08:00
2025-09-07 19:54:22 -07:00
2026-06-13 08:39:35 -07:00
2025-09-07 19:54:22 -07:00
2025-10-17 14:27:47 -07:00
2025-08-09 23:57:10 -04:00
2025-06-30 19:49:42 -07:00
2026-02-11 14:42:59 -08:00
2026-06-13 08:39:35 -07:00
2026-05-31 19:55:50 -07:00