@pokertools/engine 1.0.8 → 1.0.10

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 (55) hide show
  1. package/README.md +8 -5
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/actions/betting.js +23 -73
  4. package/dist/actions/dealing.js +29 -56
  5. package/dist/actions/management.js +3 -1
  6. package/dist/actions/{showdownActions.js → showdown-actions.js} +13 -27
  7. package/dist/actions/special.js +20 -7
  8. package/dist/actions/{streetProgression.js → street-progression.js} +16 -31
  9. package/dist/actions/tournament.js +1 -4
  10. package/dist/actions/validation.js +48 -33
  11. package/dist/browser.d.ts +5 -5
  12. package/dist/browser.js +6 -7
  13. package/dist/engine/{gameReducer.js → game-reducer.js} +16 -32
  14. package/dist/engine/{PokerEngine.js → poker-engine.js} +16 -30
  15. package/dist/errors/{ConfigError.d.ts → config-error.d.ts} +1 -1
  16. package/dist/errors/{ConfigError.js → config-error.js} +2 -2
  17. package/dist/errors/{CriticalStateError.d.ts → critical-state-error.d.ts} +1 -1
  18. package/dist/errors/{CriticalStateError.js → critical-state-error.js} +2 -2
  19. package/dist/errors/{IllegalActionError.d.ts → illegal-action-error.d.ts} +2 -2
  20. package/dist/errors/{IllegalActionError.js → illegal-action-error.js} +2 -2
  21. package/dist/errors/index.d.ts +4 -4
  22. package/dist/errors/index.js +4 -6
  23. package/dist/errors/{PokerEngineError.js → poker-engine-error.js} +2 -0
  24. package/dist/history/exporter.js +4 -4
  25. package/dist/history/formats/pokerstars.js +0 -8
  26. package/dist/history/{handHistoryBuilder.js → hand-history-builder.js} +1 -2
  27. package/dist/index.d.ts +2 -2
  28. package/dist/index.js +4 -5
  29. package/dist/rules/{actionOrder.js → action-order.js} +12 -17
  30. package/dist/rules/blinds.js +3 -3
  31. package/dist/rules/{headsUp.js → heads-up.js} +0 -1
  32. package/dist/rules/showdown.js +45 -29
  33. package/dist/rules/{sidePots.js → side-pots.js} +7 -20
  34. package/dist/utils/deck.js +1 -4
  35. package/dist/utils/invariants.js +16 -10
  36. package/dist/utils/rake.js +6 -8
  37. package/dist/utils/serialization.d.ts +1 -0
  38. package/dist/utils/serialization.js +10 -5
  39. package/dist/utils/validation.js +8 -8
  40. package/dist/utils/{viewMasking.js → view-masking.js} +16 -6
  41. package/package.json +6 -6
  42. package/dist/errors/ErrorCodes.d.ts +0 -7
  43. package/dist/errors/ErrorCodes.js +0 -12
  44. /package/dist/actions/{showdownActions.d.ts → showdown-actions.d.ts} +0 -0
  45. /package/dist/actions/{streetProgression.d.ts → street-progression.d.ts} +0 -0
  46. /package/dist/engine/{gameReducer.d.ts → game-reducer.d.ts} +0 -0
  47. /package/dist/engine/{PokerEngine.d.ts → poker-engine.d.ts} +0 -0
  48. /package/dist/errors/{PokerEngineError.d.ts → poker-engine-error.d.ts} +0 -0
  49. /package/dist/history/{handHistoryBuilder.d.ts → hand-history-builder.d.ts} +0 -0
  50. /package/dist/rules/{actionOrder.d.ts → action-order.d.ts} +0 -0
  51. /package/dist/rules/{headsUp.d.ts → heads-up.d.ts} +0 -0
  52. /package/dist/rules/{sidePots.d.ts → side-pots.d.ts} +0 -0
  53. /package/dist/utils/{cardUtils.d.ts → card-utils.d.ts} +0 -0
  54. /package/dist/utils/{cardUtils.js → card-utils.js} +0 -0
  55. /package/dist/utils/{viewMasking.d.ts → view-masking.d.ts} +0 -0
