/** * Projection helpers that turn the full table state into safe views. * * The full state (including every hole card and the deck order) must never * leave the server; clients only ever receive these views. Hole cards are * hidden except for the viewer's own seats and for live hands at showdown. */ import { getLegalActions } from "./betting"; import type { Card, PokerSeatId, PokerTableState, PrivatePlayerView, PublicPlayerView, PublicTableView, } from "./types"; function sumCommitted(state: PokerTableState): number { return state.players.reduce((total, player) => total + player.committed, 0); } function projectPlayer( state: PokerTableState, player: PokerTableState["players"][number], viewerSeat: PokerSeatId | null, ): PublicPlayerView { const isViewer = viewerSeat !== null && player.seatId === viewerSeat; const reveal = (state.phase === "showdown" && !player.folded) || isViewer; const holeCards: readonly Card[] | null = reveal && player.holeCards.length > 0 ? player.holeCards : null; return { seatId: player.seatId, name: player.name, stack: player.stack, committed: player.committed, totalCommitted: player.totalCommitted, folded: player.folded, allIn: player.allIn, hasActed: player.hasActed, eliminated: player.eliminated, holeCards, cardCount: player.holeCards.length, }; } /** * Public view of the table. Pass `viewerSeat` to also reveal that seat's own * hole cards; pass null for a fully spectator-safe view. */ export function buildPublicView( state: PokerTableState, viewerSeat: PokerSeatId | null, ): PublicTableView { return { players: state.players.map((player) => projectPlayer(state, player, viewerSeat)), buttonSeat: state.buttonSeat, smallBlindSeat: state.smallBlindSeat, bigBlindSeat: state.bigBlindSeat, street: state.street, phase: state.phase, potTotal: state.collectedPot + sumCommitted(state), communityCards: state.communityCards, currentBet: state.currentBet, toActSeat: state.toActSeat, handNumber: state.handNumber, config: state.config, showdown: state.showdown, finalPlacements: state.finalPlacements, }; } /** Everything one seated player is allowed to know, including their options. */ export function buildPrivateView(state: PokerTableState, seatId: PokerSeatId): PrivatePlayerView { const viewer = state.players[seatId]; const publicView = buildPublicView(state, seatId); return { ...publicView, viewerSeat: seatId, yourHoleCards: viewer?.holeCards ?? [], legalActions: getLegalActions(state, seatId), yourTurn: state.phase === "betting" && state.toActSeat === seatId, }; }