feature: non greedy matchmaking - #372
Merged
Merged
Conversation
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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.