Skip to main content
Hidden information — cards in your hand, a fog of war, a secret fleet placement — lives in G, the authoritative state. The engine decides who sees what when it computes player views. The hosted server sends each client only their view, so opponents never receive the secret bytes.

The rule

  • Store everything in G. The server needs the full state to validate moves.
  • Never expose secrets in views.public. Spectators and the default path read this.
  • Filter secrets in views.player. Each seat gets its own projection.

A battleship sketch

From examples/games/battleship:
The authoritative state contains every player’s fleet positions. The server needs them to decide “hit” or “miss” when someone fires.

Public view: everyone can see this

No fleet positions, no unfired cells. Just what any observer can legitimately know (that a player is ready, and which shots have landed).

Player view: you see your own fleet

views.player is called once per seat. Each player’s response only includes their own fleet. The hosted server sends the right response to each client.

Rules of thumb

  • If in doubt, leave it out of views.public. Adding something later is easier than noticing it leaked.
  • Use computed for counts. Show “opponent has 3 cards” via a computed value (opponentHandSize), not the hand itself.
  • Beware of array references. Passing G.players[id].fleet into the public view exposes it. Destructure first.

Another worked example: Splendor reserved cards

examples/games/splendor applies the same pattern at production polish. Each merchant’s reserved cards live in G.players[id].reserved (full card IDs the server needs to validate buyCard). views.player projects opponents’ reserved arrays down to a count — opponentReservedCount — so the table UI can render face-down placeholders without the IDs ever leaving the worker. The game-package test suite asserts this invariant: opponent reserved IDs never appear in any player’s view, regardless of game state. Use it as the reference implementation when designing your own per-player projections.

Local testing

Because local sessions run the same reducer, you can unit-test the player view: