Skip to content

feature: non greedy matchmaking - #372

Merged
lukepolo merged 4 commits into
mainfrom
feature/non-greedy-matching
Jul 27, 2026
Merged

feature: non greedy matchmaking#372
lukepolo merged 4 commits into
mainfrom
feature/non-greedy-matching

Conversation

@lukepolo

Copy link
Copy Markdown
Contributor

No description provided.

lukepolo and others added 4 commits July 27, 2026 08:16
Addresses review findings on the non-greedy matchmaking PR.

createMatches now loops until the queue cannot produce another match,
instead of creating one and waiting ~10-20s for the reschedule tick. A
30 player queue drained one match per pass before; it now drains all
three. Lobbies stay claimed between iterations so a lobby that misses
one match is a candidate for the next, rather than being released and
re-claimed - which churned the queue keys and pushed a redundant queue
update to everyone still waiting.

Also:
- exclude **/testing/** from tsconfig.build.json; fakeRedis.ts is not a
  *spec.ts so it was being compiled into dist
- document that BalancedTeams.unused is informational, not the requeue
  set, since createMatches tracks lock ownership itself

Adds end-to-end coverage for matches-per-pass, which is the gap that let
the single-match behaviour through: three matches from thirty players,
partial drain with a remainder, no queue churn between matches, and
stopping when the remainder cannot legally split.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Repeated queues were producing near-identical teams. With ten equally
rated solos the balancer returned only 2 distinct lineups across 200
runs, out of 126 possible - a recurring group would have seen the same
teams every night.

Two causes, both fixed:

- candidate ordering broke ties on lobby id, so the search explored the
  same region every call and returned the same first optimal split.
  Ties are now ordered by a per-call random key; injecting a constant
  rng keeps the sort stable, and therefore tests deterministic.
- the early exit fired on the first leaf, because the greedy seed had
  already set bestQuality to 0. It now waits until there are tied
  alternatives to pick between.

Ten equal solos now yield 99 distinct lineups per 200 runs, and a set
with spread ratings reaches all 5 of its perfectly balanced splits
rather than one. Search cost went down, not up: the all-equal window
resolves in 42 nodes, worst case across 300 random queues is 3688
against a 150k budget.

Stacking was never the problem - three 7000s among seven 4000s average
5200 vs 4000 together and 4800 vs 4400 apart, so the balancer already
split them 100% of the time. There is now a drift test covering it: 40
rounds where the top three keep winning and the bottom three keep
losing, asserting the winners are never stacked, lineups keep changing,
and every game stays inside 200 elo as ratings spread 3200 apart.

Follow up from the review: the two setTimeout reschedules are now an
ExpandMatchmaking delayed job keyed on matchmaking.expand.{type}.{region},
so concurrent callers collapse into one pending pass and a pass survives
a restart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two gaps in what the variety tests actually measured.

The drift test asserted lineup counts, but ratings change every round,
so a fixed candidate order produces new lineups anyway and the count
passes vacuously. The new drift test tracks which of the top three
share a team instead. With the random tie-break reverted, one pair sits
together 36 rounds out of 40 while another never shares a team at all,
and a top player never meets one of the other nine.

The early exit guard had no coverage - reverting it alone passed all 30
tests. It only matters when ratings are distinct but tightly packed, so
no two players sit the same distance from the reference and the random
tie-break has no tie to break. Across 400 random fields the guard gives
more variety on 284 and less on none, for 2x the search nodes. On a
10-player field spanning 60 elo it is the difference between 16 lineups
and exactly 1, on every seed.

Test only, no production change. 240 unit and 314 SQL tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@lukepolo
lukepolo merged commit f0a0990 into main Jul 27, 2026
2 checks passed
@lukepolo
lukepolo deleted the feature/non-greedy-matching branch July 27, 2026 13:12
theftgun added a commit to DEAFCS/api-deafcs that referenced this pull request Jul 31, 2026
createMatches assumed that once selectedPlayerCount === requiredPlayers
a valid 50/50 team split always existed, but parties are atomic. Five
2-player parties total 10 (a full Competitive lobby) yet no subset of
2s ever sums to 5, so splitIntoBalancedTeams's "a split always exists"
fallback silently produced team1=[] / team2=[all 10 players].

Added canFillTeams: a subset-sum check gating the split. When it
fails, the selected lobbies are requeued (logged) instead of a broken
match being confirmed, and the existing expand-search retry picks them
up again once a differently-sized party joins the queue.

Minimal, targeted fix — kept the current rank-grouping + combinatorial
balance search as-is rather than adopting 5stackgg/api#372's larger
matchmaking rewrite.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant