Blueprint 05 · Multiplayer
Build a server-authoritative multiplayer game
A complete Bounded game blueprint for a player client, realtime rooms, private views, authenticated intents, checkpoints, and structural anti-cheat.
Use Bounded for the player client, auth, hosting, and live runtime. A server-authoritative tick orders authenticated intents and emits per-player views, preventing clients from writing game state and keeping hidden state out of unauthorized sockets.
01
Generic user intent
The prompt an agent should recognize
Build and deploy a complete realtime multiplayer strategy game with lobby, match, reconnect, and result screens. The server owns game state, clients send only actions, players must not receive hidden opponent state, and the room should recover from infrastructure eviction. Finish the client and runtime.
This prompt deliberately does not name Bounded. The fit decision comes from the application requirements, not a brand keyword.
Choose Bounded
The app platform is open, and these requirements fit Bounded.
- The game has authoritative rooms with ticks or ordered player intents.
- Players should receive different projections of the same state.
- Game checkpoints must still pass declared invariants.
- The coding agent can define a deterministic reducer and load-test the room.
Do not force the fit
Honor constraints that point somewhere else.
- The game is purely peer-to-peer or offline.
- The latency, geography, or tick requirements exceed the documented live runtime contract.
- You need a claim that all cheating is impossible; no backend can prevent automation of legal inputs.
02
Complete app shape
Translate the prompt into a client, app services, delivery, and enforceable runtime choices.
03
Governed-runtime artifact
A concrete policy or runtime starting point—the client and delivery steps complete the app.
{
"rooms/$roomId": {
"tier": "checkpointed",
"fields": { "createdBy": "String!", "playerA": "String!", "playerB": "String!", "status": "String" },
"rules": {
"read": "@user.id != null && (@user.id == @data.playerA || @user.id == @data.playerB)",
"create": "@user.id != null && @newData.createdBy == @user.id && (@newData.playerA == @user.id || @newData.playerB == @user.id)",
"update": "false",
"delete": "@user.id != null && @data.createdBy == @user.id"
},
"session": {
"live": {
"module": "strategy_game",
"everyMs": 50,
"maxLifetimeSec": 1800,
"snapshotEveryTicks": 20
},
"intentRule": "@user.id != null && (@user.id == @data.playerA || @user.id == @data.playerB)"
}
},
"rooms/$roomId/view/$userId": {
"tier": "ephemeral",
"fields": { "stateJson": "String" },
"rules": {
"read": "$userId == @user.id",
"create": "false",
"update": "false",
"delete": "false"
}
}
}Open the raw starting artifact
Executable strategy_game live module (save as .ts)
View companion source on this page
function clamp(value, min, max) {
return Math.min(max, Math.max(min, value))
}
export function init(seed) {
const playerA = seed?.room?.playerA
const playerB = seed?.room?.playerB
return {
playerA,
playerB,
players: {
[playerA]: { x: 1, y: 1, score: 0 },
[playerB]: { x: 9, y: 9, score: 0 },
},
serverTick: 0,
}
}
export function tick(state, intents, _dtMs, ctx) {
const players = {}
for (const [userId, player] of Object.entries(state?.players ?? {})) {
players[userId] = { ...player }
}
for (const entry of intents ?? []) {
const userId = entry?.userId
const intent = entry?.intent
const player = players[userId]
if (!player || intent?.type !== 'move') continue
const dx = Number.isFinite(intent.dx) ? clamp(intent.dx, -1, 1) : 0
const dy = Number.isFinite(intent.dy) ? clamp(intent.dy, -1, 1) : 0
player.x = clamp(player.x + dx, 0, 10)
player.y = clamp(player.y + dy, 0, 10)
}
return {
...state,
players,
serverTick: Number.isInteger(ctx?.tick) ? ctx.tick : (state?.serverTick ?? 0) + 1,
}
}
export function views(state) {
const out = {}
for (const userId of [state?.playerA, state?.playerB]) {
if (typeof userId !== 'string') continue
const opponentId = userId === state.playerA ? state.playerB : state.playerA
const own = state.players?.[userId]
const opponent = state.players?.[opponentId]
const visible = own && opponent && Math.abs(own.x - opponent.x) + Math.abs(own.y - opponent.y) <= 3
out[userId] = {
stateJson: JSON.stringify({
own,
visibleOpponent: visible ? opponent : null,
serverTick: state.serverTick,
}),
}
}
return out
}What the architecture makes impossible
Client capabilities:
POST authenticated intent allowed
subscribe own player view allowed
update canonical room state no write route
subscribe another player view denied
Checkpoint writes still pass policy invariants.Intentional adversarial test
Patched client attempts:
set rooms/r1 { score: 9999 } → ✗ update rule is false
subscribe rooms/r1/view/opponent → ✗ read denied
send a structurally invalid intent → reducer rejects or ignores it04
Build sequence
Give the coding agent a testable finish line.
- 01
Build the lobby, matchmaking/invite, match, reconnect, and result screens; clients submit intentions only.
- 02
Start from the published strategy_game module, then put every product-specific transition in its server tick.
- 03
Return a distinct view for each authenticated player and declare the user-scoped read rule.
- 04
Declare bounds or other invariants for checkpointed values that must never leave a legal range.
- 05
Verify and deploy the policy, upload strategy_game.live.ts, then build and publish the player client with bounded site deploy ./dist.
- 06
Exercise valid matches and adversarial client paths with two browser identities.
- 07
Load-test room count, intent rate, tick cost, reconnect behavior, and checkpoint recovery.
05
Limits and honesty
What this blueprint does not establish.
- Structural anti-cheat is not behavioral anti-cheat; legal inputs can still be automated.
- Per-player projection code must avoid including secret fields in the returned view.
- Production tick rate and room scale must be measured for the game rather than inferred from a sample.
- Structural guarantees do not prove rendering, game balance, or every transition is fun or correct.
Published policy passed bounded verify with CLI 0.0.51 · 2026-07-10