Docs / Matchmaking

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}
    }}
]}

Where next?