Lua bots
Server-side AI players that fill empty slots, drive tutorials, or load-test your game. Bots run inside the match alongside humans — no network, no WebSocket — and share the same game.* surface as your game logic.
When to use bots
- Fill empty slots so matches start immediately.
- Tutorial / single-player sandbox with scripted opponents.
- Load test your tick loop without spawning real WebSocket sessions.
- Replay / record-and-replay testing.
Writing a bot
A bot script exports a top-level think(bot_id, state) function. Asobi calls it each bot tick (100ms) with the latest match state for that bot.
-- bots/random_player.lua
-- Optional: advertise display names the platform reads back
names = {"Spark", "Blitz", "Volt", "Neon", "Pulse"}
function think(bot_id, state)
-- state is the full match state as of the latest match.state broadcast.
-- Return an input table to submit, or nil/{} to skip this tick.
local players = state.players or {}
local me = players[bot_id]
if not me then return {} end
-- wander randomly; real bots would pick targets, cast abilities, etc.
return {
right = math.random() < 0.5,
left = math.random() < 0.5,
up = math.random() < 0.5,
down = math.random() < 0.5,
}
endWiring bots into a mode
Bots are configured per mode. In a Lua game, set bots as a top-level global in match.lua. In sys.config, use the bots key inside the mode:
-- match.lua
match_size = 4
max_players = 4
bots = {
script = "bots/random_player.lua"
}{asobi, [
{game_modes, #{
~"arena" => #{
module => my_arena,
match_size => 4,
bots => #{
enabled => true,
min_players => 4,
script => ~"bots/random_player.lua"
}
}
}}
]}When enabled = true and the queue for that mode is under min_players, asobi_bot_spawner fills the shortfall with bots that are added to the matchmaker like regular players.
Bot callbacks
think(bot_id, state)— required. Called each bot tick. Return an input map or{}.names— optional top-level table of display-name strings the spawner picks from.
There is no on_join/on_leave/on_message surface: a bot is a plain input source, nothing more. Keep per-bot state by keying off bot_id.
Difficulty knobs
Common pattern: key private state off bot_id in a module-level table and add a reaction-time delay.
local mem = {}
function think(bot_id, state)
local m = mem[bot_id] or { reaction = 0, skill = 1000 }
m.reaction = m.reaction - 1
if m.reaction > 0 then mem[bot_id] = m; return {} end
m.reaction = math.max(1, math.floor(3000 / m.skill))
mem[bot_id] = m
return pick_action(bot_id, state)
end