@pokertools/engine 1.0.1 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +591 -445
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/actions/betting.js +7 -2
  4. package/dist/actions/dealing.js +46 -20
  5. package/dist/actions/management.js +26 -5
  6. package/dist/actions/special.d.ts +18 -0
  7. package/dist/actions/special.js +20 -0
  8. package/dist/browser.d.ts +27 -0
  9. package/dist/browser.js +73 -0
  10. package/dist/engine/PokerEngine.d.ts +23 -2
  11. package/dist/engine/PokerEngine.js +54 -2
  12. package/dist/errors/ErrorCodes.d.ts +4 -35
  13. package/dist/errors/ErrorCodes.js +7 -41
  14. package/dist/errors/index.d.ts +0 -1
  15. package/dist/errors/index.js +1 -1
  16. package/dist/history/exporter.d.ts +1 -2
  17. package/dist/history/formats/json.d.ts +1 -1
  18. package/dist/history/formats/pokerstars.d.ts +1 -1
  19. package/dist/history/handHistoryBuilder.d.ts +1 -2
  20. package/dist/history/handHistoryBuilder.js +4 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/rules/actionOrder.js +4 -4
  23. package/dist/rules/blinds.d.ts +2 -0
  24. package/dist/rules/blinds.js +27 -3
  25. package/dist/rules/headsUp.js +18 -0
  26. package/dist/rules/showdown.js +10 -0
  27. package/dist/utils/cardUtils.d.ts +2 -1
  28. package/dist/utils/cardUtils.js +2 -1
  29. package/dist/utils/invariants.js +4 -0
  30. package/dist/utils/positioning.js +2 -2
  31. package/dist/utils/serialization.d.ts +1 -0
  32. package/dist/utils/serialization.js +2 -0
  33. package/dist/utils/viewMasking.d.ts +2 -1
  34. package/dist/utils/viewMasking.js +9 -1
  35. package/package.json +31 -5
  36. package/dist/history/types.d.ts +0 -73
  37. package/dist/history/types.js +0 -5
  38. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Browser-compatible entry point for PokerEngine
3
+ *
4
+ * This file provides a browser-safe RNG using Web Crypto API
5
+ * and re-exports all engine functionality for use in web applications.
6
+ */
7
+ import { PokerEngine } from "./engine/PokerEngine";
8
+ import type { TableConfig } from "@pokertools/types";
9
+ /**
10
+ * Browser-compatible RNG using Web Crypto API
11
+ * Falls back to Math.random() only in environments without crypto
12
+ */
13
+ export declare function getBrowserRNG(): () => number;
14
+ /**
15
+ * Create a PokerEngine instance with browser-compatible RNG
16
+ */
17
+ export declare function createBrowserEngine(config: TableConfig): PokerEngine;
18
+ export * from "./engine/PokerEngine";
19
+ export * from "./actions/betting";
20
+ export * from "./actions/dealing";
21
+ export * from "./actions/management";
22
+ export * from "./actions/showdownActions";
23
+ export * from "./actions/special";
24
+ export * from "./utils/viewMasking";
25
+ export * from "./utils/serialization";
26
+ export * from "./utils/cardUtils";
27
+ export * from "./history/exporter";
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ /**
3
+ * Browser-compatible entry point for PokerEngine
4
+ *
5
+ * This file provides a browser-safe RNG using Web Crypto API
6
+ * and re-exports all engine functionality for use in web applications.
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
20
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
21
+ };
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.getBrowserRNG = getBrowserRNG;
24
+ exports.createBrowserEngine = createBrowserEngine;
25
+ const PokerEngine_1 = require("./engine/PokerEngine");
26
+ /**
27
+ * Browser-compatible RNG using Web Crypto API
28
+ * Falls back to Math.random() only in environments without crypto
29
+ */
30
+ function getBrowserRNG() {
31
+ // Check for Web Crypto API
32
+ if (typeof window !== "undefined" && window.crypto?.getRandomValues) {
33
+ return () => {
34
+ const buffer = new Uint32Array(1);
35
+ window.crypto.getRandomValues(buffer);
36
+ return buffer[0] / 0x100000000;
37
+ };
38
+ }
39
+ // Check for Node.js crypto (for SSR/testing)
40
+ if (typeof globalThis !== "undefined" && globalThis.crypto?.getRandomValues) {
41
+ return () => {
42
+ const buffer = new Uint32Array(1);
43
+ globalThis.crypto.getRandomValues(buffer);
44
+ return buffer[0] / 0x100000000;
45
+ };
46
+ }
47
+ // Fallback (warn in development)
48
+ if (process.env.NODE_ENV !== "production") {
49
+ console.warn("[PokerEngine Browser] Web Crypto API not available, using Math.random(). " +
50
+ "This is NOT cryptographically secure.");
51
+ }
52
+ return Math.random;
53
+ }
54
+ /**
55
+ * Create a PokerEngine instance with browser-compatible RNG
56
+ */
57
+ function createBrowserEngine(config) {
58
+ return new PokerEngine_1.PokerEngine({
59
+ ...config,
60
+ randomProvider: getBrowserRNG(),
61
+ });
62
+ }
63
+ // Re-export everything from main engine
64
+ __exportStar(require("./engine/PokerEngine"), exports);
65
+ __exportStar(require("./actions/betting"), exports);
66
+ __exportStar(require("./actions/dealing"), exports);
67
+ __exportStar(require("./actions/management"), exports);
68
+ __exportStar(require("./actions/showdownActions"), exports);
69
+ __exportStar(require("./actions/special"), exports);
70
+ __exportStar(require("./utils/viewMasking"), exports);
71
+ __exportStar(require("./utils/serialization"), exports);
72
+ __exportStar(require("./utils/cardUtils"), exports);
73
+ __exportStar(require("./history/exporter"), exports);
@@ -1,6 +1,6 @@
1
1
  import { GameState, TableConfig, Action, PublicState } from "@pokertools/types";
