@pokertools/engine 1.0.9 → 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.
@@ -11,7 +11,6 @@ const blinds_1 = require("./blinds");
11
11
  * Returns seat number or null if action is complete
12
12
  */
13
13
  function getNextToAct(state) {
14
- // Special case: heads-up has different rules
15
14
  if ((0, heads_up_1.isHeadsUp)(state)) {
16
15
  return getNextToActHeadsUp(state);
17
16
  }
@@ -42,12 +41,10 @@ function getNextToActNormal(state) {
42
41
  // 2. Current bet is higher than their bet
43
42
  const playerBet = state.currentBets.get(seat) ?? 0;
44
43
  if (playerBet < currentBet) {
45
- return seat; // Player needs to respond to bet
44
+ return seat;
46
45
  }
47
- // Player has matched current bet
48
- // Check if they've already acted
49
46
  if (!hasActedThisStreet(state, seat)) {
50
- return seat; // Player hasn't acted yet
47
+ return seat;
51
48
  }
52
49
  foundActionable = true;
53
50
  seat = (0, positioning_1.getNextSeat)(seat, state.maxPlayers);
@@ -125,7 +122,6 @@ function getFirstToAct(state) {
125
122
  function getNextActionableSeat(startSeat, state) {
126
123
  let seat = (0, positioning_1.getNextSeat)(startSeat, state.maxPlayers);
127
124
  const endSeat = startSeat;
128
- // Scan full circle
129
125
  while (seat !== endSeat) {
130
126
  const player = state.players[seat];
131
127
  if (player?.status === "ACTIVE" /* PlayerStatus.ACTIVE */ && player.stack > 0 && !player.isSittingOut) {
@@ -183,7 +179,6 @@ function isActionComplete(state) {
183
179
  * This is tracked by checking if they appear in the action history for this street
184
180
  */
185
181
  function hasActedThisStreet(state, seat) {
186
- // Find actions from current street
187
182
  const streetStartIndex = findStreetStartIndex(state);
188
183
  for (let i = streetStartIndex; i < state.actionHistory.length; i++) {
189
184
  if (state.actionHistory[i].seat === seat) {
@@ -66,7 +66,7 @@ function getNextActiveOrOccupiedSeat(currentSeat, players, maxPlayers, isTournam
66
66
  }
67
67
  seat = (0, positioning_1.getNextSeat)(seat, maxPlayers);
68
68
  }
69
- return null; // No eligible seats
69
+ return null;
70
70
  }
71
71
  /**
72
72
  * Calculate blind amounts for antes
@@ -32,7 +32,6 @@ 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
35
  const isButtonActive = activePlayers.includes(buttonSeat);
37
36
  if (!isButtonActive) {
38
37
  // Dead button scenario - button is not one of the active players
@@ -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);
@@ -150,10 +141,8 @@ function evaluatePot(state, pot) {
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);
157
146
  }
158
147
  /**
159
148
  * Return the concrete five cards that produce the best evaluator score.
@@ -192,16 +181,10 @@ function getBestFiveCardHand(cards) {
192
181
  * Check if hand should go to showdown
193
182
  */
194
183
  function shouldShowdown(state) {
195
- // Showdown if:
196
- // 1. We're at SHOWDOWN street
197
- // 2. Multiple players remain (not folded)
198
- // 3. Winners haven't been determined yet
199
- if (state.street !== "SHOWDOWN" /* Street.SHOWDOWN */) {
184
+ if (state.street !== "SHOWDOWN" /* Street.SHOWDOWN */)
185
+ return false;
186
+ if (state.winners !== null)
200
187
  return false;
201
- }
202
- if (state.winners !== null) {
203
- return false; // Already determined winners
204
- }
205
188
  const activePlayers = state.players.filter((p) => p && (p.status === "ACTIVE" /* PlayerStatus.ACTIVE */ || p.status === "ALL_IN" /* PlayerStatus.ALL_IN */));
206
189
  return activePlayers.length >= 2;
207
190
  }
@@ -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,14 +46,11 @@ 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
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,
@@ -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;
@@ -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;
@@ -75,10 +75,10 @@ function restoreFromSnapshot(snapshot) {
75
75
  ...snapshot,
76
76
  currentBets,
77
77
  timeBanks,
78
- timeBankActiveSeat: snapshot.timeBankActiveSeat ?? null, // Backward compatibility
78
+ timeBankActiveSeat: snapshot.timeBankActiveSeat,
79
79
  previousStates,
80
80
  initialChips: snapshot.initialChips,
81
- rakeThisHand: snapshot.rakeThisHand ?? 0, // Add missing field with default
81
+ rakeThisHand: snapshot.rakeThisHand,
82
82
  };
83
83
  }
84
84
  /**
@@ -99,7 +99,6 @@ function deserializeSnapshot(json) {
99
99
  */
100
100
  function validateSnapshot(snapshot) {
101
101
  try {
102
- // Basic validation
103
102
  if (!snapshot.handId)
104
103
  return false;
105
104
  if (snapshot.maxPlayers < 2 || snapshot.maxPlayers > 10)
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.validateChipAmount = validateChipAmount;
7
7
  exports.validateTimestamp = validateTimestamp;
8
8
  const illegal_action_error_1 = require("../errors/illegal-action-error");
9
- const error_codes_1 = require("../errors/error-codes");
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 illegal_action_error_1.IllegalActionError(error_codes_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 illegal_action_error_1.IllegalActionError(error_codes_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 illegal_action_error_1.IllegalActionError(error_codes_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 illegal_action_error_1.IllegalActionError(error_codes_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 illegal_action_error_1.IllegalActionError(error_codes_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 illegal_action_error_1.IllegalActionError(error_codes_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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pokertools/engine",
3
- "version": "1.0.9",
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.9",
70
- "@pokertools/types": "1.0.9"
69
+ "@pokertools/evaluator": "1.0.10",
70
+ "@pokertools/types": "1.0.10"
71
71
  },
72
72
  "peerDependencies": {
73
- "@pokertools/evaluator": "1.0.9",
74
- "@pokertools/types": "1.0.9"
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; } });