@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
@@ -6,8 +6,8 @@ exports.handleCall = handleCall;
6
6
  exports.handleBet = handleBet;
7
7
  exports.handleRaise = handleRaise;
8
8
  const positioning_1 = require("../utils/positioning");
9
- const actionOrder_1 = require("../rules/actionOrder");
10
- const CriticalStateError_1 = require("../errors/CriticalStateError");
9
+ const action_order_1 = require("../rules/action-order");
10
+ const critical_state_error_1 = require("../errors/critical-state-error");
11
11
  const rake_1 = require("../utils/rake");
12
12
  /**
13
13
  * Handle FOLD action
@@ -15,18 +15,15 @@ const rake_1 = require("../utils/rake");
15
15
  function handleFold(state, action) {
16
16
  const result = (0, positioning_1.getPlayerById)(state, action.playerId);
17
17
  if (!result) {
18
- return state; // Should have been caught by validation
18
+ return state;
19
19
  }
20
20
  const { seat } = result;
21
21
  const newPlayers = [...state.players];
22
- // Set player status to FOLDED
23
22
  newPlayers[seat] = {
24
23
  ...newPlayers[seat],
25
24
  status: "FOLDED" /* PlayerStatus.FOLDED */,
26
25
  };
27
- // Remove from active players
28
26
  const newActivePlayers = state.activePlayers.filter((s) => s !== seat);