@@ -34,8 +34,8 @@ function determineWinners(state) {
34
34
  // Distribute pot among winners
35
35
  const share = Math.floor(potAfterRake / potWinners.length);
36
36
  const remainder = potAfterRake % potWinners.length;
37
- // Sort winners by position (worst to best) for odd chip distribution
38
- // TDA Rule: Odd chips go to first player(s) clockwise from button
37
+ // Sort winners by position for odd chip distribution.
38
+ // TDA Rule: odd chips go to first player(s) clockwise from the button.
39
39
  const sortedWinners = [...potWinners].sort((a, b) => {
40
40
  if (state.buttonSeat === null)
41
41
  return 0;
@@ -47,20 +47,17 @@ function determineWinners(state) {
47
47
  for (let i = 0; i < sortedWinners.length; i++) {
48
48
  const evaluation = sortedWinners[i];
49
49
  let award = share;
50
- // Distribute odd chips one at a time to worst positions
51
- // (first N winners in sorted order get the extra chips)
50
+ // First N winners (worst position) each get one extra chip.
52
51
  if (i < remainder) {
53
52
  award += 1;
54
53
  }
55
- // Track winner seats
56
54
  winnerSeats.add(evaluation.seat);
57
- // Award chips to player
58
55
  const player = newPlayers[evaluation.seat];
59
56
  newPlayers[evaluation.seat] = {
60
57
  ...player,
61
58
  stack: player.stack + award,
62
59
  };
63
- // Record winner
60
+ // Record winner with hand description.
64
61
  winners.push({
65
62
  seat: evaluation.seat,
66
63
  amount: award,
@@ -104,11 +101,10 @@ function determineWinners(state) {
104
101
  * Evaluate a single pot and return winner(s)
105
102
  */
106
103
  function evaluatePot(state, pot) {
107
- // Get eligible players (not folded)
108
104
  const eligible = pot.eligibleSeats
109
105
  .map((seat) => state.players[seat])
110
106
  .filter((player) => player && (player.status === "ACTIVE" /* PlayerStatus.ACTIVE */ || player.status === "ALL_IN" /* PlayerStatus.ALL_IN */));
111
- // If only one player, they win without showing
107
+ // Single remaining player wins uncontested.
112
108
  if (eligible.length === 1) {
113
109
  const player = eligible[0];
114
110
  return [
@@ -120,22 +116,17 @@ function evaluatePot(state, pot) {
120
116
  },
121
117
  ];
122
118
  }
123
- // Evaluate all hands
124
119
  const evaluations = [];
125
120
  for (const player of eligible) {
126
121
  if (!player?.hand)
127
122
  continue;
128
- // Skip masked hands (client mode)
123
+ // Skip masked hands in client mode.
129
124
  if (player.hand.some((c) => c === null)) {
130
125
  continue;
131
126
  }
132
- // Combine hole cards + board (7 cards total for river)
133
127
  const allCards = [...player.hand, ...state.board];
134
- if (allCards.length < 5) {
135
- // Not enough cards (shouldn't happen)
128
+ if (allCards.length < 5)
136
129
  continue;
137
- }
138
- // Evaluate using @pokertools/evaluator
139
130
  const cardCodes = (0, evaluator_1.getCardCodes)(allCards);
140
131
  const score = (0, evaluator_1.evaluate)(cardCodes);
141
132
  const handRank = (0, evaluator_1.rank)(cardCodes);
@@ -143,32 +134,57 @@ function evaluatePot(state, pot) {
143
134
  evaluations.push({
144
135
  seat: player.seat,
145
136
  score,
146
- hand: [...player.hand], // Store hole cards (copy to mutable array)
137
+ hand: getBestFiveCardHand(allCards),
147
138
  description,
148
139
  });
149
140
  }
150
141
  if (evaluations.length === 0) {
151
142
  return [];
152
143
  }
153
- // Find best hand(s)
154
144
  const bestScore = Math.min(...evaluations.map((e) => e.score));
155
- const winners = evaluations.filter((e) => e.score === bestScore);
156
- return winners;
145
+ return evaluations.filter((e) => e.score === bestScore);
146
+ }
147
+ /**
148
+ * Return the concrete five cards that produce the best evaluator score.
149
+ * The evaluator score is lower-is-better, so enumerate all 5-card subsets and
150
+ * keep the first subset with the minimum score. Hold'em has at most 7 cards,
151
+ * making this deterministic brute-force path only 21 evaluations per player.
152
+ */
153
+ function getBestFiveCardHand(cards) {
154
+ if (cards.length === 5) {
155
+ return [...cards];
156
+ }
157
+ let bestHand = null;
158
+ let bestScore = Number.POSITIVE_INFINITY;
159
+ for (let a = 0; a < cards.length - 4; a++) {
160
+ for (let b = a + 1; b < cards.length - 3; b++) {
161
+ for (let c = b + 1; c < cards.length - 2; c++) {
162
+ for (let d = c + 1; d < cards.length - 1; d++) {
163
+ for (let e = d + 1; e < cards.length; e++) {
164
+ const candidate = [cards[a], cards[b], cards[c], cards[d], cards[e]];
165
+ const score = (0, evaluator_1.evaluate)((0, evaluator_1.getCardCodes)(candidate));
166
+ if (score < bestScore) {
167
+ bestScore = score;
168
+ bestHand = candidate;
169
+ }
170
+ }
171
+ }
172
+ }
173
+ }
174
+ }
175
+ if (bestHand === null) {
176
+ return [...cards.slice(0, 5)];
177
+ }
178
+ return bestHand;
157
179
  }
158
180
  /**
159
181
  * Check if hand should go to showdown
160
182
  */
161
183
  function shouldShowdown(state) {
162
- // Showdown if:
163
- // 1. We're at SHOWDOWN street
164
- // 2. Multiple players remain (not folded)
165
- // 3. Winners haven't been determined yet
166
- if (state.street !== "SHOWDOWN" /* Street.SHOWDOWN */) {
184
+ if (state.street !== "SHOWDOWN" /* Street.SHOWDOWN */)
185
+ return false;
186
+ if (state.winners !== null)
167
187
  return false;
168
- }
169
- if (state.winners !== null) {
170
- return false; // Already determined winners
171
- }
172
188
  const activePlayers = state.players.filter((p) => p && (p.status === "ACTIVE" /* PlayerStatus.ACTIVE */ || p.status === "ALL_IN" /* PlayerStatus.ALL_IN */));
173
189
  return activePlayers.length >= 2;
174
190
  }
@@ -4,7 +4,7 @@ exports.calculateSidePots = calculateSidePots;
4
4
  exports.calculateUncalledBet = calculateUncalledBet;
5
5
  exports.returnUncalledBet = returnUncalledBet;
6
6
  exports.recalculatePots = recalculatePots;
7
- const CriticalStateError_1 = require("../errors/CriticalStateError");
7
+ const critical_state_error_1 = require("../errors/critical-state-error");
8
8
  /**
9
9
  * Calculate side pots using iterative subtraction method
10
10
  *
@@ -36,11 +36,9 @@ function calculateSidePots(state) {
36
36
  });
37
37
  }
38
38
  }
39
- // If no investments, return empty
40
39
  if (investments.length === 0) {
41
40
  return [];
42
41
  }
43
- // Sort by investment (ascending)
44
42
  investments.sort((a, b) => a.amount - b.amount);
45
43
  const pots = [];
46
44
  let prevAmount = 0;
@@ -48,16 +46,13 @@ function calculateSidePots(state) {
48
46
  const current = investments[i];
49
47
  const allAtThisLevel = investments.slice(i); // Current + all higher investors
50
48
  const increment = current.amount - prevAmount;
51
- // Create pot for this level
52
49
  if (increment > 0) {
53
- // Pot includes chips from ALL players at this level (including folded)
50
+ // Pot includes ALL players at this level (folded players' chips stay in).
54
51
  const potAmount = increment * allAtThisLevel.length;
55
- // But only non-folded players are eligible to win
52
+ // Only non-folded players are eligible to win.
56
53
  const eligibleSeats = allAtThisLevel.filter((inv) => !inv.folded).map((inv) => inv.seat);
57
- // Must have at least one eligible player
58
- // If everyone folded at this level, something went wrong in the game logic
59
54
  if (eligibleSeats.length === 0) {
60
- throw new CriticalStateError_1.CriticalStateError(`Side pot has no eligible players - all ${allAtThisLevel.length} players at this level have folded`, {
55
+ throw new critical_state_error_1.CriticalStateError(`Side pot has no eligible players - all ${allAtThisLevel.length} players at this level have folded`, {
61
56
  potAmount,
62
57
  potLevel: i,
63
58
  investmentLevel: current.amount,
@@ -86,10 +81,8 @@ function calculateSidePots(state) {
86
81
  * @returns Tuple of [uncalled amount, seat to return to]
87
82
  */
88
83
  function calculateUncalledBet(state) {
89
- if (state.currentBets.size === 0) {
84
+ if (state.currentBets.size === 0)
90
85
  return null;
91
- }
92
- // Find highest bet
93
86
  let maxBet = 0;
94
87
  let maxBetSeat = -1;
95
88
  let secondMaxBet = 0;
@@ -119,21 +112,17 @@ function returnUncalledBet(state) {
119
112
  }
120
113
  const [amount, seat] = uncalled;
121
114
  const player = state.players[seat];
122
- if (!player) {
115
+ if (!player)
123
116
  return state;
124
- }
125
- // Return chips to player
126
117
  const newPlayers = [...state.players];
127
118
  newPlayers[seat] = {
128
119
  ...player,
129
120
  stack: player.stack + amount,
130
121
  totalInvestedThisHand: player.totalInvestedThisHand - amount,
131
122
  };
132
- // Reduce current bet
133
123
  const newCurrentBets = new Map(state.currentBets);
134
124
  const currentBet = newCurrentBets.get(seat) ?? 0;
135
125
  newCurrentBets.set(seat, currentBet - amount);
136
- // Record to action history
137
126
  const actionRecord = {
138
127
  action: {
139
128
  type: "UNCALLED_BET_RETURNED" /* ActionType.UNCALLED_BET_RETURNED */,
@@ -158,11 +147,9 @@ function returnUncalledBet(state) {
158
147
  * This is called before progressing to next street
159
148
  */
160
149
  function recalculatePots(state) {
161
- // First, return any uncalled bet
162
150
  const newState = returnUncalledBet(state);
163
- // Calculate side pots based on all investments
164
151
  const pots = calculateSidePots(newState);
165
- // Reset betThisStreet for all players (bets collected into pots)
152
+ // Reset betThisStreet after collecting bets into pots.
166
153
  const newPlayers = newState.players.map((p) => (p ? { ...p, betThisStreet: 0 } : null));
167
154
  return {
168
155
  ...newState,
@@ -28,10 +28,8 @@ function createDeck() {
28
28
  * be used for production poker games. Always provide a secure RNG.
29
29
  */
30
30
  function getSecureRandom() {
31
- // Check if we're in Node.js environment
32
31
  if (typeof process !== "undefined" && process.versions?.node) {
33
32
  try {
34
- // Use Node.js crypto for production
35
33
  // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
36
34
  const crypto = require("crypto");
37
35
  return () => {
@@ -81,11 +79,10 @@ function getSecureRandom() {
81
79
  */
82
80
  function shuffle(deck, rng) {
83
81
  const random = rng ?? getSecureRandom();
84
- const shuffled = [...deck]; // Create mutable copy
82
+ const shuffled = [...deck];
85
83
  // Fisher-Yates shuffle
86
84
  for (let i = shuffled.length - 1; i > 0; i--) {
87
85
  const j = Math.floor(random() * (i + 1));
88
- // Swap elements
89
86
  const temp = shuffled[i];
90
87
  shuffled[i] = shuffled[j];
91
88
  shuffled[j] = temp;
@@ -7,7 +7,7 @@ exports.calculatePotTotal = calculatePotTotal;
7
7
  exports.calculateBetTotal = calculateBetTotal;
8
8
  exports.getInitialChips = getInitialChips;
9
9
  exports.validateGameStateIntegrity = validateGameStateIntegrity;
10
- const CriticalStateError_1 = require("../errors/CriticalStateError");
10
+ const critical_state_error_1 = require("../errors/critical-state-error");
11
11
  /**
12
12
  * Audit chip conservation
13
13
  * Formula: ∑(player.stack) + ∑(pot.amount) + ∑(currentBets) = initialChips
@@ -17,9 +17,10 @@ const CriticalStateError_1 = require("../errors/CriticalStateError");
17
17
  * @throws CriticalStateError if chips don't match
18
18
  */
19
19
  function auditChipConservation(state, initialChips) {
20
- const currentChips = getInitialChips(state);
20
+ const handComplete = state.winners !== null && state.pots.length === 0 && state.currentBets.size === 0;
21
+ const currentChips = calculateTotalChips(state) + (handComplete ? state.rakeThisHand : 0);
21
22
  if (currentChips !== initialChips) {
22
- throw new CriticalStateError_1.CriticalStateError(`Chip conservation violated: expected ${initialChips}, found ${currentChips}`, {
23
+ throw new critical_state_error_1.CriticalStateError(`Chip conservation violated: expected ${initialChips}, found ${currentChips}`, {
23
24
  expected: initialChips,
24
25
  actual: currentChips,
25
26
  difference: currentChips - initialChips,
@@ -77,6 +78,10 @@ function calculateBetTotal(state) {
77
78
  * - After hand complete: stack + rake (all chips have been distributed, rake removed)
78
79
  */
79
80
  function getInitialChips(state) {
81
+ const baseline = state.initialChips;
82
+ if (typeof baseline === "number") {
83
+ return baseline;
84
+ }
80
85
  let total = 0;
81
86
  // Hand is complete if winners are declared AND pots/bets have been distributed
82
87
  // This ensures we don't switch modes mid-hand
@@ -110,12 +115,13 @@ function validateGameStateIntegrity(state) {
110
115
  return;
111
116
  }
112
117
  // 1. Chip conservation
113
- const initialChips = getInitialChips(state);
118
+ const baseline = state.initialChips;
119
+ const initialChips = typeof baseline === "number" ? baseline : getInitialChips(state);
114
120
  auditChipConservation(state, initialChips);
115
121
  // 2. No negative stacks
116
122
  for (const player of state.players) {
117
123
  if (player && player.stack < 0) {
118
- throw new CriticalStateError_1.CriticalStateError(`Player ${player.id} has negative stack: ${player.stack}`, {
124
+ throw new critical_state_error_1.CriticalStateError(`Player ${player.id} has negative stack: ${player.stack}`, {
119
125
  playerId: player.id,
120
126
  stack: player.stack,
121
127
  });
@@ -124,7 +130,7 @@ function validateGameStateIntegrity(state) {
124
130
  // 3. No negative bets
125
131
  for (const [seat, bet] of state.currentBets.entries()) {
126
132
  if (bet < 0) {
127
- throw new CriticalStateError_1.CriticalStateError(`Seat ${seat} has negative bet: ${bet}`, {
133
+ throw new critical_state_error_1.CriticalStateError(`Seat ${seat} has negative bet: ${bet}`, {
128
134
  seat,
129
135
  bet,
130
136
  });
@@ -134,7 +140,7 @@ function validateGameStateIntegrity(state) {
134
140
  for (let i = 0; i < state.pots.length; i++) {
135
141
  const pot = state.pots[i];
136
142
  if (pot.amount < 0) {
137
- throw new CriticalStateError_1.CriticalStateError(`Pot ${i} has negative amount: ${pot.amount}`, {
143
+ throw new critical_state_error_1.CriticalStateError(`Pot ${i} has negative amount: ${pot.amount}`, {
138
144
  potIndex: i,
139
145
  amount: pot.amount,
140
146
  });
@@ -143,14 +149,14 @@ function validateGameStateIntegrity(state) {
143
149
  // 5. ActionTo must be valid seat or null
144
150
  if (state.actionTo !== null) {
145
151
  if (state.actionTo < 0 || state.actionTo >= state.maxPlayers) {
146
- throw new CriticalStateError_1.CriticalStateError(`Invalid actionTo: ${state.actionTo}`, {
152
+ throw new critical_state_error_1.CriticalStateError(`Invalid actionTo: ${state.actionTo}`, {
147
153
  actionTo: state.actionTo,
148
154
  maxPlayers: state.maxPlayers,
149
155
  });
150
156
  }
151
157
  const player = state.players[state.actionTo];
152
158
  if (!player) {
153
- throw new CriticalStateError_1.CriticalStateError(`ActionTo points to empty seat: ${state.actionTo}`, {
159
+ throw new critical_state_error_1.CriticalStateError(`ActionTo points to empty seat: ${state.actionTo}`, {
154
160
  actionTo: state.actionTo,
155
161
  });
156
162
  }
@@ -158,7 +164,7 @@ function validateGameStateIntegrity(state) {
158
164
  // 6. Button must be valid or null
159
165
  if (state.buttonSeat !== null) {
160
166
  if (state.buttonSeat < 0 || state.buttonSeat >= state.maxPlayers) {
161
- throw new CriticalStateError_1.CriticalStateError(`Invalid buttonSeat: ${state.buttonSeat}`, {
167
+ throw new critical_state_error_1.CriticalStateError(`Invalid buttonSeat: ${state.buttonSeat}`, {
162
168
  buttonSeat: state.buttonSeat,
163
169
  maxPlayers: state.maxPlayers,
164
170
  });
@@ -10,25 +10,23 @@ exports.calculateRake = calculateRake;
10
10
  * @returns Object with rake amount and whether cap was hit
11
11
  */
12
12
  function calculateRake(state, potAmount, rakeTakenSoFar = 0) {
13
- // No rake for tournaments (identified by presence of blind structure)
13
+ // No rake for tournaments (identified by presence of blindStructure)
14
14
  if (state.config.blindStructure) {
15
15
  return { rake: 0, capReached: false };
16
16
  }
17
- // No rake if not configured
18
17
  const rakePercent = state.config.rakePercent ?? 0;
19
18
  if (rakePercent === 0) {
20
19
  return { rake: 0, capReached: false };
21
20
  }
22
- // "No Flop, No Drop" rule (standard in most cash games)
23
- // If enabled (default true), no rake is taken if no flop was dealt
24
- // This applies whether hand ends preflop OR players go all-in preflop without seeing flop
25
- const noFlopNoDrop = state.config.noFlopNoDrop !== false; // Default true
21
+ // "No Flop, No Drop" rule (standard in most cash games).
22
+ // When enabled (default), no rake is taken if no flop was dealt,
23
+ // regardless of whether the hand ended preflop or all-in preflop.
24
+ const noFlopNoDrop = state.config.noFlopNoDrop !== false;
26
25
  if (noFlopNoDrop && state.board.length === 0) {
27
26
  return { rake: 0, capReached: false };
28
27
  }
29
- // Calculate rake as percentage
30
28
  let rake = Math.floor((potAmount * rakePercent) / 100);
31
- // Apply GLOBAL rake cap (per-hand, not per-pot)
29
+ // Apply per-hand rake cap (not per-pot).
32
30
  let capReached = false;
33
31
  if (state.config.rakeCap !== undefined) {
34
32
  const rakeAllowed = state.config.rakeCap - rakeTakenSoFar;
@@ -28,6 +28,7 @@ export interface Snapshot {
28
28
  readonly timeBankActiveSeat: number | null;
29
29
  readonly actionHistory: ActionRecord[];
30
30
  readonly previousStates: Snapshot[];
31
+ readonly initialChips?: number;
31
32
  readonly timestamp: number;
32
33
  readonly handId: string;
33
34
  }
@@ -19,8 +19,12 @@ function createSnapshot(state) {
19
19
  for (const [seat, time] of state.timeBanks.entries()) {
20
20
  timeBanks[seat] = time;
21
21
  }
22
- // Truncate previous states (keep last 10 only)
23
- const previousStates = state.previousStates.slice(-10).map((s) => createSnapshot(s));
22
+ // Truncate previous states and do not recursively serialize nested undo
23
+ // histories, avoiding exponential snapshot growth.
24
+ const previousStates = state.previousStates.slice(-10).map((s) => createSnapshot({
25
+ ...s,
26
+ previousStates: [],
27
+ }));
24
28
  return {
25
29
  config: state.config,
26
30
  players: [...state.players],
@@ -47,6 +51,7 @@ function createSnapshot(state) {
47
51
  timeBankActiveSeat: state.timeBankActiveSeat,
48
52
  actionHistory: Array.from(state.actionHistory),
49
53
  previousStates,
54
+ initialChips: state.initialChips,
50
55
  timestamp: state.timestamp,
51
56
  handId: state.handId,
52
57
  };
@@ -70,9 +75,10 @@ function restoreFromSnapshot(snapshot) {
70
75
  ...snapshot,
71
76
  currentBets,
72
77
  timeBanks,
73
- timeBankActiveSeat: snapshot.timeBankActiveSeat ?? null, // Backward compatibility
78
+ timeBankActiveSeat: snapshot.timeBankActiveSeat,
74
79
  previousStates,
75
- rakeThisHand: snapshot.rakeThisHand || 0, // Add missing field with default
80
+ initialChips: snapshot.initialChips,
81
+ rakeThisHand: snapshot.rakeThisHand,
76
82
  };
77
83
  }
78
84
  /**
@@ -93,7 +99,6 @@ function deserializeSnapshot(json) {
93
99
  */
94
100
  function validateSnapshot(snapshot) {
95
101
  try {
96
- // Basic validation
97
102
  if (!snapshot.handId)
98
103
  return false;
99
104
  if (snapshot.maxPlayers < 2 || snapshot.maxPlayers > 10)
@@ -5,8 +5,8 @@
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.validateChipAmount = validateChipAmount;
7
7
  exports.validateTimestamp = validateTimestamp;
8
- const IllegalActionError_1 = require("../errors/IllegalActionError");
9
- const ErrorCodes_1 = require("../errors/ErrorCodes");
8
+ const illegal_action_error_1 = require("../errors/illegal-action-error");
9
+ const types_1 = require("@pokertools/types");
10
10
  const constants_1 = require("./constants");
11
11
  /**
12
12
  * Validate that a chip amount is a non-negative integer
@@ -18,13 +18,13 @@ const constants_1 = require("./constants");
18
18
  */
19
19
  function validateChipAmount(amount, context) {
20
20
  if (!Number.isFinite(amount)) {
21
- throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.INVALID_AMOUNT, `${context}: ${amount} is not a valid number`, { amount, context });
21
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.INVALID_AMOUNT, `${context}: ${amount} is not a valid number`, { amount, context });
22
22
  }
23
23
  if (!Number.isInteger(amount)) {
24
- throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.INVALID_AMOUNT, `${context}: ${amount} must be an integer (fractional chips not allowed)`, { amount, context });
24
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.INVALID_AMOUNT, `${context}: ${amount} must be an integer (fractional chips not allowed)`, { amount, context });
25
25
  }
26
26
  if (amount < 0) {
27
- throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.INVALID_AMOUNT, `${context}: ${amount} cannot be negative`, { amount, context });
27
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.INVALID_AMOUNT, `${context}: ${amount} cannot be negative`, { amount, context });
28
28
  }
29
29
  }
30
30
  /**
@@ -36,17 +36,17 @@ function validateChipAmount(amount, context) {
36
36
  */
37
37
  function validateTimestamp(timestamp, previousTimestamp) {
38
38
  if (!Number.isFinite(timestamp) || timestamp < 0) {
39
- throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.INVALID_TIMESTAMP, `Invalid timestamp: ${timestamp}`, {
39
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.INVALID_TIMESTAMP, `Invalid timestamp: ${timestamp}`, {
40
40
  timestamp,
41
41
  });
42
42
  }
43
43
  // Allow some clock drift tolerance for "future" timestamps
44
44
  const now = Date.now() + constants_1.TIMESTAMP_FUTURE_TOLERANCE_MS;
45
45
  if (timestamp > now) {
46
- throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.INVALID_TIMESTAMP, `Timestamp ${timestamp} is in the future (current: ${Date.now()})`, { timestamp, currentTime: Date.now() });
46
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.INVALID_TIMESTAMP, `Timestamp ${timestamp} is in the future (current: ${Date.now()})`, { timestamp, currentTime: Date.now() });
47
47
  }
48
48
  // Ensure timestamps are monotonically increasing (or equal for same-time actions)
49
49
  if (previousTimestamp !== undefined && timestamp < previousTimestamp) {
50
- throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.INVALID_TIMESTAMP, `Timestamp ${timestamp} is before previous action timestamp ${previousTimestamp}`, { timestamp, previousTimestamp });
50
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.INVALID_TIMESTAMP, `Timestamp ${timestamp} is before previous action timestamp ${previousTimestamp}`, { timestamp, previousTimestamp });
51
51
  }
52
52
  }
@@ -24,20 +24,30 @@ function createPublicView(state, playerId = null, version = 0) {
24
24
  hand: visibleHand,
25
25
  };
26
26
  });
27
- // Convert Map to plain object for JSON serialization
28
- const currentBetsObj = {};
29
- for (const [seat, amount] of state.currentBets.entries()) {
30
- currentBetsObj[seat] = amount;
31
- }
27
+ const currentBets = createSerializableReadonlyMap(state.currentBets);
32
28
  return {
33
29
  ...state,
34
30
  deck: [], // Always hide deck
35
31
  players: maskedPlayers,
36
- currentBets: currentBetsObj, // Serializable record
32
+ currentBets,
37
33
  viewingPlayerId: playerId,
38
34
  version,
39
35
  };
40
36
  }
37
+ function createSerializableReadonlyMap(source) {
38
+ const map = new Map(source);
39
+ Object.defineProperty(map, "toJSON", {
40
+ value: () => {
41
+ const record = {};
42
+ for (const [seat, amount] of map.entries()) {
43
+ record[seat] = amount;
44
+ }
45
+ return record;
46
+ },
47
+ enumerable: false,
48
+ });
49
+ return map;
50
+ }
41
51
  /**
42
52
  * Get visible cards for a player based on shownCards and viewer permissions
43
53
  * Returns null (all hidden), full hand, or partial hand with positional context preserved
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pokertools/engine",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "Enterprise-grade Texas Hold'em poker engine",
5
5
  "author": "A.Aurelius",
6
6
  "license": "MIT",
@@ -63,14 +63,14 @@
63
63
  "access": "public"
64
64
  },
65
65
  "engines": {
66
- "node": ">=18.0.0"
66
+ "node": ">=24.0.0"
67
67
  },
68
68
  "dependencies": {
69
- "@pokertools/evaluator": "1.0.8",
70
- "@pokertools/types": "1.0.8"
69
+ "@pokertools/evaluator": "1.0.10",
70
+ "@pokertools/types": "1.0.10"
71
71
  },
72
72
  "peerDependencies": {
73
- "@pokertools/evaluator": "1.0.8",
74
- "@pokertools/types": "1.0.8"
73
+ "@pokertools/evaluator": "1.0.10",
74
+ "@pokertools/types": "1.0.10"
75
75
  }
76
76
  }
@@ -1,7 +0,0 @@
1
- /**
2
- * ErrorCodes are now managed in @pokertools/types for consistency across packages.
3
- * This file re-exports them for backwards compatibility.
4
- *
5
- * @deprecated Import from "@pokertools/types" instead
6
- */
7
- export { ErrorCodes, type ErrorCode, hasErrorCode } from "@pokertools/types";
@@ -1,12 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.hasErrorCode = exports.ErrorCodes = void 0;
4
- /**
5
- * ErrorCodes are now managed in @pokertools/types for consistency across packages.
6
- * This file re-exports them for backwards compatibility.
7
- *
8
- * @deprecated Import from "@pokertools/types" instead
9
- */
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; } });
File without changes
File without changes
File without changes
File without changes