Skip to main content

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.