Matchmaking
Periodic-tick matchmaker (default 1 Hz). Players submit tickets with a mode, optional properties, and an optional party; a per-mode strategy module groups tickets into matches and spawns them.
Submitting a ticket
-- WebSocket
{"type": "matchmaker.add",
"payload": {
"mode": "arena",
"properties": {"skill": 1200, "region": "eu-west"}
}}%% Erlang API
{ok, TicketId} = asobi_matchmaker:add(PlayerId, #{
mode => <<"arena">>,
properties => #{skill => 1200, region => <<"eu-west">>}
}).REST equivalent:
curl -X POST http://localhost:8080/api/v1/matchmaker \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"mode": "arena",
"properties": {"skill": 1200, "region": "eu-west"}
}'Ticket shape. A ticket currently supports mode, properties, and party. A query-language extension (numeric ranges, required keys, auto skill-window expansion) is on the roadmap but not shipped — do the filtering inside your strategy module instead.
Skill-based matching
Enable the built-in skill_based strategy per mode. Tickets are sorted by properties.skill and paired within an expanding window (configurable via skill_window + skill_expand_rate).
{asobi, [
{game_modes, #{
~"ranked" => #{
module => my_arena,
match_size => 4,
strategy => skill_based,
skill_window => 200,
skill_expand_rate => 50
}
}}
]}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}
}}Cancelling
{"type": "matchmaker.remove", "payload": {"ticket_id": "..."}}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 asobi_matchmaker_fill (first-come-first-matched). For MMR-bucketed matching use asobi_matchmaker_skill. Write your own by implementing the asobi_matchmaker_strategy behaviour (a single match/2 callback):
-module(my_matchmaker).
-behaviour(asobi_matchmaker_strategy).
-export([match/2]).
%% match(Tickets, ModeConfig) -> {Matched, Unmatched}
%% Matched is a list of groups (each group is a list of tickets).
match(Tickets, Config) ->
Size = maps:get(match_size, Config, 4),
%% Bucket by skill tier, form groups of Size, return leftovers.
Buckets = bucket_by_skill(Tickets),
{Groups, Leftover} = lists:foldl(
fun(Bucket, {Gs, Left}) ->
{Full, Rest} = take_full_groups(Bucket, Size),
{Full ++ Gs, Rest ++ Left}
end,
{[], []},
Buckets),
{Groups, Leftover}.Strategy is selected per mode via the strategy key in game_modes (there is no top-level matchmaker_strategy config):
{asobi, [
{game_modes, #{
~"ranked" => #{module => my_arena, match_size => 4, strategy => my_matchmaker}
}}
]}