Game module callbacks
The functions you write in a game module. Asobi calls them at the right moments in the match lifecycle. If you write in Erlang instead, these map 1:1 to the asobi_match behaviour - see the Erlang API.
End-of-match from Lua: to finish a match from tick or leave, set state._finished = true and state._result = {...}, then return the state. Returning { finished = true, ... } does nothing.
Required: init, join, leave, handle_input, get_state. Optional: tick, phases, on_phase_started, on_phase_ended, vote_requested, vote_resolved.
init(config)
Called once when the match is created. Receives the config map the match was started with (mode-specific). Return the initial state.
function init(config)
return {
board = { 0, 0, 0, 0, 0, 0, 0, 0, 0 },
turn = "x",
players = {},
started = false,
}
endjoin(player_id, state)
A player is entering. Accept and attach them, or reject. Return the new state, or nil, error to reject.
function join(player_id, state)
if state.started then
return nil, "match_in_progress"
end
state.players[player_id] = (next(state.players) == nil) and "x" or "o"
if #state.players == 2 then state.started = true end
game.send(player_id, { kind = "welcome", mark = state.players[player_id] })
return state
endleave(player_id, state)
A player disconnected or was removed. Cannot fail. Use this to stop timers, release reservations, or mark the slot empty.
function leave(player_id, state)
state.players[player_id] = nil
if state.started then
state._finished = true
state._result = { forfeit = player_id }
end
return state
endhandle_input(player_id, input, state)
A player action arrived over WebSocket. Validate and apply. Inputs are serialised onto the match process - you can mutate state here without worrying about races.
function handle_input(player_id, input, state)
local mark = state.players[player_id]
if not mark or mark ~= state.turn then return state end
local cell = tonumber(input.cell)
if not cell or state.board[cell] ~= 0 then return state end
state.board[cell] = mark
state.turn = (mark == "x") and "o" or "x"
game.broadcast("move", { cell = cell, mark = mark })
return state
endtick(state)
Called on a fixed interval (default 10 Hz, configurable per mode). Advance time, resolve AI, check win conditions. Return the new state; to end the match, set state._finished = true and state._result first (see the callout above).
function tick(state)
local w = winner(state.board)
if w then
state._finished = true
state._result = { winner = w }
elseif is_full(state.board) then
state._finished = true
state._result = { draw = true }
end
return state
endget_state(player_id, state)
Project the full match state into what this player should see. Hide opponent cards, enemy positions out of sight, hidden rolls. Called whenever a client asks for the current state (on reconnect, on view refresh).
function get_state(player_id, state)
return {
board = state.board,
turn = state.turn,
your_mark = state.players[player_id],
}
endOptional: phases (Erlang match mode, Lua world mode)
Declare named phases (lobby, active, results...) and hook into their transitions. Phases are supported for Erlang match games and for Lua world games. Lua match games cannot use phases/1 yet - model them inside tick using explicit state fields.
phases(_Config) ->
[
#{name => <<"lobby">>, duration => 30000},
#{name => <<"active">>, duration => 180000},
#{name => <<"results">>, duration => 15000}
].
on_phase_started(Name, State) ->
asobi_match_server:broadcast_event(self(), <<"phase">>, #{name => Name}),
{ok, State}.
on_phase_ended(_Name, State) -> {ok, State}.Optional: voting
Hook into in-match voting - provide vote config on request, react to results.
function vote_requested(state)
if state.offer_boons then
return {
template = "boon_pick",
options = { "fireball", "shield", "speed" },
window_ms = 20000,
}
end
return nil
end
function vote_resolved(_template, result, state)
state.picked_boon = result.winner
return state
endWorld-mode callbacks
A game with game_type = "world" runs on the world server and implements a different callback set. init, join, leave, get_state, phases and the on_phase_* hooks carry over; the rest are world-specific. Worked examples live on the world server page.
generate_world(seed, config)- return the initial per-zone state map, keyed by"x,y".spawn_templates(config)- declare the entity templatesgame.zone.spawndraws from.spawn_position(player_id, state)- where a joining player enters the world.zone_tick(entities, zone_state)- per-zone simulation step; returnentities, zone_state. Replacestick.handle_input(player_id, input, entities)- apply a player action; return the entities table.post_tick(tick_n, state)- world-level step after all zones tick (boss phases, votes, finish).on_world_recovered(snapshots, state),on_zone_loaded(cx, cy, state),on_zone_unloaded(cx, cy, state)- lazy-zone and snapshot lifecycle hooks.
Pattern: minimum viable game
The smallest correct game implements init, join, leave, handle_input, get_state. tick is optional - if you don't need a fixed time step, skip it.
Where next?
- Tic-tac-toe tutorial - all the callbacks in context.
- game.* API reference - what you call from these callbacks.
- Cookbook - recipes for common patterns.