Docs / Learn / Set up a match and its modes

Set up a match and its modes

Define a match server-side so the arena has a mode a client can create and join.

A match is an arena round: an ephemeral, bounded fight that spins up, players join, the server runs the simulation, and it ends. Everything here is server-authoritative. The client sends intent, the server decides, the server broadcasts state. You define both halves of that in Lua: a mode name, and the script that implements it.

Two files do the work:

  • match.lua implements the match: join, handle_input, tick, and the rest of the callbacks.
  • config.lua is an optional manifest that maps a mode name to a match script. You only need it once you have more than one mode.

Single mode: just match.lua

With no config.lua, Asobi loads a top-level match.lua as the mode named "default". That is the whole registration step - drop the file in and the mode exists.

Here is the arena, server-side. Your fighter lives in shared match state, the server moves it in response to input, and every player sees the same arena.

-- lua/match.lua
match_size = 2

function init(config)
    return {
        arena_w = 16,
        arena_h = 16,
        fighter = { x = 8, y = 8 }
    }
end

function join(player_id, state)
    return state
end

function leave(player_id, state)
    return state
end

function handle_input(player_id, input, state)
    local f = state.fighter
    f.x = math.max(1, math.min(state.arena_w, f.x + (input.move_x or 0)))
    f.y = math.max(1, math.min(state.arena_h, f.y + (input.move_y or 0)))
    return state
end

function tick(state)
    return state
end

function get_state(player_id, state)
    return { arena_w = state.arena_w, arena_h = state.arena_h, fighter = state.fighter }
end

match_size = 2 means the matchmaker pairs two queued players into one match, which is what the next step's "two clients, one match" checkpoint needs. Lower it to 1 for solo play, or raise it for larger rosters.

Every match script must define init, join, leave, handle_input, tick, and get_state. tick runs at 10 Hz by default; here it is a no-op because your fighter only moves on input. The client sends move_x/move_y deltas in {-1, 0, 1}, and the server clamps your fighter to the arena. For the full callback contract, including the optional join/3 context and vote hooks, see the Lua scripting guide. You will flesh out handle_input and tick in step 8; for now they just need to exist.

Multiple modes: the config.lua manifest

To give the mode a real name, or to ship more than one, add a config.lua that maps mode names to scripts. When config.lua exists, Asobi reads it instead of looking for a top-level match.lua.

-- lua/config.lua
return {
    arena = "arena/match.lua"
}
my_arena/
└── lua/
    ├── config.lua
    └── arena/
        └── match.lua

Now the mode is "arena", not "default", and that is the name a client will queue for. Add more rows to add more modes:

-- lua/config.lua
return {
    arena = "arena/match.lua",
    duel  = "duel/match.lua"
}

Each match script keeps its own per-mode globals (match_size, max_players, strategy) at the top of that script. Deployment-wide globals such as guest_auth go in config.lua, not the per-mode scripts, because they describe the whole bundle rather than one mode. The full list of globals and matchmaking knobs lives in Configuration.

Deploy and boot

The bundle above is identical on both paths - config.lua, match.lua, and every callback are byte-for-byte the same whether you run managed cloud or your own release. Only how you boot it and where you read the log differ.

Cloud. asobi deploy pushes the bundle; the mode is registered on each rollout. Read the boot log in the console at console.asobi.dev (or via the CLI log stream). The per-environment database and guest pepper are already provisioned, so there is nothing else to wire up.

Self-hosted. Bring up the asobi_lua Docker image with your game directory mounted at /app/game/ (docker compose up); the boot log prints to container stdout. Your own Postgres and any ASOBI_* env vars are configured as in Configuration.

Checkpoint

Boot the server (step 1) and watch the startup log. Asobi logs one line as it registers your modes:

{"msg":"lua game config loaded","modes":["arena"]}

modes lists exactly what you registered - ["arena"] for the manifest above, or ["default"] if you kept a single top-level match.lua. Seeing your mode name there means the mode is registered and a match of that mode can now be created.

To confirm the server accepts the mode over REST, list live matches for it:

GET /api/v1/matches/live?mode=arena

The array is empty until a player actually creates a match, which is exactly what you do next.

Next: Step 7 - Connect to a match

connect a client to the mode with match.join, and put two clients in the same match.