End a match
Goal: the server decides the arena round is over, computes a result, and both clients receive it.
So far the round runs forever: clients send input, the server moves your fighter across the arena, the server broadcasts match.state. Now the server ends the round on its own terms and hands every player a final result table.
This is a server decision. A client never ends a round. The server detects the end condition inside the tick loop, builds the result, and the platform pushes match.finished to everyone.
How an arena round finishes
Your round ends the moment tick(state) marks the state finished. Set two fields on the state table:
state._finished = true- stop the round after this tick.state._result = {...}- the result table sent to every player.
The platform reads those fields after tick returns, closes the round, and pushes _result to all players as the match.finished event. The table shape is yours; clients receive it as JSON.
Compute a result for the arena
Keep the through-line: one fighter in the arena, moved by player input. The arena is 16 by 16. Give the round an end condition and a score. Here the fighter chases a target cell, each player earns a point for the move that lands on it, and the round ends after a fixed number of ticks.
local ARENA = 16
local MATCH_TICKS = 300
function init(config)
return {
fighter = { x = 8, y = 8 },
target = { x = 3, y = 12 },
scores = {},
tick_count = 0
}
end
function join(player_id, state)
state.scores[player_id] = state.scores[player_id] or 0
return state
end
function handle_input(player_id, input, state)
local fighter = state.fighter
fighter.x = math.max(0, math.min(ARENA - 1, fighter.x + (input.move_x or 0)))
fighter.y = math.max(0, math.min(ARENA - 1, fighter.y + (input.move_y or 0)))
if fighter.x == state.target.x and fighter.y == state.target.y then
state.scores[player_id] = state.scores[player_id] + 1
state.target = { x = math.random(0, ARENA - 1), y = math.random(0, ARENA - 1) }
end
return state
end
function tick(state)
state.tick_count = state.tick_count + 1
if state.tick_count >= MATCH_TICKS then
state._finished = true
state._result = {
status = "completed",
scores = state.scores,
winner = top_scorer(state.scores)
}
end
return state
endtop_scorer is plain Lua - no platform call:
function top_scorer(scores)
local best_id, best_score = nil, -1
for player_id, score in pairs(scores) do
if score > best_score then
best_id, best_score = player_id, score
end
end
return best_id
endAt 10 ticks per second (the match default), MATCH_TICKS = 300 ends the round after 30 seconds. Change the condition to whatever ends your game: a score cap, one fighter left, a captured flag.
The Erlang form
If you write your match in Erlang instead of Lua, the same decision is a return value. tick/1 returns {finished, Result, State} rather than the plain State, and Result is the map delivered as match.finished. The Lua _finished/_result fields are the Lua-side spelling of that same signal.
Cloud and self-hosted are identical here
Ending an arena round is pure game logic. It runs the same whether you deploy to Asobi Cloud with asobi deploy or run your own release of asobi + asobi_lua. There is no config, secret, or database difference for this step. Edit match.lua, and the running server hot-reloads it between ticks.
What the clients receive
Every player in the match gets one push:
{"type": "match.finished", "payload": {"match_id": "...", "result": {"status": "completed", "scores": {"...": 3, "...": 1}, "winner": "..."}}}result is your _result table verbatim. Handling it is the same SDK pattern you used for match.state in Run a match: register a handler for the match.finished event before the round ends. See WebSocket protocol - match.finished for the full envelope.
Checkpoint
With two clients still joined to the same round from the previous step:
- Let the round run to
MATCH_TICKS(or lower the constant to end it sooner). - On end, both clients receive one
match.finishedevent. - The
resultpayload carries the samescoresmap andwinneron both clients.
Log the event on each client and confirm the two payloads match:
match.finished result={status=completed, scores={p1=3, p2=1}, winner=p1}If only the mover sees an end, check that you set _finished on the state you return from tick, not on a local copy.
Next: Create a world
Arena rounds are ephemeral - they start, run, and finish. Next you meet worlds: persistent arenas that outlive any single session.