Recipes — common game shapes
The whole game is expressed through pendingPlayers and what
computeObservation reveals. A few canonical shapes:
Sequential (perfect information)
Checkers, Connect Four. One seat pending at a time; each move hands the turn to
the next seat. Use passthroughObservation (everyone sees everything). The
same-view rule is automatically strict — no stale move survives an opponent's
turn.
applyAction({ state, playerIndex, data }) {
const next = applyMove(state, playerIndex, data);
return next.won
? { state: next, pendingPlayers: [], outcome: win(playerIndex) }
: { state: next, pendingPlayers: [(playerIndex + 1) % playerCount] };
}
computeObservation: passthroughObservation,
Simultaneous (hidden commitment)
RPS, blind bidding. All actors pending each round; store each commit in the
state and hide the opponents' commits in computeObservation, also masking their
pending status so a hidden commit doesn't change anyone else's view (that's what
lets both submissions land in either order — see
the same-view rule). Resolve when the last commit
arrives.
Team games
Set teamIndex on outcome entries to the team, not the seat, so OpenSkill rates
teammates together. placement is the team's finish.
Elimination / multiplayer
Shrink pendingPlayers as seats bust out; give an eliminated seat
result: "eliminated" with its placement. The game ends when
pendingPlayers empties; the final outcome ranks everyone by placement.
Reveal for animation
Carry a "what just happened" field (RPS's lastRound) in the projected data so
clients can animate the transition. Decide per seat what that reveal shows using
cause and playerIndex — see Rendering.
Phased turns / variable clocks
A phase that needs longer returns turnSeconds: N on its envelope to widen just
that action's deadline, leaving every player's bank untouched.