29
- // Add to action history
30
27
  const actionRecord = {
31
28
  action,
32
29
  seat,
@@ -39,21 +36,14 @@ function handleFold(state, action) {
39
36
  players: newPlayers,
40
37
  activePlayers: newActivePlayers,
41
38
  actionHistory: [...state.actionHistory, actionRecord],
42
- timeBankActiveSeat: null, // Clear time bank flag on any action
39
+ timeBankActiveSeat: null,
43
40
  timestamp: action.timestamp,
44
41
  };
45
- // Check if only one player with a live hand remains
46
- // Count players who have not folded (Active + All-In)
47
42
  const playersWithLiveHands = currentState.players.filter((p) => p && p.status !== "FOLDED" /* PlayerStatus.FOLDED */);
48
- // Only end the hand if exactly one player has cards
49
43
  if (playersWithLiveHands.length === 1 && playersWithLiveHands[0]) {
50
- // Award remaining pots to last player with live hand
51
44
  return awardPotToLastPlayer(currentState, playersWithLiveHands[0].seat);
52
45
  }
53
- // If we have 1 Active player but multiple Live players (others are All-In),
54
- // the game should naturally progress to Showdown via progressStreet/checkAutoRunout
55
- // Move to next player
56
- const nextToAct = (0, actionOrder_1.getNextToAct)(currentState);
46
+ const nextToAct = (0, action_order_1.getNextToAct)(currentState);
57
47
  return {
58
48
  ...currentState,
59
49
  actionTo: nextToAct,
@@ -68,7 +58,6 @@ function handleCheck(state, action) {
68
58
  return state;
69
59
  }
70
60
  const { seat } = result;
71
- // Add to action history
72
61
  const actionRecord = {
73
62
  action,
74
63
  seat,
@@ -79,11 +68,9 @@ function handleCheck(state, action) {
79
68
  const newState = {
80
69
  ...state,
81
70
  actionHistory: [...state.actionHistory, actionRecord],
82
- timeBankActiveSeat: null, // Clear time bank flag on any action
83
71
  timestamp: action.timestamp,
84
72
  };
85
- // Move to next player
86
- const nextToAct = (0, actionOrder_1.getNextToAct)(newState);
73
+ const nextToAct = (0, action_order_1.getNextToAct)(newState);
87
74
  return {
88
75
  ...newState,
89
76
  actionTo: nextToAct,
@@ -98,14 +85,11 @@ function handleCall(state, action) {
98
85
  return state;
99
86
  }
100
87
  const { player, seat } = result;
101
- // Calculate amount to call
102
88
  const currentBet = getCurrentBet(state);
103
89
  const playerBet = state.currentBets.get(seat) ?? 0;
104
90
  const toCall = currentBet - playerBet;
105
- // Determine actual call amount (may be all-in)
106
91
  const callAmount = Math.min(toCall, player.stack);
107
92
  const isAllIn = callAmount === player.stack;
108
- // Update player
109
93
  const newPlayers = [...state.players];
110
94
  newPlayers[seat] = {
111
95
  ...player,
@@ -114,12 +98,10 @@ function handleCall(state, action) {
114
98
  totalInvestedThisHand: player.totalInvestedThisHand + callAmount,
115
99
  status: isAllIn ? "ALL_IN" /* PlayerStatus.ALL_IN */ : "ACTIVE" /* PlayerStatus.ACTIVE */,
116
100
  };
117
- // Update current bets
118
101
  const newCurrentBets = new Map(state.currentBets);
119
102
  newCurrentBets.set(seat, playerBet + callAmount);
120
- // Add to action history (include actual call amount in the action)
121
103
  const actionRecord = {
122
- action: { ...action, amount: callAmount }, // Populate amount field for history
104
+ action: { ...action, amount: callAmount },
123
105
  seat,
124
106
  resultingPot: getTotalPot(state) + callAmount,
125
107
  resultingStack: newPlayers[seat].stack,
@@ -130,11 +112,9 @@ function handleCall(state, action) {
130
112
  players: newPlayers,
131
113
  currentBets: newCurrentBets,
132
114
  actionHistory: [...state.actionHistory, actionRecord],
133
- timeBankActiveSeat: null, // Clear time bank flag on any action
134
115
  timestamp: action.timestamp,
135
116
  };
136
- // Move to next player
137
- const nextToAct = (0, actionOrder_1.getNextToAct)(newState);
117
+ const nextToAct = (0, action_order_1.getNextToAct)(newState);
138
118
  return {
139
119
  ...newState,
140
120
  actionTo: nextToAct,
@@ -151,7 +131,6 @@ function handleBet(state, action) {
151
131
  const { player, seat } = result;
152
132
  const betAmount = Math.min(action.amount, player.stack);
153
133
  const isAllIn = betAmount === player.stack;
154
- // Update player
155
134
  const newPlayers = [...state.players];
156
135
  newPlayers[seat] = {
157
136
  ...player,
@@ -160,10 +139,8 @@ function handleBet(state, action) {
160
139
  totalInvestedThisHand: player.totalInvestedThisHand + betAmount,
161
140
  status: isAllIn ? "ALL_IN" /* PlayerStatus.ALL_IN */ : "ACTIVE" /* PlayerStatus.ACTIVE */,
162
141
  };
163
- // Update current bets
164
142
  const newCurrentBets = new Map(state.currentBets);
165
143
  newCurrentBets.set(seat, betAmount);
166
- // Add to action history
167
144
  const actionRecord = {
168
145
  action,
169
146
  seat,
@@ -175,15 +152,13 @@ function handleBet(state, action) {
175
152
  ...state,
176
153
  players: newPlayers,
177
154
  currentBets: newCurrentBets,
178
- minRaise: betAmount + betAmount, // Min raise is current bet + raise increment
155
+ minRaise: betAmount + betAmount,
179
156
  lastRaiseAmount: betAmount,
180
157
  lastAggressorSeat: seat,
181
158
  actionHistory: [...state.actionHistory, actionRecord],
182
- timeBankActiveSeat: null, // Clear time bank flag on any action
183
159
  timestamp: action.timestamp,
184
160
  };
185
- // Move to next player
186
- const nextToAct = (0, actionOrder_1.getNextToAct)(newState);
161
+ const nextToAct = (0, action_order_1.getNextToAct)(newState);
187
162
  return {
188
163
  ...newState,
189
164
  actionTo: nextToAct,
@@ -203,9 +178,7 @@ function handleRaise(state, action) {
203
178
  const raiseAmount = Math.min(action.amount, playerBet + player.stack);
204
179
  const addedChips = raiseAmount - playerBet;
205
180
  const isAllIn = addedChips === player.stack;
206
- // Calculate raise increment
207
181
  const raiseIncrement = raiseAmount - currentBet;
208
- // Update player
209
182
  const newPlayers = [...state.players];
210
183
  newPlayers[seat] = {
211
184
  ...player,
@@ -214,10 +187,8 @@ function handleRaise(state, action) {
214
187
  totalInvestedThisHand: player.totalInvestedThisHand + addedChips,
215
188
  status: isAllIn ? "ALL_IN" /* PlayerStatus.ALL_IN */ : "ACTIVE" /* PlayerStatus.ACTIVE */,
216
189
  };
217
- // Update current bets
218
190
  const newCurrentBets = new Map(state.currentBets);
219
191
  newCurrentBets.set(seat, raiseAmount);
220
- // Add to action history
221
192
  const actionRecord = {
222
193
  action,
223
194
  seat,
@@ -225,12 +196,9 @@ function handleRaise(state, action) {
225
196
  resultingStack: newPlayers[seat].stack,
226
197
  street: state.street,
227
198
  };
228
- // Determine if this reopens betting (incomplete raise rule)
199
+ // Incomplete-raise rule: does this raise reopen the betting?
229
200
  const reopensBetting = raiseIncrement >= state.lastRaiseAmount;
230
- // Min-raise calculation:
231
- // - If reopens betting: new currentBet + new increment
232
- // - If incomplete raise: new currentBet + old increment (TDA/WSOP rule)
233
- // Example: P1 bets 100, P2 all-in 120, P3 must raise to 120+100=220 minimum
201
+ // When reopened, min raise = new bet + new increment; otherwise, new bet + old increment.
234
202
  const newMinRaise = reopensBetting
235
203
  ? raiseAmount + raiseIncrement
236
204
  : raiseAmount + state.lastRaiseAmount;
@@ -242,11 +210,9 @@ function handleRaise(state, action) {
242
210
  lastRaiseAmount: reopensBetting ? raiseIncrement : state.lastRaiseAmount,
243
211
  lastAggressorSeat: reopensBetting ? seat : state.lastAggressorSeat,
244
212
  actionHistory: [...state.actionHistory, actionRecord],
245
- timeBankActiveSeat: null, // Clear time bank flag on any action
246
213
  timestamp: action.timestamp,
247
214
  };
248
- // Move to next player
249
- const nextToAct = (0, actionOrder_1.getNextToAct)(newState);
215
+ const nextToAct = (0, action_order_1.getNextToAct)(newState);
250
216
  return {
251
217
  ...newState,
252
218
  actionTo: nextToAct,
@@ -278,31 +244,26 @@ function getTotalPot(state) {
278
244
  return total;
279
245
  }
280
246
  /**
281
- * Award pots to remaining eligible players when hand ends by folds
282
- * Properly handles side pot eligibility and uncalled bets
247
+ * Award pots to remaining eligible players when hand ends by folds.
283
248
  *
284
249
  * Key principle: Uncalled bets are NOT raked and are returned to the bettor immediately.
285
- * Only the contested portion of the pot (money actually at risk) is subject to rake.
250
+ * Only the contested portion is subject to rake.
286
251
  */
287
252
  function awardPotToLastPlayer(state, winningSeat) {
288
253
  const newPlayers = [...state.players];
289
254
  const newActionHistory = [...state.actionHistory];
290
255
  const winners = [];
291
- // Process each pot separately, checking eligibility
292
256
  let totalRakeFromPots = 0;
293
257
  for (const pot of state.pots) {
294
- // Find all non-folded players eligible for this pot
295
258
  const eligibleNonFolded = pot.eligibleSeats.filter((seat) => {
296
259
  const player = state.players[seat];
297
260
  return player && player.status !== "FOLDED" /* PlayerStatus.FOLDED */;
298
261
  });
299
- // Calculate rake for this pot - GLOBAL cap applied across all pots
300
262
  const { rake: potRake } = (0, rake_1.calculateRake)(state, pot.amount, totalRakeFromPots);
301
263
  totalRakeFromPots += potRake;
302
264
  const potAfterRake = pot.amount - potRake;
303
265
  if (eligibleNonFolded.length === 0) {
304
- // No eligible players remain - should not happen, but defensive
305
- // Award to last player to fold from eligible seats (fallback)
266
+ // Defensive fallback: no eligible players remain.
306
267
  const lastEligible = pot.eligibleSeats[pot.eligibleSeats.length - 1];
307
268
  const player = newPlayers[lastEligible];
308
269
  if (player) {
@@ -319,7 +280,6 @@ function awardPotToLastPlayer(state, winningSeat) {
319
280
  }
320
281
  }
321
282
  else if (eligibleNonFolded.length === 1) {
322
- // Exactly one eligible player - they win this pot
323
283
  const winnerSeat = eligibleNonFolded[0];
324
284
  const player = newPlayers[winnerSeat];
325
285
  newPlayers[winnerSeat] = {
@@ -334,9 +294,7 @@ function awardPotToLastPlayer(state, winningSeat) {
334
294
  });
335
295
  }
336
296
  else {
337
- // Multiple eligible players remain - this means awardPotToLastPlayer was called incorrectly
338
- // The hand should have gone to showdown instead
339
- throw new CriticalStateError_1.CriticalStateError("awardPotToLastPlayer called with multiple eligible players remaining", {
297
+ throw new critical_state_error_1.CriticalStateError("awardPotToLastPlayer called with multiple eligible players remaining", {
340
298
  potAmount: pot.amount,
341
299
  eligibleSeats: pot.eligibleSeats,
342
300
  eligibleNonFolded,
@@ -344,28 +302,25 @@ function awardPotToLastPlayer(state, winningSeat) {
344
302
  });
345
303
  }
346
304
  }
347
- // Handle current bets with proper uncalled bet logic
305
+ // Uncalled bet logic: three-step process.
348
306
  if (state.currentBets.size > 0) {
349
307
  const winnersBet = state.currentBets.get(winningSeat) ?? 0;
350
- // Find the second-highest bet (highest opponent bet)
351
- // This determines how much of the winner's bet was actually "called"
308
+ // Second-highest bet determines how much of winner's bet was actually "called".
352
309
  let maxOpponentBet = 0;
353
310
  for (const [seat, amount] of state.currentBets.entries()) {
354
311
  if (seat !== winningSeat && amount > maxOpponentBet) {
355
312
  maxOpponentBet = amount;
356
313
  }
357
314
  }
358
- // Calculate uncalled and called portions
359
315
  const uncalledAmount = winnersBet > maxOpponentBet ? winnersBet - maxOpponentBet : 0;
360
316
  const calledPortion = winnersBet > maxOpponentBet ? maxOpponentBet : winnersBet;
361
- // Step 1: Return uncalled bet immediately (NO RAKE on uncalled bets)
317
+ // Step 1: Return uncalled bet immediately (NO RAKE on uncalled bets).
362
318
  if (uncalledAmount > 0) {
363
319
  const player = newPlayers[winningSeat];
364
320
  newPlayers[winningSeat] = {
365
321
  ...player,
366
322
  stack: player.stack + uncalledAmount,
367
323
  };
368
- // Record the uncalled bet return
369
324
  newActionHistory.push({
370
325
  action: {
371
326
  type: "UNCALLED_BET_RETURNED" /* ActionType.UNCALLED_BET_RETURNED */,
@@ -379,14 +334,14 @@ function awardPotToLastPlayer(state, winningSeat) {
379
334
  street: state.street,
380
335
  });
381
336
  }
382
- // Step 2: Calculate contested pot (winner's called portion + all opponent bets)
337
+ // Step 2: Contested pot = winner's called portion + all opponent bets.
383
338
  let contestedPot = calledPortion;
384
339
  for (const [seat, amount] of state.currentBets.entries()) {
385
340
  if (seat !== winningSeat) {
386
341
  contestedPot += amount;
387
342
  }
388
343
  }
389
- // Step 3: Rake and award the contested portion only
344
+ // Step 3: Rake and award the contested portion only.
390
345
  if (contestedPot > 0) {
391
346
  const { rake } = (0, rake_1.calculateRake)(state, contestedPot, totalRakeFromPots);
392
347
  const totalRake = totalRakeFromPots + rake;
@@ -396,8 +351,6 @@ function awardPotToLastPlayer(state, winningSeat) {
396
351
  ...player,
397
352
  stack: player.stack + winnings,
398
353
  };
399
- // Update winners array
400
- // Only count actual winnings (contested pot after rake, minus winner's own contribution)
401
354
  const actualWinnings = winnings - calledPortion;
402
355
  if (actualWinnings > 0) {
403
356
  const existingIndex = winners.findIndex((w) => w.seat === winningSeat);
@@ -416,16 +369,13 @@ function awardPotToLastPlayer(state, winningSeat) {
416
369
  });
417
370
  }
418
371
  }
419
- // Update total rake for the hand
420
372
  totalRakeFromPots = totalRake;
421
373
  }
422
374
  }
423
- // NOTE: We do NOT reset totalInvestedThisHand here because it's used by getInitialChips()
424
- // to calculate total chips in the game. It will be reset when a new hand is dealt.
425
375
  return {
426
376
  ...state,
427
377
  players: newPlayers,
428
- street: "SHOWDOWN" /* Street.SHOWDOWN */, // Mark hand as complete
378
+ street: "SHOWDOWN" /* Street.SHOWDOWN */,
429
379
  pots: [],
430
380
  currentBets: new Map(),
431
381
  winners,
@@ -2,9 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.handleDeal = handleDeal;
4
4
  const deck_1 = require("../utils/deck");
5
- const cardUtils_1 = require("../utils/cardUtils");
5
+ const card_utils_1 = require("../utils/card-utils");
6
6
  const blinds_1 = require("../rules/blinds");
7
- const actionOrder_1 = require("../rules/actionOrder");
7
+ const action_order_1 = require("../rules/action-order");
8
8
  const positioning_1 = require("../utils/positioning");
9
9
  /**
10
10
  * Deal a new hand
@@ -14,66 +14,56 @@ const positioning_1 = require("../utils/positioning");
14
14
  * - Sets action to first to act
15
15
  */
16
16
  function handleDeal(state, action) {
17
- // Move button (Dead Button logic: moves to next seat index regardless of occupancy)
17
+ // Dead Button rule: advances to next seat index regardless of occupancy.
18
18
  let newButtonSeat = moveButton(state);
19
- // Determine if this is a tournament
20
19
  const isTournament = !!state.config.blindStructure;
21
20
  const isClient = !!state.config.isClient;
22
- // Create and shuffle deck (server only)
23
- // In client mode, we use an empty deck and deal masked cards
21
+ // Client mode: empty deck, cards dealt as masked (null).
24
22
  const rng = state.config.randomProvider ?? Math.random;
25
23
  const deck = isClient ? [] : (0, deck_1.shuffle)((0, deck_1.createDeck)(), rng);
26
- // Create a copy of timeBanks to modify
27
24
  const newTimeBanks = new Map(state.timeBanks);
28
- // First, merge pendingAddOn into stack for all players
25
+ // Merge pending add-ons; expire stale reservations.
29
26
  const newPlayers = state.players.map((player) => {
30
27
  if (!player)
31
28
  return null;
32
- // Skip reserved players (they haven't confirmed yet)
33
29
  if (player.status === "RESERVED" /* PlayerStatus.RESERVED */) {
34
- // Check if reservation has expired
35
30
  if (player.reservationExpiry && action.timestamp >= player.reservationExpiry) {
36
- // Reservation expired, remove player
37
31
  newTimeBanks.delete(player.seat);
38
32
  return null;
39
33
  }
40
- // Keep reserved player as-is
41
34
  return player;
42
35
  }
43
- // Merge pendingAddOn into stack
44
36
  const newStack = player.stack + player.pendingAddOn;
45
37
  return {
46
38
  ...player,
47
39
  stack: newStack,
48
- pendingAddOn: 0, // Clear pending add-on
40
+ pendingAddOn: 0,
49
41
  };
50
42
  });
51
43
  newButtonSeat = moveHeadsUpButtonToOccupiedSeat(newButtonSeat, {
52
44
  ...state,
53
45
  players: newPlayers,
54
46
  });
55
- // Get blind positions for this hand
47
+ // Recompute blind positions with updated button/players.
56
48
  const blindPositions = (0, blinds_1.getBlindPositions)({
57
49
  ...state,
58
50
  buttonSeat: newButtonSeat,
59
51
  players: newPlayers,
60
52
  });
61
- // Get players who will be dealt in
53
+ // Determine which players receive cards.
62
54
  const playersToReceive = [];
63
55
  for (let seat = 0; seat < newPlayers.length; seat++) {
64
56
  const player = newPlayers[seat];
65
- // Basic eligibility checks
66
57
  if (!player || player.stack <= 0 || player.status === "RESERVED" /* PlayerStatus.RESERVED */) {
67
58
  continue;
68
59
  }
69
- // We check WAIT_FOR_BB *before* checking isSittingOut.
70
- // This allows us to "unsit" a player if they hit the Big Blind.
60
+ // WAIT_FOR_BB check: sit player IN when they reach the Big Blind,
61
+ // sit player OUT when they haven't reached it yet.
71
62
  let shouldPlay = true;
72
63
  if (!isTournament && player.sitInOption === "WAIT_FOR_BB" /* SitInOption.WAIT_FOR_BB */) {
73
64
  const isInBigBlind = blindPositions?.bigBlindSeat === seat;
74
65
  if (isInBigBlind) {
75
- // PLAYER RE-ENTRY: They are in the Big Blind. Force them active.
76
- // We must update the player object in newPlayers to reflect they are back.
66
+ // PLAYER RE-ENTRY: Force them active when they are in the Big Blind.
77
67
  newPlayers[seat] = {
78
68
  ...player,
79
69
  isSittingOut: false,
@@ -81,8 +71,7 @@ function handleDeal(state, action) {
81
71
  shouldPlay = true;
82
72
  }
83
73
  else {
84
- // Not in BB yet. Force them to sit out.
85
- // Only update if not already sitting out to avoid object churn
74
+ // Not yet in BB; force sit-out (avoids object churn if already sitting out).
86
75
  if (!player.isSittingOut) {
87
76
  newPlayers[seat] = {
88
77
  ...player,
@@ -93,67 +82,60 @@ function handleDeal(state, action) {
93
82
  }
94
83
  }
95
84
  else if (player.isSittingOut) {
96
- // Standard sitting out check
97
85
  shouldPlay = false;
98
86
  }
99
87
  if (shouldPlay) {
100
88
  playersToReceive.push(seat);
101
89
  }
102
90
  }
103
- // Deal 2 cards to each player
104
91
  let remainingDeck = deck;
105
- // Initialize hands for receiving players (active, not sitting out)
106
92
  for (const seat of playersToReceive) {
107
93
  newPlayers[seat] = {
108
94
  ...newPlayers[seat],
109
- hand: [], // Initialize empty array
110
- shownCards: null, // Reset from previous hand
95
+ hand: [],
96
+ shownCards: null,
111
97
  status: "ACTIVE" /* PlayerStatus.ACTIVE */,
112
98
  betThisStreet: 0,
113
99
  totalInvestedThisHand: 0,
114
100
  };
115
101
  }
116
- // In tournaments, initialize sitting-out players too (they must post blinds/antes)
102
+ // Tournament: sitting-out players initialized as FOLDED (must post blinds/antes).
117
103
  if (isTournament) {
118
104
  for (let seat = 0; seat < newPlayers.length; seat++) {
119
105
  const player = newPlayers[seat];
120
106
  if (player && player.stack > 0 && player.isSittingOut && !playersToReceive.includes(seat)) {
121
107
  newPlayers[seat] = {
122
108
  ...player,
123
- hand: null, // No cards dealt
109
+ hand: null,
124
110
  shownCards: null,
125
- status: "FOLDED" /* PlayerStatus.FOLDED */, // Start as folded
111
+ status: "FOLDED" /* PlayerStatus.FOLDED */,
126
112
  betThisStreet: 0,
127
113
  totalInvestedThisHand: 0,
128
114
  };
129
115
  }
130
116
  }
131
117
  }
132
- // Deal 2 cards, one by one, in circle (standard poker procedure)
118
+ // Deal 2 hole cards, one at a time, clockwise.
133
119
  for (let round = 0; round < 2; round++) {
134
120
  for (const seat of playersToReceive) {
135
121
  let cardStrings;
136
122
  if (isClient) {
137
- // Client mode: Deal masked cards
138
123
  cardStrings = [null];
139
124
  }
140
125
  else {
141
- // Server mode: Deal from deck
142
126
  const [cards, nextDeck] = (0, deck_1.dealCards)(remainingDeck, 1);
143
127
  remainingDeck = nextDeck;
144
- cardStrings = (0, cardUtils_1.cardCodesToStrings)(cards);
128
+ cardStrings = (0, card_utils_1.cardCodesToStrings)(cards);
145
129
  }
146
- // Append to existing hand
147
130
  const currentPlayer = newPlayers[seat];
148
- const currentHand = currentPlayer.hand ?? []; // Should be [] from initialization
131
+ const currentHand = currentPlayer.hand ?? [];
149
132
  newPlayers[seat] = {
150
133
  ...currentPlayer,
151
134
  hand: [...currentHand, ...cardStrings],
152
135
  };
153
136
  }
154
137
  }
155
- // Post blinds and antes
156
- // Recalculate blind positions with the new button seat
138
+ // Post blinds and antes.
157
139
  const finalBlindPositions = (0, blinds_1.getBlindPositions)({
158
140
  ...state,
159
141
  buttonSeat: newButtonSeat,
@@ -162,9 +144,6 @@ function handleDeal(state, action) {
162
144
  const currentBets = new Map();
163
145
  if (finalBlindPositions) {
164
146
  const { smallBlindSeat, bigBlindSeat } = finalBlindPositions;
165
- // Post small blind
166
- // In tournaments: sitting-out players MUST post to prevent "blinding off" exploit
167
- // In cash games: sitting-out SB is treated as "Dead Small Blind" (no post)
168
147
  const sbPlayer = newPlayers[smallBlindSeat];
169
148
  if (sbPlayer && sbPlayer.stack > 0) {
170
149
  const shouldPostSB = isTournament || !sbPlayer.isSittingOut;
@@ -184,12 +163,11 @@ function handleDeal(state, action) {
184
163
  };
185
164
  }
186
165
  }
187
- // If sbPlayer is null or (cash game && sitting out), Dead Small Blind applies
188
- // Post big blind (Must exist for hand to start)
166
+ // Dead Small Blind: sbPlayer is null or (cash game && sitting out).
189
167
  const bbPlayer = newPlayers[bigBlindSeat];
190
168
  if (bbPlayer) {
191
- // In Cash Games: sitting-out players should NEVER post blinds (they are skipped by getBlindPositions)
192
- // In Tournaments: sitting-out players MUST post blinds to prevent "blinding off" exploit
169
+ // In cash games sitting-out players are skipped by getBlindPositions.
170
+ // In tournaments sitting-out players MUST post blinds (anti-blinding-off).
193
171
  const shouldPostBB = isTournament || !bbPlayer.isSittingOut;
194
172
  if (shouldPostBB) {
195
173
  const bbAmount = Math.min(bbPlayer.stack, state.bigBlind);
@@ -208,9 +186,7 @@ function handleDeal(state, action) {
208
186
  }
209
187
  }
210
188
  }
211
- // Post antes if configured
212
- // In tournaments: ALL players with chips must post (including sitting-out)
213
- // In cash games: Only active players post
189
+ // Antes: tournament all players with chips (incl. sitting-out); cash — active only.
214
190
  if (state.ante > 0) {
215
191
  const playersToAnteFrom = isTournament
216
192
  ? state.players.map((p, idx) => (p && p.stack > 0 ? idx : -1)).filter((idx) => idx >= 0)
@@ -236,9 +212,7 @@ function handleDeal(state, action) {
236
212
  }
237
213
  }
238
214
  }
239
- // Start with empty pots (bets will be collected when street progresses)
240
215
  const pots = [];
241
- // Get active players
242
216
  const activePlayers = playersToReceive.filter((seat) => {
243
217
  const player = newPlayers[seat];
244
218
  return player.status === "ACTIVE" /* PlayerStatus.ACTIVE */;
@@ -254,6 +228,8 @@ function handleDeal(state, action) {
254
228
  players: newPlayers,
255
229
  pots,
256
230
  currentBets,
231
+ initialChips: newPlayers.reduce((sum, player) => sum + (player ? player.stack : 0), 0) +
232
+ Array.from(currentBets.values()).reduce((sum, amount) => sum + amount, 0),
257
233
  minRaise: state.bigBlind,
258
234
  lastRaiseAmount: state.bigBlind,
259
235
  activePlayers,
@@ -262,8 +238,7 @@ function handleDeal(state, action) {
262
238
  actionHistory: [],
263
239
  timestamp: action.timestamp,
264
240
  };
265
- // Set first to act
266
- const firstToAct = (0, actionOrder_1.getFirstToAct)(newState);
241
+ const firstToAct = (0, action_order_1.getFirstToAct)(newState);
267
242
  return {
268
243
  ...newState,
269
244
  actionTo: firstToAct,
@@ -275,7 +250,6 @@ function handleDeal(state, action) {
275
250
  */
276
251
  function moveButton(state) {
277
252
  if (state.buttonSeat === null) {
278
- // First hand, find first seated player
279
253
  for (let seat = 0; seat < state.maxPlayers; seat++) {
280
254
  if (state.players[seat] !== null) {
281
255
  return seat;
@@ -283,8 +257,7 @@ function moveButton(state) {
283
257
  }
284
258
  return 0;
285
259
  }
286
- // Simply increment seat index (Dead Button)
287
- // We do not skip empty seats here.
260
+ // Dead Button: advance to next seat index regardless of occupancy.
288
261
  return (0, positioning_1.getNextSeat)(state.buttonSeat, state.maxPlayers);
289
262
  }
290
263
  function moveHeadsUpButtonToOccupiedSeat(buttonSeat, state) {
@@ -28,7 +28,6 @@ function handleSit(state, action) {
28
28
  };
29
29
  const newPlayers = [...state.players];
30
30
  newPlayers[action.seat] = newPlayer;
31
- // Add to time banks
32
31
  const newTimeBanks = new Map(state.timeBanks);
33
32
  newTimeBanks.set(action.seat, newPlayer.timeBank);
34
33
  return {
@@ -65,6 +64,8 @@ function handleStand(state, action) {
65
64
  // advanced to the next player. The invariant check will now pass.
66
65
  }
67
66
  // 2. Remove player from table (Standard Stand Logic)
67
+ const chipsLeavingTable = currentState.players[seat]?.stack ?? 0;
68
+ const currentBaseline = currentState.initialChips;
68
69
  const newPlayers = [...currentState.players];
69
70
  newPlayers[seat] = null;
70
71
  // Remove from time banks
@@ -79,6 +80,7 @@ function handleStand(state, action) {
79
80
  players: newPlayers,
80
81
  activePlayers: newActivePlayers,
81
82
  timeBanks: newTimeBanks,
83
+ initialChips: typeof currentBaseline === "number" ? currentBaseline - chipsLeavingTable : undefined,
82
84
  timestamp: action.timestamp,
83
85
  };
84
86
  }