New

dot-matchmaking

Who plays whom — Glicko-2 ratings per queue, playlists whose tolerances start strict and widen with waiting, parties placed whole, balanced sides, an accept step that keeps everybody's place, and ratings the website computes.

The website’s matchmaking finds a server for a party. There is no player queue in it, and it had no skill rating at all until its rating routes were added for this addon, so dot-matchmaking is the whole player-queue half: who plays whom, on which server, and how the result moves everybody’s rating.

Requires dot-core. dot-party is not a dependency — a party is simply a ticket with several ids on it — and dot-auth’s backbone client is reached by duck typing.

var fives := DotMmPlaylist.of(&"ranked5", 2, 5)     # two sides of five
fives.max_party_size = 3                             # no full five-stacks against strangers

var mm := DotMatchmaker.new()
mm.playlists = [DotMmPlaylist.of(&"duel", 2, 1), fives]
mm.allocator = my_servers                            # anything with allocate(match)
add_child(mm)

mm.enqueue("t-81", &"ranked5", PackedStringArray(["ada", "bo"]),
        [{"eu": 31, "na": 110}, {"eu": 44}], party_id)
mm.match_found.connect(func(m, deadline): show_accept_prompt(m, deadline))
mm.match_ready.connect(func(m): send_everyone_to(m.allocation["address"]))

DotMatchmaker holds no socket, so it can run in a lobby server, a hub process or anything else that can hear clients.

Ratings: Glicko-2, per queue

One number says a newcomer and a thousand-game veteran on that number are the same player. Glicko-2 keeps three — a rating, a deviation (how sure we are) and a volatility — and the deviation shrinks with every game and grows back with absence, so newcomers and returning players move fast and regulars slowly.

  • The published algorithm, step for step, checked against the worked example in Glickman’s paper, to within 0.001 of the exact figures (the paper prints them from rounded intermediates).
  • A rating belongs to one queue. Skill at two-a-side and at a twelve-player free-for-all are different facts.
  • A board shows the rating minus two deviations (DotMmRating.conservative()), so two lucky wins do not top it.
  • Team games shift the opponent, not the player. Each player is rated against other_mean - (own_mean - player): everyone on a side sees the expectation the two means predict and moves by their own deviation. A 2000 on a side of 1000s loses about 6 points to an expected defeat, not the naive 20.
  • A leaver is rated as having lost, fully. Otherwise leaving a losing match is free. A player there for under a quarter of the match is not rated at all.

The queue widens with waiting

Oldest ticket first

Each pass builds a match around whoever has waited longest — the only order that cannot starve anybody. Their skill window (skill_window, skill_window_growth, skill_window_max) and quality floor (min_quality, min_quality_decay) are the relaxed ones.

Each ticket's own latency

A candidate is judged against its own latency ceiling (max_latency_ms, growing to max_latency_cap_ms). A long wait makes the waiter accept a worse match; it does not hand a newcomer eighty extra milliseconds.

Parties are never split

A party is one ticket. Its latency to a region is its worst member’s, and its matching rating leans from its mean towards its best player (party_skill_blend, premade_bonus), because a strong player carries.

Balanced sides

DotMmBalance tries every split for two sides up to sixteen tickets, and greedy-plus-swaps beyond, always moving whole parties. Deterministic.

The accept step

A decline or a timeout cools down only the person responsible (decline_cooldown_sec) and drops their ticket — the whole party, since requeueing the rest would split it. Everybody else goes back in at the time they first queued. A match with no free server falls through with nobody cooled down.

The website rates

Ratings decide who other people are made to play against, so on TMC the website computes them, not the game server. DotMmBackbone sends the skill-rating routes the result — sides, places, participation, leavers — and stores the ratings that come back:

var bb := DotMmBackbone.new()
bb.client = backbone     # dot-auth's DotBackboneClient: get_integration / post_integration
bb.store = mm.store
await bb.define(mm.playlists, mm.config)               # POST rating/define, once at boot
await bb.submit(&"ranked5", match_id, sides, placements)  # POST rating/submit, per match

The algorithm is duplicated in the site on purpose, as dot-stats’ merge rule is, and both halves test the same two figures. Without the routes — and permanently, for a self-hosted server — the matchmaker rates locally into user://matchmaking/ratings.json.

The site rates by the same rules you set

define(playlists, config) sends each queue’s τ, inactivity period, participation floor and leaver-takes-loss (tau, ratingPeriodDays, minParticipation, leaverTakesLoss), and refuses a DotMatchmakingConfig outside the site’s bounds before sending — so a ranked queue is rated by one rule offline and online. A site from before that change ignores the four keys.

A result for a queue the site has not heard of — its 404 “No playlist” — re-declares that one queue and files the result once more. A result the site refuses for its shape is not kept: flush lets it go with a warning, counts it in refused, and carries on, so one bad result cannot sit at the head of the queue holding every result behind it. A missing credential or route still stops the flush and keeps everything.

Not here yet

Backfill into a running match, short-handed matches after a long wait, role queues, and an orchestrator that starts servers on demand. allocator is one method; DotMmAllocatorList is the fixed server list a small deployment has.