Lua cookbook
Short, copy-pasteable patterns for common gameplay tasks. Each recipe is self-contained and assumes you already have a Lua game module loaded.
Send to one player vs. everyone
-- to everyone in the match
game.broadcast("announce", { text = "Match starting!" })
-- to one player
game.send(player_id, { kind = "private_hint", text = "The treasure is north" })
-- to a specific subset (loop + send)
for pid, _ in pairs(state.players) do
if state.teams[pid] == "red" then
game.send(pid, { kind = "team_chat", from = from, text = msg })
end
endReject an input with an error echo
function game.handle_input(player_id, input, state)
if input.action == "buy" and not can_afford(player_id, input.item, state) then
game.send(player_id, { kind = "error", code = "too_poor", item = input.item })
return state
end
-- happy path
return apply_buy(player_id, input.item, state)
endPer-player scoped persistence
-- save on leave, restore on join
function game.join(player_id, state)
local saved = game.storage.player_get(player_id, "inv", "backpack") or {}
state.inventories[player_id] = saved
return state
end
function game.leave(player_id, state)
game.storage.player_set(player_id,
"inv", "backpack",
state.inventories[player_id] or {})
state.inventories[player_id] = nil
return state
endTicking AI without blowing the budget
-- Only step a fraction of NPCs per tick to spread work.
function game.tick(state)
state.cursor = (state.cursor or 0) + 1
local total = #state.npcs
local batch = math.max(1, math.floor(total / 10)) -- 10% per tick
for i = 1, batch do
local idx = ((state.cursor + i - 1) % total) + 1
step_npc(state.npcs[idx], state)
end
return state
endGrant currency and check balance
-- winner: grant, then submit to the leaderboard
game.economy.grant(winner_id, "gold", 50, "match_win")
game.leaderboard.submit("arena:weekly", winner_id, state.scores[winner_id])
-- check before showing a buy button
local wallet = game.economy.balance(player_id)
local can_buy = (wallet.gold or 0) >= listing.price
game.send(player_id, { kind = "shop", listing = listing, can_buy = can_buy })Atomic shop purchase
-- Use purchase() rather than manual debit + grant — it's transactional.
function buy_listing(player_id, listing_id)
local result = game.economy.purchase(player_id, listing_id)
if result.ok then
game.send(player_id, { kind = "purchase_ok", listing_id = listing_id })
else
game.send(player_id, { kind = "purchase_fail", reason = result.error })
end
endSpatial: nearest enemies for auto-target
local targets = game.spatial.nearest(state.enemies, player.x, player.y, 3, {
type = "hostile",
sort = "nearest",
})
for _, t in ipairs(targets) do
deal_damage(t, 10)
endWorld zone: spawn a boss and announce it
local boss = game.zone.spawn("dragon", 2048, 512, {
hp = 10000,
phase = "sleeping",
})
game.broadcast("boss_spawned", {
id = boss.id, x = boss.x, y = boss.y
})Phases: lobby → active → results
function game.phases(_config)
return {
{ name = "lobby", duration_ms = 30000 },
{ name = "active", duration_ms = 300000 },
{ name = "results", duration_ms = 15000 },
}
end
function game.on_phase_started(name, state)
if name == "active" then
state.active_at = os.time()
game.broadcast("go", {})
elseif name == "results" then
game.broadcast("results", { final = state.scores })
end
return state
endIn-match vote with short-circuit on majority
-- Offer a boon pick every time a wave clears.
function game.vote_requested(state)
if state.wave_cleared then
state.wave_cleared = false
return {
template = "boon",
method = "plurality",
options = pick_3_boons(),
duration_ms = 20000,
quorum = math.ceil(count_alive(state) / 2), -- resolves early
}
end
return nil
end
function game.vote_resolved(_template, result, state)
apply_boon(result.winner, state)
game.broadcast("boon_picked", { boon = result.winner })
return state
endReconnect-friendly ephemeral state
-- Keep minimal authoritative data in state; derive view via get_state.
function game.get_state(player_id, state)
-- On reconnect the client gets this fresh snapshot.
return {
you = state.players[player_id] or {},
others = visible_others(player_id, state),
phase = state.current_phase,
elapsed = state.elapsed,
}
endNotify winners without blocking the tick
-- notify_many fans out via the background broadcaster.
game.notify_many(winners, "tournament_win", "You won the bracket!", {
prize_id = "trophy_gold",
unlock_at = os.time() + 60,
})Where next?
- game.* API reference — the full surface these recipes call into.
- Game module callbacks — what you implement.
- Tic-tac-toe tutorial — everything applied to a concrete game.