Keeping a secret
Some games hold a fact one player has and another must not. This is the mode that makes that true of the bytes on the wire, not just of what the client chooses to draw.
- Never encoded
- Server simulates truth
- Costs a round trip
Status: in development. This tier compiles and runs, but it is not yet reachable from the Creator Studio — you cannot publish a game with it today.
Most games have nothing to hide. Every player sees the same board, so every device can run the same simulation and the wire only carries what people pressed.
A card game is not most games. If every device holds the whole state, every device holds your hand — and “the client politely does not draw it” is not a secret, it is a lie you can read with a debugger.
What changes
Declare the mode and add one function.
game.ts
export default defineGame({
mode: "private",
visibleTo(state, seat) {
// Return a copy holding only what this seat may know.
},
// …everything else is the same file it was.
});visibleTo is not part of defineGame yet. The server side of this page is built and tested; the authoring side is not, so the snippets here describe the shape the runtime was written to accept rather than something you can compile against today. Read them as the design, and expect the exact spelling to be confirmed when the tier opens.
Per tick, on our servers:
- 1Every seat’s input is collected.
- 2
stepruns once — one true world, on our machine. - 3
visibleToruns once per seat. - 4Each seat is sent the difference between their view now and their view before.
Bytes a player may not know are never encoded. They do not reach that device, so they are not in its memory to be found.
Your clients still simulate. They run step on the redacted state they hold and get corrected by the next update — and they will be wrong about things they cannot see, which is the point rather than a defect.
The difference in step four is bytes, not fields. A seat that has never been sent anything gets its whole redacted view once; after that it gets the runs of bytes that changed, with runs closer together than eight bytes merged because a run header costs more than the identical bytes between them. A tick where nothing that seat can see moved costs one byte, which is what makes a poker table between actions nearly free.
The trap, which is the whole difficulty
Absence is information.
// Wrong. The client now knows an enemy exists and is hidden.
enemy.x = 0;
enemy.y = 0;Zeroing a position removes the position and keeps the fact. For fog of war a hidden entity has to be indistinguishable from an empty slot, not a blanked one — same bytes an unused slot would have.
A fixed-capacity layout helps, because an unused slot is already all zeros and that is the natural shape of nothing. But it is your function, and this is the mistake to check for before shipping.
The runtime carries one check for the commonest version of it: compare a redacted region against the bytes an empty slot would hold, and if they differ, that seat can tell an occupied slot from an unoccupied one. It is a lint and not a guarantee — it can only see the region you point it at — so it catches the blanked-entity mistake above and nothing subtler.
Watch for the second-order version too: an entity count elsewhere in your state gives away everything a careful visibleTo just hid.
Spectators
Someone watching is not a player, and the right answer is genuinely a design choice. A poker broadcast shows every hand. A competitive stream shows what a player sees.
So you have to say:
visibleTo(state, seat) {
if (seat === SPECTATOR) { /* your answer */ }
}A spectator is not a seat, and the runtime keeps that as a distinction it can enforce rather than a numbering convention you have to remember — a viewer is never passed to you as “seat 8”. All spectators share one stream, because they all see the same thing by definition and an audience should not cost per head.
If you do not, your game has no spectators. Not an error, and not an empty view — no frame at all. It is not a setting you can turn on; it is whether the function exists. That is deliberate: defaulting to “show everything” would leak your hidden state through a feature you had forgotten you had, which is exactly how these things go wrong.
What it costs
Your own actions
No longer instant. This mode pays a round trip for authority, and that is what a secret costs.
Server work
visibleToand one diff per seat, per tick — cheap at a card game’s pace, real at thirty ticks a second.
Seats
Comfortable to eight. Your visibleTo produces a whole redacted copy per seat, so the copying is the seat count times the state size: around 61 MB/s at sixteen seats and 492 MB/s at sixty-four. The mode is built for the games that want it — cards, hidden roles, deduction, a fog-of-war board — which are four to eight seats and a few kilobytes.
Which is why this is opt-in. If your game has no secrets, the other mode is faster and cheaper in every direction, and you should stay there.
One thing only this mode can do: tickRate: "event" — a clock that advances when somebody acts rather than sixty times a second, which is what a card game actually wants. It requires mode: "private" and is refused outside it, because a tick that happens when an input arrives cannot be predicted by a client that has not seen the input.
Debugging is harder here, and worth pricing in
The debugging story for the default mode is: run the game, read the arena, print the numbers. It works because every client holds the same world, so the bytes in front of you are the bytes everyone has.
That is exactly what this mode takes away. A client’s state is not the world here — it is one seat’s redacted view of it, being simulated forward with half the information. Print it and you get a coherent, confident, wrong picture. Not wrong because of a bug: wrong because you hid the rest of it on purpose, and the client is doing its best without. The only place the true world exists is our server.
The second-order cost is that you lose a signal. In the default mode two clients disagreeing is a defect and the room can notice it by comparing notes. Here two clients holding different bytes is the entire point, so there is nothing to compare — being wrong is corrected by the next update from the authority rather than caught by anybody. What that means in practice is that a redaction bug and a simulation bug look the same from a client, and you separate them by checking your visibleTo against what that seat was supposed to know, rather than by watching the game.
And there is no worked example to copy. The redaction, the per-seat baselines and the diff are all tested, but no game has been through this mode end to end — the harnesses that print a real match all run the default one. You would be the first, which is worth knowing before you choose it rather than after.
Choosing
| Your game | Mode |
|---|---|
| A shooter, a racer, a platformer, a brawler | The default |
| Cards, hidden roles, sealed bids, fog of war | This one |
| A board game where everything is face up | The default, even if it feels formal |
“The default” is mode: "shared", which is what writing a game’s logic describes. “This one” is mode: "private".
The question is not “is my game competitive”. It is “is there a fact one player holds that another must not”. If not, you do not want the round trip.