2
2
  import { Snapshot } from "../utils/serialization";
3
- import { HandHistory, ExportOptions } from "../history/types";
3
+ import { HandHistory, ExportOptions } from "@pokertools/types";
4
4
  /**
5
5
  * Event listener callback type
6
6
  */
@@ -35,6 +35,27 @@ export declare class PokerEngine {
35
35
  * If action.timestamp is not provided, the engine will automatically set it
36
36
  */
37
37
  act(action: Action): GameState;
38
+ /**
39
+ * Validate an action without executing it
40
+ * Useful for UI state (enabling/disabling buttons)
41
+ */
42
+ validate(action: Action): {
43
+ valid: true;
44
+ } | {
45
+ valid: false;
46
+ error: string;
47
+ code?: string;
48
+ };
49
+ /**
50
+ * Reconcile local state with server state
51
+ * Smoothly merges server updates into client engine
52
+ */
53
+ reconcile(serverState: PublicState | GameState): void;
54
+ /**
55
+ * Optimistically execute an action and return the provisional state
56
+ * Does not modify the engine's actual state
57
+ */
58
+ optimisticAct(action: Action): GameState;
38
59
  /**
39
60
  * Undo last action
40
61
  */
@@ -46,7 +67,7 @@ export declare class PokerEngine {
46
67
  /**
47
68
  * Get player view (masked)
48
69
  */
49
- view(playerId?: string): PublicState;
70
+ view(playerId?: string, version?: number): PublicState;
50
71
  /**
51
72
  * Get snapshot for serialization
52
73
  */
@@ -80,6 +80,57 @@ class PokerEngine {
80
80
  this.dispatch(actionWithTimestamp);
81
81
  return this.currentState;
82
82
  }
83
+ /**
84
+ * Validate an action without executing it
85
+ * Useful for UI state (enabling/disabling buttons)
86
+ */
87
+ validate(action) {
88
+ try {
89
+ // Dry-run the reducer
90
+ // We don't need to deep clone state because reducer is immutable
91
+ // and pure, and we discard the result.
92
+ (0, gameReducer_1.gameReducer)(this.currentState, action);
93
+ return { valid: true };
94
+ }
95
+ catch (err) {
96
+ const message = err?.message ?? "Invalid action";
97
+ const code = err?.code;
98
+ return {
99
+ valid: false,
100
+ error: message,
101
+ code,
102
+ };
103
+ }
104
+ }
105
+ /**
106
+ * Reconcile local state with server state
107
+ * Smoothly merges server updates into client engine
108
+ */
109
+ reconcile(serverState) {
110
+ // Hydrate PublicState into GameState if needed
111
+ const newState = {
112
+ ...serverState,
113
+ // Ensure deck exists (empty for client/public state)
114
+ deck: "deck" in serverState ? serverState.deck : [],
115
+ // Ensure players map correctly (PublicPlayer.hand is compatible with Player.hand)
116
+ players: serverState.players, // Type assertion needed due to deep readonly/mutable mismatch potential
117
+ // Ensure config carries isClient flag if set locally
118
+ config: {
119
+ ...serverState.config,
120
+ isClient: this.currentState.config.isClient,
121
+ },
122
+ };
123
+ this.currentState = newState;
124
+ }
125
+ /**
126
+ * Optimistically execute an action and return the provisional state
127
+ * Does not modify the engine's actual state
128
+ */
129
+ optimisticAct(action) {
130
+ const timestamp = action.timestamp ?? this.timeProvider();
131
+ const actionWithTimestamp = { ...action, timestamp };
132
+ return (0, gameReducer_1.gameReducer)(this.currentState, actionWithTimestamp);
133
+ }
83
134
  /**
84
135
  * Undo last action
85
136
  */
@@ -100,8 +151,8 @@ class PokerEngine {
100
151
  /**
101
152
  * Get player view (masked)
102
153
  */
103
- view(playerId) {
104
- return (0, viewMasking_1.createPublicView)(this.currentState, playerId ?? null);
154
+ view(playerId, version) {
155
+ return (0, viewMasking_1.createPublicView)(this.currentState, playerId ?? null, version ?? 0);
105
156
  }
106
157
  /**
107
158
  * Get snapshot for serialization
@@ -236,6 +287,7 @@ class PokerEngine {
236
287
  ante: initialBlinds?.ante ?? config.ante ?? 0,
237
288
  blindLevel: 0,
238
289
  timeBanks: new Map(),
290
+ timeBankActiveSeat: null,
239
291
  actionHistory: [],
240
292
  previousStates: [],
241
293
  timestamp: Date.now(),
@@ -1,38 +1,7 @@
1
1
  /**
2
- * Standardized error codes for IllegalActionError
2
+ * ErrorCodes are now managed in @pokertools/types for consistency across packages.
3
+ * This file re-exports them for backwards compatibility.
3
4
  *
4
- * Using a const enum pattern for:
5
- * - Type safety at compile time
6
- * - Autocomplete in IDEs
7
- * - Documentation of all possible error codes
8
- * - Easy refactoring
5
+ * @deprecated Import from "@pokertools/types" instead
9
6
  */
10
- export declare const ErrorCodes: {
11
- readonly INVALID_ACTION: "INVALID_ACTION";
12
- readonly PLAYER_NOT_FOUND: "PLAYER_NOT_FOUND";
13
- readonly PLAYER_NOT_ACTIVE: "PLAYER_NOT_ACTIVE";
14
- readonly NOT_YOUR_TURN: "NOT_YOUR_TURN";
15
- readonly NO_CHIPS: "NO_CHIPS";
16
- readonly CANNOT_CHECK: "CANNOT_CHECK";
17
- readonly NOTHING_TO_CALL: "NOTHING_TO_CALL";
18
- readonly CANNOT_BET: "CANNOT_BET";
19
- readonly BET_TOO_SMALL: "BET_TOO_SMALL";
20
- readonly CANNOT_RAISE: "CANNOT_RAISE";
21
- readonly CANNOT_RERAISE: "CANNOT_RERAISE";
22
- readonly RAISE_TOO_SMALL: "RAISE_TOO_SMALL";
23
- readonly CANNOT_DEAL: "CANNOT_DEAL";
24
- readonly NOT_ENOUGH_PLAYERS: "NOT_ENOUGH_PLAYERS";
25
- readonly INVALID_SEAT: "INVALID_SEAT";
26
- readonly SEAT_OCCUPIED: "SEAT_OCCUPIED";
27
- readonly INVALID_STACK: "INVALID_STACK";
28
- readonly INVALID_AMOUNT: "INVALID_AMOUNT";
29
- readonly INVALID_TIMESTAMP: "INVALID_TIMESTAMP";
30
- };
31
- /**
32
- * Type representing all valid error codes
33
- */
34
- export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
35
- /**
36
- * Helper to check if an error message contains a specific error code
37
- */
38
- export declare function hasErrorCode(error: Error, code: ErrorCode): boolean;
7
+ export { ErrorCodes, type ErrorCode, hasErrorCode } from "@pokertools/types";
@@ -1,46 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ErrorCodes = void 0;
4
- exports.hasErrorCode = hasErrorCode;
3
+ exports.hasErrorCode = exports.ErrorCodes = void 0;
5
4
  /**
6
- * Standardized error codes for IllegalActionError
5
+ * ErrorCodes are now managed in @pokertools/types for consistency across packages.
6
+ * This file re-exports them for backwards compatibility.
7
7
  *
8
- * Using a const enum pattern for:
9
- * - Type safety at compile time
10
- * - Autocomplete in IDEs
11
- * - Documentation of all possible error codes
12
- * - Easy refactoring
8
+ * @deprecated Import from "@pokertools/types" instead
13
9
  */
14
- exports.ErrorCodes = {
15
- // Generic errors
16
- INVALID_ACTION: "INVALID_ACTION",
17
- // Player errors
18
- PLAYER_NOT_FOUND: "PLAYER_NOT_FOUND",
19
- PLAYER_NOT_ACTIVE: "PLAYER_NOT_ACTIVE",
20
- NOT_YOUR_TURN: "NOT_YOUR_TURN",
21
- NO_CHIPS: "NO_CHIPS",
22
- // Betting action errors
23
- CANNOT_CHECK: "CANNOT_CHECK",
24
- NOTHING_TO_CALL: "NOTHING_TO_CALL",
25
- CANNOT_BET: "CANNOT_BET",
26
- BET_TOO_SMALL: "BET_TOO_SMALL",
27
- CANNOT_RAISE: "CANNOT_RAISE",
28
- CANNOT_RERAISE: "CANNOT_RERAISE",
29
- RAISE_TOO_SMALL: "RAISE_TOO_SMALL",
30
- // Deal errors
31
- CANNOT_DEAL: "CANNOT_DEAL",
32
- NOT_ENOUGH_PLAYERS: "NOT_ENOUGH_PLAYERS",
33
- // Seat errors
34
- INVALID_SEAT: "INVALID_SEAT",
35
- SEAT_OCCUPIED: "SEAT_OCCUPIED",
36
- INVALID_STACK: "INVALID_STACK",
37
- // Validation errors
38
- INVALID_AMOUNT: "INVALID_AMOUNT",
39
- INVALID_TIMESTAMP: "INVALID_TIMESTAMP",
40
- };
41
- /**
42
- * Helper to check if an error message contains a specific error code
43
- */
44
- function hasErrorCode(error, code) {
45
- return error.message.includes(code);
46
- }
10
+ var types_1 = require("@pokertools/types");
11
+ Object.defineProperty(exports, "ErrorCodes", { enumerable: true, get: function () { return types_1.ErrorCodes; } });
12
+ Object.defineProperty(exports, "hasErrorCode", { enumerable: true, get: function () { return types_1.hasErrorCode; } });
@@ -2,4 +2,3 @@ export * from "./PokerEngineError";
2
2
  export * from "./CriticalStateError";
3
3
  export * from "./IllegalActionError";
4
4
  export * from "./ConfigError";
5
- export * from "./ErrorCodes";
@@ -19,4 +19,4 @@ __exportStar(require("./PokerEngineError"), exports);
19
19
  __exportStar(require("./CriticalStateError"), exports);
20
20
  __exportStar(require("./IllegalActionError"), exports);
21
21
  __exportStar(require("./ConfigError"), exports);
22
- __exportStar(require("./ErrorCodes"), exports);
22
+ // ErrorCodes now exported from @pokertools/types
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * Main hand history exporter
3
3
  */
4
- import { GameState } from "@pokertools/types";
5
- import { HandHistory, ExportOptions } from "./types";
4
+ import { GameState, HandHistory, ExportOptions } from "@pokertools/types";
6
5
  /**
7
6
  * Export hand history from game state
8
7
  *
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Machine-readable format for analysis and storage
5
5
  */
6
- import { HandHistory, ExportOptions } from "../types";
6
+ import { HandHistory, ExportOptions } from "@pokertools/types";
7
7
  /**
8
8
  * Export hand history to JSON format
9
9
  */
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Format specification based on PokerStars hand history text format
5
5
  */
6
- import { HandHistory, ExportOptions } from "../types";
6
+ import { HandHistory, ExportOptions } from "@pokertools/types";
7
7
  /**
8
8
  * Export hand history to PokerStars format
9
9
  */
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * Build hand history from game state and action history
3
3
  */
4
- import { GameState } from "@pokertools/types";
5
- import { HandHistory } from "./types";
4
+ import { GameState, HandHistory } from "@pokertools/types";
6
5
  /**
7
6
  * Build complete hand history from final game state
8
7
  * Call this after a hand is complete (winners determined)
@@ -41,12 +41,15 @@ function buildPlayerHistory(state) {
41
41
  continue;
42
42
  // Calculate starting stack (current + invested)
43
43
  const startingStack = player.stack + player.totalInvestedThisHand;
44
+ // Only include cards if they are fully visible (no masked/null cards)
45
+ const hasMaskedCards = player.hand?.some((c) => c === null);
46
+ const cards = player.hand && !hasMaskedCards ? player.hand : undefined;
44
47
  players.push({
45
48
  seat: player.seat,
46
49
  name: player.name,
47
50
  startingStack,
48
51
  endingStack: player.stack,
49
- cards: player.hand ? [...player.hand] : undefined,
52
+ cards,
50
53
  });
51
54
  }
52
55
  return players;
package/dist/index.d.ts CHANGED
@@ -5,4 +5,4 @@ export { createSnapshot, restoreFromSnapshot, Snapshot } from "./utils/serializa
5
5
  export { createPublicView } from "./utils/viewMasking";
6
6
  export { calculateTotalChips, auditChipConservation } from "./utils/invariants";
7
7
  export { exportHandHistory, getHandHistory, exportMultipleHands } from "./history/exporter";
8
- export type { HandHistory, HandHistoryPlayer, StreetHistory, ExportOptions } from "./history/types";
8
+ export { HandHistory, HandHistoryPlayer, StreetHistory, ExportOptions } from "@pokertools/types";
@@ -33,7 +33,7 @@ function getNextToActNormal(state) {
33
33
  while (seat !== startSeat) {
34
34
  const player = state.players[seat];
35
35
  // Skip if: no player, folded, all-in, or busted
36
- if (!player || player.status !== "ACTIVE" /* PlayerStatus.ACTIVE */ || player.stack === 0) {
36
+ if (player?.status !== "ACTIVE" /* PlayerStatus.ACTIVE */ || player.stack === 0) {
37
37
  seat = (0, positioning_1.getNextSeat)(seat, state.maxPlayers);
38
38
  continue;
39
39
  }
@@ -77,7 +77,7 @@ function getNextToActHeadsUp(state) {
77
77
  // Check both players
78
78
  for (const seat of actionOrder) {
79
79
  const player = state.players[seat];
80
- if (!player || player.status !== "ACTIVE" /* PlayerStatus.ACTIVE */ || player.stack === 0) {
80
+ if (player?.status !== "ACTIVE" /* PlayerStatus.ACTIVE */ || player.stack === 0) {
81
81
  continue;
82
82
  }
83
83
  const playerBet = state.currentBets.get(seat) ?? 0;
@@ -128,7 +128,7 @@ function getNextActionableSeat(startSeat, state) {
128
128
  // Scan full circle
129
129
  while (seat !== endSeat) {
130
130
  const player = state.players[seat];
131
- if (player && player.status === "ACTIVE" /* PlayerStatus.ACTIVE */ && player.stack > 0) {
131
+ if (player?.status === "ACTIVE" /* PlayerStatus.ACTIVE */ && player.stack > 0) {
132
132
  return seat;
133
133
  }
134
134
  seat = (0, positioning_1.getNextSeat)(seat, state.maxPlayers);
@@ -173,7 +173,7 @@ function isActionComplete(state) {
173
173
  if (activeCount === 0) {
174
174
  // Only return true if we're in a hand (not pre-deal)
175
175
  // Check: Are there all-in players with bets?
176
- const allInPlayers = state.players.filter((p) => p && p.status === "ALL_IN" /* PlayerStatus.ALL_IN */);
176
+ const allInPlayers = state.players.filter((p) => p?.status === "ALL_IN" /* PlayerStatus.ALL_IN */);
177
177
  return allInPlayers.length > 0 && state.currentBets.size > 0;
178
178
  }
179
179
  return activeCount > 0 && activeCount === actedCount;
@@ -16,6 +16,8 @@ export interface BlindPositions {
16
16
  * Normal (Dead Button Rule):
17
17
  * - SB = Button + 1 (Can be empty -> Dead Small Blind)
18
18
  * - BB = Next Occupied Seat after SB
19
+ *
20
+ * For cash games: Skips sitting-out players (unless they are in tournament mode)
19
21
  */
20
22
  export declare function getBlindPositions(state: GameState): BlindPositions | null;
21
23
  /**
@@ -14,15 +14,18 @@ const headsUp_1 = require("./headsUp");
14
14
  * Normal (Dead Button Rule):
15
15
  * - SB = Button + 1 (Can be empty -> Dead Small Blind)
16
16
  * - BB = Next Occupied Seat after SB
17
+ *
18
+ * For cash games: Skips sitting-out players (unless they are in tournament mode)
17
19
  */
18
20
  function getBlindPositions(state) {
19
21
  if (state.buttonSeat === null) {
20
22
  return null;
21
23
  }
22
24
  const buttonSeat = state.buttonSeat;
25
+ const isTournament = !!state.config.blindStructure;
23
26
  // Heads-up specific logic (Button is SB)
24
27
  if ((0, headsUp_1.isHeadsUp)(state)) {
25
- const bbSeat = (0, positioning_1.getNextOccupiedSeat)(buttonSeat, state.players, state.maxPlayers);
28
+ const bbSeat = getNextActiveOrOccupiedSeat(buttonSeat, state.players, state.maxPlayers, isTournament);
26
29
  if (bbSeat === null) {
27
30
  return null;
28
31
  }
@@ -34,8 +37,10 @@ function getBlindPositions(state) {
34
37
  // Normal Play (Dead Button / Dead Small Blind Logic)
35
38
  // 1. SB is ALWAYS the immediate next seat, even if empty
36
39
  const sbSeat = (0, positioning_1.getNextSeat)(buttonSeat, state.maxPlayers);
37
- // 2. BB is the next ACTIVE/OCCUPIED player after the SB position
38
- const bbSeat = (0, positioning_1.getNextOccupiedSeat)(sbSeat, state.players, state.maxPlayers);
40
+ // 2. BB is the next ACTIVE player after the SB position
41
+ // In cash games, skip sitting-out players
42
+ // In tournaments, include sitting-out players (they must post blinds)
43
+ const bbSeat = getNextActiveOrOccupiedSeat(sbSeat, state.players, state.maxPlayers, isTournament);
39
44
  if (bbSeat === null) {
40
45
  return null;
41
46
  }
@@ -44,6 +49,25 @@ function getBlindPositions(state) {
44
49
  bigBlindSeat: bbSeat,
45
50
  };
46
51
  }
52
+ /**
53
+ * Get next seat that is occupied and (if cash game) not sitting out
54
+ */
55
+ function getNextActiveOrOccupiedSeat(currentSeat, players, maxPlayers, isTournament) {
56
+ let seat = (0, positioning_1.getNextSeat)(currentSeat, maxPlayers);
57
+ const startSeat = currentSeat;
58
+ while (seat !== startSeat) {
59
+ const player = players[seat];
60
+ if (player !== null && player.stack > 0) {
61
+ // In tournaments, include sitting-out players (they must blind off)
62
+ // In cash games, skip sitting-out players
63
+ if (isTournament || !player.isSittingOut) {
64
+ return seat;
65
+ }
66
+ }
67
+ seat = (0, positioning_1.getNextSeat)(seat, maxPlayers);
68
+ }
69
+ return null; // No eligible seats
70
+ }
47
71
  /**
48
72
  * Calculate blind amounts for antes
49
73
  */
@@ -32,6 +32,24 @@ function getHeadsUpActionOrder(state, street) {
32
32
  }
33
33
  // Find the two seats
34
34
  const [seat1, seat2] = activePlayers.sort((a, b) => a - b);
35
+ // Check if button is one of the active players
36
+ const isButtonActive = activePlayers.includes(buttonSeat);
37
+ if (!isButtonActive) {
38
+ // Dead button scenario - button is not one of the active players
39
+ // In this case, the "button" for action purposes is the first active player
40
+ // after the actual button position
41
+ const effectiveButton = seat1 > buttonSeat || seat2 < buttonSeat ? seat1 : seat2;
42
+ const otherSeat = effectiveButton === seat1 ? seat2 : seat1;
43
+ if (street === "PREFLOP" /* Street.PREFLOP */) {
44
+ // Effective button acts first preflop
45
+ return [effectiveButton, otherSeat];
46
+ }
47
+ else {
48
+ // Effective button acts last postflop
49
+ return [otherSeat, effectiveButton];
50
+ }
51
+ }
52
+ // Normal case: button is one of the active players
35
53
  const otherSeat = seat1 === buttonSeat ? seat2 : seat1;
36
54
  if (street === "PREFLOP" /* Street.PREFLOP */) {
37
55
  // Button acts first preflop
@@ -23,6 +23,9 @@ function determineWinners(state) {
23
23
  });
24
24
  for (const pot of sortedPots) {
25
25
  const potWinners = evaluatePot(state, pot);
26
+ if (potWinners.length === 0) {
27
+ continue;
28
+ }
26
29
  // Calculate and deduct rake (cash games only)
27
30
  // Apply GLOBAL rake cap across all pots (per-hand, not per-pot)
28
31
  const { rake } = (0, rake_1.calculateRake)(state, pot.amount, totalRake);
@@ -122,6 +125,10 @@ function evaluatePot(state, pot) {
122
125
  for (const player of eligible) {
123
126
  if (!player?.hand)
124
127
  continue;
128
+ // Skip masked hands (client mode)
129
+ if (player.hand.some((c) => c === null)) {
130
+ continue;
131
+ }
125
132
  // Combine hole cards + board (7 cards total for river)
126
133
  const allCards = [...player.hand, ...state.board];
127
134
  if (allCards.length < 5) {
@@ -140,6 +147,9 @@ function evaluatePot(state, pot) {
140
147
  description,
141
148
  });
142
149
  }
150
+ if (evaluations.length === 0) {
151
+ return [];
152
+ }
143
153
  // Find best hand(s)
144
154
  const bestScore = Math.min(...evaluations.map((e) => e.score));
145
155
  const winners = evaluations.filter((e) => e.score === bestScore);
@@ -4,8 +4,9 @@
4
4
  export declare function cardCodesToStrings(codes: readonly number[]): string[];
5
5
  /**
6
6
  * Convert string card array to integer codes
7
+ * Filters out null (masked) cards
7
8
  */
8
- export declare function cardStringsToCards(cards: readonly string[]): number[];
9
+ export declare function cardStringsToCards(cards: ReadonlyArray<string | null>): number[];
9
10
  /**
10
11
  * Validate card string format
11
12
  */
@@ -12,9 +12,10 @@ function cardCodesToStrings(codes) {
12
12
  }
13
13
  /**
14
14
  * Convert string card array to integer codes
15
+ * Filters out null (masked) cards
15
16
  */
16
17
  function cardStringsToCards(cards) {
17
- return cards.map((card) => (0, evaluator_1.getCardCode)(card));
18
+ return cards.filter((c) => c !== null).map((card) => (0, evaluator_1.getCardCode)(card));
18
19
  }
19
20
  /**
20
21
  * Validate card string format
@@ -105,6 +105,10 @@ function getInitialChips(state) {
105
105
  * Checks multiple invariants beyond just chip conservation
106
106
  */
107
107
  function validateGameStateIntegrity(state) {
108
+ // Skip strict integrity checks in client mode to prevent UI crashes on minor sync issues
109
+ if (state.config.isClient) {
110
+ return;
111
+ }
108
112
  // 1. Chip conservation
109
113
  const initialChips = getInitialChips(state);
110
114
  auditChipConservation(state, initialChips);
@@ -30,7 +30,7 @@ function getActivePlayers(state) {
30
30
  const active = [];
31
31
  for (let i = 0; i < state.players.length; i++) {
32
32
  const player = state.players[i];
33
- if (player && player.status === "ACTIVE" /* PlayerStatus.ACTIVE */ && player.stack > 0) {
33
+ if (player?.status === "ACTIVE" /* PlayerStatus.ACTIVE */ && player.stack > 0) {
34
34
  active.push(i);
35
35
  }
36
36
  }
@@ -70,7 +70,7 @@ function getNextActionableSeat(currentSeat, state) {
70
70
  const startSeat = currentSeat;
71
71
  while (seat !== startSeat) {
72
72
  const player = state.players[seat];
73
- if (player && player.status === "ACTIVE" /* PlayerStatus.ACTIVE */ && player.stack > 0) {
73
+ if (player?.status === "ACTIVE" /* PlayerStatus.ACTIVE */ && player.stack > 0) {
74
74
  return seat;
75
75
  }
76
76
  seat = getNextSeat(seat, state.maxPlayers);
@@ -25,6 +25,7 @@ export interface Snapshot {
25
25
  readonly ante: number;
26
26
  readonly blindLevel: number;
27
27
  readonly timeBanks: Record<number, number>;
28
+ readonly timeBankActiveSeat: number | null;
28
29
  readonly actionHistory: ActionRecord[];
29
30
  readonly previousStates: Snapshot[];
30
31
  readonly timestamp: number;
@@ -44,6 +44,7 @@ function createSnapshot(state) {
44
44
  ante: state.ante,
45
45
  blindLevel: state.blindLevel,
46
46
  timeBanks,
47
+ timeBankActiveSeat: state.timeBankActiveSeat,
47
48
  actionHistory: Array.from(state.actionHistory),
48
49
  previousStates,
49
50
  timestamp: state.timestamp,
@@ -69,6 +70,7 @@ function restoreFromSnapshot(snapshot) {
69
70
  ...snapshot,
70
71
  currentBets,
71
72
  timeBanks,
73
+ timeBankActiveSeat: snapshot.timeBankActiveSeat ?? null, // Backward compatibility
72
74
  previousStates,
73
75
  rakeThisHand: snapshot.rakeThisHand || 0, // Add missing field with default
74
76
  };
@@ -6,9 +6,10 @@ import { GameState, PublicState } from "@pokertools/types";
6
6
  *
7
7
  * @param state Full game state
8
8
  * @param playerId Player requesting view (null = spectator)
9
+ * @param version State version number (defaults to 0 if not provided)
9
10
  * @returns Masked public state
10
11
  */
11
- export declare function createPublicView(state: GameState, playerId?: string | null): PublicState;
12
+ export declare function createPublicView(state: GameState, playerId?: string | null, version?: number): PublicState;
12
13
  /**
13
14
  * Create spectator view (no player-specific information)
14
15
  */