Skip to content

feat: cancel ranked 2v2 matches that don't fill or fully spawn#4655

Merged
evanpelle merged 1 commit into
mainfrom
feat/ranked-2v2-cancel-unfilled
Jul 21, 2026
Merged

feat: cancel ranked 2v2 matches that don't fill or fully spawn#4655
evanpelle merged 1 commit into
mainfrom
feat/ranked-2v2-cancel-unfilled

Conversation

@evanpelle

Copy link
Copy Markdown
Collaborator

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

A ranked 2v2 that starts short a player, or where a player idles through
the spawn phase, plays out as a lopsided 2v1 and records a ranked result.
Instead, void the match: WinCheckExecution now runs a one-time check on
the first tick after the spawn phase and ends the game with no winner
when fewer than 4 players spawned (a player who never joined isn't in
the start info at all, so the same check covers both cases).

setWinner accepts null and emits a Win update with winner undefined —
the wire schema already allowed an absent winner. WinModal's empty
undefined-winner branch now shows "Match cancelled", and still votes the
winnerless result to the server so the record archives promptly. The
API treats winnerless ranked records as void.

Server-side, the only change is keying winner votes with
JSON.stringify(winner ?? null), since JSON.stringify(undefined) is not
a string.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ca1f2d2c-cfca-47df-ba41-adb89f405e4e

📥 Commits

Reviewing files that changed from the base of the PR and between da2e091 and 749a038.

📒 Files selected for processing (7)
  • resources/lang/en.json
  • src/client/hud/layers/WinModal.ts
  • src/core/execution/WinCheckExecution.ts
  • src/core/game/Game.ts
  • src/core/game/GameImpl.ts
  • src/server/GameServer.ts
  • tests/Ranked2v2Cancel.test.ts

Walkthrough

Ranked 2v2 matches now cancel without a winner when not all expected human players spawn. The result is stored and emitted consistently, server vote keys normalize missing winners, and the client shows a localized replay modal.

Changes

Ranked 2v2 cancellation

Layer / File(s) Summary
Detect and record cancellation
src/core/execution/WinCheckExecution.ts, src/core/game/Game.ts, src/core/game/GameImpl.ts
Incomplete ranked 2v2 spawns produce a winnerless game result and a Win update with an omitted winner.
Validate cancellation outcomes
tests/Ranked2v2Cancel.test.ts
Tests cover incomplete ranked games, missing players, complete spawns, and unranked games.
Propagate and display winnerless results
src/server/GameServer.ts, src/client/hud/layers/WinModal.ts, resources/lang/en.json
Missing winners use a consistent server vote key, and the client displays the localized cancellation replay modal.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WinCheckExecution
  participant GameImpl
  participant WinModal
  participant GameServer
  WinCheckExecution->>GameImpl: setWinner(null, allPlayersStats)
  GameImpl-->>WinModal: emit Win update without winner
  WinModal->>GameServer: send undefined winner
  GameServer->>GameServer: normalize vote key to null
  WinModal->>WinModal: show cancellation replay modal
Loading

Possibly related PRs

Suggested labels: UI/UX

Suggested reviewers: aotumuri, ryanbarlow97, jrouillard

Poem

Four spawn points wait in line,
Three arrive—no crown to shine.
The match is marked, the replay glows,
A quiet modal now explains what froze.
No winner lost, just none arose.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: canceling ranked 2v2 matches that do not fill or fully spawn.
Description check ✅ Passed The description is directly related to the PR and accurately describes the ranked 2v2 cancellation behavior.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@evanpelle evanpelle added this to the v33 milestone Jul 21, 2026
@evanpelle
evanpelle merged commit 49c358d into main Jul 21, 2026
14 of 15 checks passed
@evanpelle
evanpelle deleted the feat/ranked-2v2-cancel-unfilled branch July 21, 2026 18:18
@github-project-automation github-project-automation Bot moved this from Triage to Complete in OpenFront Release Management Jul 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Complete

Development

Successfully merging this pull request may close these issues.

1 participant