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:<=1200key: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}]}