← All documentation

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.