What your game can and cannot do
Read this first. It explains the one rule everything else follows from, and why the SDK is shaped the way it is.
Your game never decides who wins
Your game draws what the server sends and reports what the player did. The server plays the game, and decides what that was worth.
This is not a policy we ask you to respect. It is enforced by the architecture, and it is enforced against first-party games in exactly the same way as third-party ones.
A game has two halves, and they run in different places:
| Half | Runs | Does |
|---|---|---|
The rules (src/game.ts) |
On our server, never in the player's browser | Builds the board from the seed, applies each move, counts the score, and says what the player may see |
The bundle (src/main.ts and the rest) |
In the player's browser, in a sandboxed frame | Draws what it is sent, and sends what the player does |
Which rules you write depends on the kind of game:
| Kind | The server runs | The bundle is sent |
|---|---|---|
realtime-authoritative — players take turns against each other |
Your match handler (server rules) | Each player's own viewFor(state, playerId) |
async-pool — each player alone, on a board everyone in the pool shares |
Your ServerGame, stepped by the platform's solo handler |
view(state, tick) — what the player may see |
realtime-simulated — live physics, two devices |
Both devices simulate; the server compares their accounts | Its own table |
The rest of this page is about the first two, where everything that matters happens on the server.
What your bundle is given
For a pooled game: frames. Each one is a view of the run, and the clock:
{ tick: 1830, tickRate: 60, over: false, state: { /* your view */ } }
The server sends a frame whenever the view changes, and once a second besides. The SDK's
createSoloClient reads them for you and counts the clock on between them.
That is all. Your bundle is never told:
- the seed, or anything drawn from it that the player could not see at a table — the undealt deck, the mines, the shapes still to come
- the stake, or that money is involved at all
- a player's balance or wallet
- who the other players are, or how they are doing
- the prize, the payout split, or the platform's fee
The manifest validator rejects a bundle whose config so much as mentions stake,
balance, wallet, payout or prize.
What your bundle sends
Moves. A move is an action name and at most one whole number:
client.send("place", encodePlacement(slot, row, col));
The server applies it at the tick it arrives — the server's tick, on the server's clock — and the next frame shows what became of it. A move that is not legal is simply ignored.
What the server does with it
player's device our server
─────────────── ──────────
seed ──▶ board (kept here)
frame (the view) ◀────────────────── what the player may see
player taps
move ───────────────────────────────▶ applied at this tick
frame (the view) ◀────────────────── what became of it
… …
"run over — 420" ◀────────────────── the rules end the run and count it
score 420 → pool / board
The score that is paid is the one the server counted, from moves it applied itself. There is no score for the device to claim and no log for it to hand in.
Why cheating does not work
Editing the score in memory. Your bundle has no score to send. The one on screen is a copy of the server's.
Reading ahead. The seed never reaches the device, so there is no deck to peek at and no
mine map to find. A pooled game's view must leave out what is still to come — see
hidden information below — and the platform checks every game's view
for exactly that.
Sending moves nobody could make. They are moves like any other: applied if legal, ignored if not, at the server's tick. A flood is cut off.
Slowing time down. The clock is the server's. A run of two minutes is two minutes of the server's ticks, however slowly the device draws.
Leaving to look, and coming back. The clock keeps running while the page is closed. A player who leaves has their run end on its clock, with the score they had.
Modifying the game bundle. It only draws and sends. The rules it would need to change are not in it.
Calling home for help, reading storage, reaching the hosting page. The sandbox blocks all three.
What this means for how you build
Four rules follow from all of the above, and certification enforces every one:
1. Use the SDK's random generator, never the engine's
Math.random(), Defold's math.random(), Phaser's Phaser.Math.RND — none can be
seeded. Five players in a pool must face the identical puzzle, and that is only true if
the board comes from the seed the platform gives your rules.
2. Drive your rules from ticks, never from elapsed time
Never delta, dt, Date.now() or performance.now() in update. One tick is one step.
The server runs your rules at your manifest's tick rate on its own clock; your bundle may
use frame time freely for drawing — the distinction is between what the player sees and
what the game decides.
3. Write the rules for the server's JavaScript
Pooled rules run in Nakama's JavaScript runtime, which is ES2015 with no DOM, no timers and
no import at runtime. Array.prototype.includes, String.prototype.padStart,
Object.values, Array.prototype.flat and anything newer are not there. Import only from
@crazy8s/game-sdk/rules, which carries the generator, the time bonus and the types and
nothing that needs a browser. See determinism.
4. Make your score an integer
A score is compared exactly, ranked and paid. A floating-point one has no business there.
Hidden information
The server holds the state; each player is sent only a view of it.
- A head-to-head game implements
viewFor(state, playerId), returning what that player may see. A card game returns the player's own hand and only the count of everyone else's. - A pooled game implements
view(state, tick). Leave out everything the player could not see at a table: an undealt deck becomes a count, a mine becomes nothing until the run is over, a tape of pieces to come is not sent at all, and neither is anything that gives it away — a count of neighbouring mines on a closed square is a clue to where they are.
The engine never broadcasts raw state, so a game that forgets to redact leaks nothing by default; it simply shows nothing.
Practice mode
Practice runs the identical code path — a pooled game's practice run is played on the server like any other. There is no separate "free" build. The only difference is that no money moves.
That is deliberate: a game that behaves differently in practice than in a real run is a game whose testing tells you nothing.
Related
- Getting started — the path end to end
- Writing the server rules — a head-to-head game's match handler
- Determinism — what the server runs, and proving your rules are fair
- Publishing lifecycle — review, updates, revenue
- Building a client — for a front end, not a game
- Server API reference — every RPC, and who may call it
- ADR 0024 — why pooled games moved to the server
- Engine guides: Excalibur.js · Phaser · Defold
