Erlang API reference
Asobi is a plain Erlang/OTP library. You can use it directly from Erlang (or any BEAM language) without touching Lua — Lua is just a convenience layer that dispatches to these same modules.
When to use Erlang over Lua: you want behaviour-level control (supervision trees, custom match state machines, direct gen_statem handling), or you're embedding Asobi in an existing Erlang application.
asobi_match — the game behaviour
Every game mode is an Erlang module that implements the asobi_match behaviour. This is the canonical contract — the Lua adapter implements it on your behalf.
-module(my_card_game).
-behaviour(asobi_match).
-export([init/1, join/2, leave/2, handle_input/3, tick/1, get_state/2]).
init(Config) ->
{ok, #{deck => shuffle(Config), players => #{}, pot => 0}}.
join(PlayerId, #{players := Players} = State) ->
{ok, State#{players := Players#{PlayerId => starting_hand()}}}.
leave(PlayerId, #{players := Players} = State) ->
{ok, State#{players := maps:remove(PlayerId, Players)}}.
handle_input(PlayerId, #{action := bet, amount := N}, State) ->
{ok, place_bet(PlayerId, N, State)}.
tick(State) ->
case round_complete(State) of
true -> {finished, summary(State), State};
false -> {ok, State}
end.
get_state(PlayerId, State) ->
redact_for(PlayerId, State).init/1
-callback init(Config :: map()) -> {ok, GameState :: term()}.Called once when the match starts. Receives the config passed to `asobi_match_server:start_link/1`. Returns the initial game state.
join/2, leave/2
-callback join(PlayerId :: binary(), GameState :: term()) ->
{ok, GameState1 :: term()} | {error, Reason :: term()}.
-callback leave(PlayerId :: binary(), GameState :: term()) ->
{ok, GameState1 :: term()}.Player joined or left. `join` may reject with `{error, Reason}` (e.g. match full, banned); `leave` is best-effort and cannot fail.
handle_input/3
-callback handle_input(PlayerId :: binary(), Input :: map(), GameState :: term()) ->
{ok, GameState1 :: term()} | {error, Reason :: term()}.Player action (click, move, ability). Inputs arrive asynchronously — the match server serialises them onto the match process, so you never race on state.
tick/1
-callback tick(GameState :: term()) ->
{ok, GameState1 :: term()} | {finished, Result :: map(), GameState1 :: term()}.Called on a fixed interval (default 10 Hz). Return `{finished, Result, State}` to end the match — Asobi persists the result, broadcasts to clients, and tears down the process.
get_state/2
-callback get_state(PlayerId :: binary(), GameState :: term()) ->
StateForPlayer :: map().Project the full match state into what ONE player sees. Use this to hide opponent hands, enemy positions outside of sight, etc.
Optional: phases, voting
-callback phases(Config :: map()) -> [asobi_phase:phase_def()].
-callback on_phase_started(PhaseName :: binary(), GameState :: term()) ->
{ok, GameState1 :: term()}.
-callback on_phase_ended(PhaseName :: binary(), GameState :: term()) ->
{ok, GameState1 :: term()}.
-callback vote_requested(GameState :: term()) ->
{ok, VoteConfig :: map()} | none.
-callback vote_resolved(Template :: binary(), Result :: map(), GameState :: term()) ->
{ok, GameState1 :: term()}.All optional. Implement `phases/1` to drive a phase state machine; implement the vote callbacks to react to in-match voting.
asobi_match_server — match lifecycle
Your behaviour module is hosted by asobi_match_server, a gen_statem that drives the tick loop and routes player input. You rarely start one directly — the matchmaker does that — but you can for tests or custom lobbies.
asobi_match_server:start_link(Config)
Start a new match. Config must include `callback_module` and may include `tick_rate_ms`, `min_players`, `max_players`, `mode`.
{ok, Pid} = asobi_match_server:start_link(#{
callback_module => my_card_game,
tick_rate_ms => 100,
min_players => 2,
max_players => 4,
mode => ~"ranked"
}).asobi_match_server:join/2, :leave/2, :handle_input/3
Attach a player, detach, forward input. These are the hot-path calls — input is a cast (fire-and-forget), join is a call (awaits accept/reject).
ok = asobi_match_server:join(Pid, <<"p_alice">>),
asobi_match_server:handle_input(Pid, <<"p_alice">>, #{action => bet, amount => 50}),
asobi_match_server:leave(Pid, <<"p_alice">>).asobi_match_server:pause/1, :resume/1, :cancel/1
Administrative control. `pause` stops the tick and queues input; `resume` continues; `cancel` ends the match without a winner.
asobi_match_server:whereis(MatchId)
Locate the Pid for a given match ID. Uses the `pg` scope so this works across cluster nodes.
case asobi_match_server:whereis(MatchId) of
{ok, Pid} -> asobi_match_server:handle_input(Pid, PlayerId, Input);
{error, not_found} -> {error, match_gone}
end.asobi_match_server:start_vote/2, :cast_vote/4, :use_veto/3
In-match voting. `start_vote` opens a ballot; players cast with `cast_vote`; holders of a veto token can cancel with `use_veto`.
asobi_matchmaker — queues
Pluggable matchmaking. Add a ticket, the matchmaker groups compatible players via a strategy module, and spawns a match when a valid group forms.
asobi_matchmaker:add(PlayerId, Params)
Add a player to the queue. Params include the mode, skill, region, etc.; the strategy module decides how to use them.
{ok, TicketId} = asobi_matchmaker:add(<<"p_alice">>, #{
mode => ~"ranked",
skill => 1250,
region => eu_west
}).asobi_matchmaker:remove(PlayerId, TicketId)
Cancel a ticket. Safe to call even if already matched — it's a cast.
asobi_matchmaker:get_ticket(TicketId)
Poll a ticket's status. Returns the full ticket map including `status` (`waiting` / `matched` / `expired`).
case asobi_matchmaker:get_ticket(TicketId) of
{ok, #{status := matched, match_id := Id}} -> join_match(Id);
{ok, #{status := waiting}} -> wait();
{error, not_found} -> expired
end.asobi_world_server — persistent worlds
For games with a shared, persistent space (MMO, open-world, sandbox). A world process orchestrates zones (spatial partitions) and routes player I/O.
asobi_world_server:join(Pid, PlayerId)
Attach a player to the world. The world picks their initial zone based on spawn rules or a saved position.
asobi_world_server:move_player(Pid, PlayerId, {X, Y})
Move a player. The world recomputes zone membership and updates subscriptions.
asobi_world_server:move_player(WorldPid, PlayerId, {1024, 768}).asobi_world_server:spawn_at/3, :spawn_at/4
Spawn an entity from a template at a position. The optional 4th arg overrides template fields.
{ok, EntityId} = asobi_world_server:spawn_at(WorldPid,
<<"goblin_warrior">>,
{512, 512},
#{hp => 150}).asobi_zone — spatial partitions
A zone is one Erlang process managing a rectangular region of the world. Zones are lazy-spawned, reaped when empty, and crash-isolated.
asobi_zone:subscribe(ZonePid, PlayerId)
Subscribe a player to a zone's event stream. From now on they receive entity updates and chat from this zone.
asobi_zone:query_radius(ZonePid, {X, Y}, Radius)
Return all entities within `Radius` of a point. Uses the zone's internal spatial grid — O(k) where k is matches, not total entities.
{ok, Entities} = asobi_zone:query_radius(ZonePid, {100, 200}, 50).asobi_zone:spawn_entity/3, :spawn_entity/4
Spawn into a specific zone. Usually called via `asobi_world_server:spawn_at/3` which routes to the correct zone for you.
asobi_spatial — in-memory spatial queries
Stateless helpers for querying a list of entities by position. Use these inside match state when you don't need the zone infrastructure.
asobi_spatial:query_radius(Entities, {X, Y}, Radius)
Entities is a list of maps with `x` and `y` keys. Returns the subset within radius.
Nearby = asobi_spatial:query_radius(Entities, {0, 0}, 100),
lists:foreach(fun(E) -> notify(E) end, Nearby).asobi_spatial:nearest(Entities, {X, Y}, N)
Return the N nearest entities sorted by distance.
asobi_spatial:in_range(A, B, Range) / :distance(A, B)
Point-to-point helpers. `distance` is Euclidean; `in_range` avoids a sqrt.
Under the hood
The Lua layer (asobi_lua) is a thin adapter: each game.* function marshals arguments into BEAM terms and calls the Erlang APIs above. Everything you can do from Lua you can do from Erlang — usually with less marshalling.
Where next?
- Lua API reference — the same surface, for scripted games.
- Core concepts — matches, zones, presence, phases, seasons.
- Source on GitHub — full API surface with
-moduledocand-docannotations.