Docs / Matchmaking

Matchmaking

Query-based matchmaker running as a periodic tick (default 1 Hz). Players submit tickets with properties and a query; when mutually compatible tickets exist, a match is spawned and players are notified.

Submitting a ticket

JSON

-- WebSocket
{"type": "matchmaker.add",
 "payload": {
   "mode": "arena",
   "properties": {"skill": 1200, "region": "eu-west"},
   "query": "+region:eu-west skill:>=1000 skill:<=1400"
 }}

Erlang

%% Erlang API
{ok, TicketId} = asobi_matchmaker:add(PlayerId, #{
    mode       => <<"arena">>,
    properties => #{skill => 1200, region => <<"eu-west">>},
    query      => <<"+region:eu-west skill:>=1000 skill:<=1400">>
}).

REST equivalent:

curl -X POST http://localhost:8080/api/v1/matchmaker \
  -H 'Authorization: Bearer ' \
  -H 'Content-Type: application/json' \
  -d '{
    "mode": "arena",
    "properties": {"skill": 1200, "region": "eu-west"},
    "query": "+region:eu-west skill:>=1000 skill:<=1400"
  }'

Query language

Tickets include a query describing acceptable opponents. Both sides must match each other's query for a pairing to form.

+region:eu-west mode:ranked skill:>=800 skill:<=1200
  • key:value — exact match
  • +key:value — required (must match)
  • key:>=N / key:<=N — numeric range
  • Multiple conditions are AND-ed.

Skill window expansion

When a player waits too long, the matchmaker widens the skill window automatically. Each tick increments the expansion_level for unfilled tickets, relaxing numeric constraints. This trades strict skill-fairness for queue time.

Parties

Queue together — all party members land in the same match:

{"type": "matchmaker.add",
 "payload": {
   "mode": "arena",
   "party": ["player_id_2", "player_id_3"],
   "properties": {"skill": 1200},
   "query": "skill:>=1000 skill:<=1400"
 }}

Cancelling

JSON

{"type": "matchmaker.remove", "payload": {"ticket_id": "..."}}

Erlang

asobi_matchmaker:remove(PlayerId, TicketId).

Configuration

{asobi, [
    {matchmaker, #{
        tick_interval    => 1000,   %% ms between matchmaker ticks
        max_wait_seconds => 60      %% max wait before timeout
    }}
]}

Custom strategies

The default strategy is the asobi_matchmaker_fill first-come-first-matched module. For MMR-bucketed matching, use asobi_matchmaker_skill. Write your own by implementing the asobi_matchmaker_strategy behaviour:

-module(my_matchmaker).
-behaviour(asobi_matchmaker_strategy).

-export([group/2, compatible/3]).

%% group tickets into potential matches each tick
group(Tickets, _Cfg) ->
    lists:filter(fun ready_group/1,
        bucket_by(fun(#{properties := #{skill := S}}) -> S div 100 end, Tickets)).

%% return true if two tickets can play together
compatible(#{properties := A}, #{properties := B}, _Cfg) ->
    abs(maps:get(skill, A) - maps:get(skill, B)) =< 150.

Register it in config:

{asobi, [{matchmaker_strategy, my_matchmaker}]}

Where next?