@pokertools/engine 1.0.9 → 1.0.11

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.
@@ -24,13 +24,11 @@ function handleTimeout(state, action) {
24
24
  return state;
25
25
  }
26
26
  const { player, seat } = result;
27
- // Determine if player needs to call
28
27
  const currentBet = getCurrentBet(state);
29
28
  const playerBet = state.currentBets.get(seat) ?? 0;
30
29
  const needsToCall = currentBet > playerBet;
31
30
  const newPlayers = [...state.players];
32
31
  if (needsToCall) {
33
- // Player must fold
34
32
  newPlayers[seat] = {
35
33
  ...player,
36
34
  status: "FOLDED" /* PlayerStatus.FOLDED */,
@@ -38,7 +36,6 @@ function handleTimeout(state, action) {
38
36
  };
39
37
  }
40
38
  else {
41
- // Player can check, but mark as sitting out
42
39
  newPlayers[seat] = {
43
40
  ...player,
44
41
  isSittingOut: true,
@@ -61,7 +58,6 @@ function handleTimeout(state, action) {
61
58
  actionHistory: [...state.actionHistory, actionRecord],
62
59
  timestamp: action.timestamp,
63
60
  };
64
- // Move to next player
65
61
  const nextToAct = (0, action_order_1.getNextToAct)(newState);
66
62
  const actionableNext = nextToAct !== null && !newPlayers[nextToAct]?.isSittingOut ? nextToAct : null;
67
63
  return {
@@ -12,32 +12,25 @@ const action_order_1 = require("../rules/action-order");
12
12
  * - Resets action
13
13
  */
14
14
  function progressStreet(state) {
15
- // Determine next street
16
15
  const nextStreet = getNextStreet(state.street);
17
16
  if (nextStreet === null) {
18
- // Already at showdown or beyond
19
17
  return state;
20
18
  }
21
- // Check if we should auto-runout (all remaining players all-in)
22
- const shouldAutoRunout = checkAutoRunout(state);
23
- if (shouldAutoRunout) {
19
+ if (checkAutoRunout(state)) {
24
20
  return handleAutoRunout(state);
25
21
  }
26
- // Deal community cards
27
22
  const { board, deck } = dealCommunityCards(state, nextStreet);
28
- // Reset for new street
29
- // Note: Pots are calculated by recalculatePots() before progressStreet() is called
23
+ // Pots are recalculated by recalculatePots() before progressStreet() is called.
30
24
  const newState = {
31
25
  ...state,
32
26
  street: nextStreet,
33
27
  board,
34
28
  deck,
35
- pots: state.pots, // Keep existing pots (already updated by recalculatePots)
29
+ pots: state.pots,
36
30
  currentBets: new Map(),
37
31
  lastAggressorSeat: null,
38
- // Keep timestamp from last action (street progression is not a user action)
32
+ // Street progression is not a user action, keep last timestamp.
39
33
  };
40
- // Set first to act
41
34
  const firstToAct = (0, action_order_1.getFirstToAct)(newState);
42
35
  return {
43
36
  ...newState,
@@ -106,7 +99,6 @@ function checkAutoRunout(state) {
106
99
  allInCount++;
107
100
  }
108
101
  }
109
- // Auto-runout if ≤1 active player with chips (rest are all-in or folded)
110
102
  return activeCount <= 1 && allInCount > 0;
111
103
  }
112
104
  /**
@@ -116,25 +108,21 @@ function checkAutoRunout(state) {
116
108
  function handleAutoRunout(state) {
117
109
  let currentState = state;
118
110
  let currentStreet = state.street;
119
- // Deal remaining streets manually (FLOP -> TURN -> RIVER)
120
111
  while (currentStreet !== "RIVER" /* Street.RIVER */) {
121
112
  const nextStreet = getNextStreet(currentStreet);
122
113
  if (nextStreet === null || nextStreet === "SHOWDOWN" /* Street.SHOWDOWN */)
123
114
  break;
124
- // Deal community cards for this street
125
115
  const { board, deck } = dealCommunityCards(currentState, nextStreet);
126
- // Update state with new street and board
127
116
  currentState = {
128
117
  ...currentState,
129
118
  street: nextStreet,
130
119
  board,
131
120
  deck,
132
- currentBets: new Map(), // Clear bets between streets
121
+ currentBets: new Map(),
133
122
  lastAggressorSeat: null,
134
123
  };
135
124
  currentStreet = nextStreet;
136
125
  }
137
- // Move to showdown
138
126
  return {
139
127
  ...currentState,
140
128
  street: "SHOWDOWN" /* Street.SHOWDOWN */,
@@ -146,12 +134,9 @@ function handleAutoRunout(state) {
146
134
  * (All players have acted and matched bets)
147
135
  */
148
136
  function shouldProgressStreet(state) {
149
- if (state.actionTo !== null) {
150
- return false; // Action still in progress
151
- }
152
- if (state.street === "SHOWDOWN" /* Street.SHOWDOWN */) {
153
- return false; // Already at showdown
154
- }
155
- // Check if all active players have acted
137
+ if (state.actionTo !== null)
138
+ return false;
139
+ if (state.street === "SHOWDOWN" /* Street.SHOWDOWN */)
140
+ return false;
156
141
  return (0, action_order_1.isActionComplete)(state);
157
142
  }
@@ -5,17 +5,14 @@ exports.handleNextBlindLevel = handleNextBlindLevel;
5
5
  * Handle NEXT_BLIND_LEVEL action - advance to next blind level in tournament
6
6
  */
7
7
  function handleNextBlindLevel(state, action) {
8
- // Only applicable for tournaments
9
8
  if (!state.config.blindStructure) {
10
9
  return state;
11
10
  }
12
11
  const nextLevel = state.blindLevel + 1;
13
- // Check if we're at max level
14
12
  if (nextLevel >= state.config.blindStructure.length) {
15
- return state; // At max level, no change
13
+ return state;
16
14
  }
17
15
  const blindLevel = state.config.blindStructure[nextLevel];
18
- // Record action to history
19
16
  const actionRecord = {
20
17
  action: {
21
18
  type: "NEXT_BLIND_LEVEL" /* ActionType.NEXT_BLIND_LEVEL */,
@@ -2,14 +2,13 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.validateAction = validateAction;
4
4
  const illegal_action_error_1 = require("../errors/illegal-action-error");
5
- const error_codes_1 = require("../errors/error-codes");
5
+ const types_1 = require("@pokertools/types");
6
6
  const positioning_1 = require("../utils/positioning");
7
7
  /**
8
8
  * Validate that an action is legal in the current game state
9
9
  * Throws IllegalActionError if action is invalid
10
10
  */
11
11
  function validateAction(state, action) {
12
- // Type-specific validation
13
12
  switch (action.type) {
14
13
  case "FOLD" /* ActionType.FOLD */:
15
14
  case "CHECK" /* ActionType.CHECK */:
@@ -44,45 +43,41 @@ function validateAction(state, action) {
44
43
  }
45
44
  function validateBettingAction(state, action) {
46
45
  if (!("playerId" in action)) {
47
- throw new illegal_action_error_1.IllegalActionError(error_codes_1.ErrorCodes.INVALID_ACTION, "Action missing playerId");
46
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.INVALID_ACTION, "Action missing playerId");
48
47
  }
49
48
  const result = (0, positioning_1.getPlayerById)(state, action.playerId);
50
49
  if (!result) {
51
- throw new illegal_action_error_1.IllegalActionError(error_codes_1.ErrorCodes.PLAYER_NOT_FOUND, `Player ${action.playerId} not found`, { playerId: action.playerId });
50
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.PLAYER_NOT_FOUND, `Player ${action.playerId} not found`, { playerId: action.playerId });
52
51
  }
53
52
  const { player, seat } = result;
54
- // Check if it's player's turn
55
53
  if (state.actionTo !== seat) {
56
- throw new illegal_action_error_1.IllegalActionError(error_codes_1.ErrorCodes.NOT_YOUR_TURN, `Player ${action.playerId} attempted to act, but action is on seat ${state.actionTo}`, {
54
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.NOT_YOUR_TURN, `Player ${action.playerId} attempted to act, but action is on seat ${state.actionTo}`, {
57
55
  playerId: action.playerId,
58
56
  playerSeat: seat,
59
57
  actionTo: state.actionTo,
60
58
  street: state.street,
61
59
  });
62
60
  }
63
- // Check player status
64
61
  if (player.status !== "ACTIVE" /* PlayerStatus.ACTIVE */) {
65
- throw new illegal_action_error_1.IllegalActionError(error_codes_1.ErrorCodes.PLAYER_NOT_ACTIVE, `Player ${action.playerId} cannot act with status ${player.status}`, { playerId: action.playerId, status: player.status });
62
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.PLAYER_NOT_ACTIVE, `Player ${action.playerId} cannot act with status ${player.status}`, { playerId: action.playerId, status: player.status });
66
63
  }
67
- // Check player has chips
68
64
  if (player.stack === 0 && action.type !== "FOLD" /* ActionType.FOLD */) {
69
- throw new illegal_action_error_1.IllegalActionError(error_codes_1.ErrorCodes.NO_CHIPS, `Player ${action.playerId} has no chips`, {
65
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.NO_CHIPS, `Player ${action.playerId} has no chips`, {
70
66
  playerId: action.playerId,
71
67
  });
72
68
  }
73
- // Action-specific validation
74
69
  const currentBet = getCurrentBet(state);
75
70
  const playerBet = state.currentBets.get(seat) ?? 0;
76
71
  const toCall = currentBet - playerBet;
77
72
  switch (action.type) {
78
73
  case "CHECK" /* ActionType.CHECK */:
79
74
  if (toCall > 0) {
80
- throw new illegal_action_error_1.IllegalActionError(error_codes_1.ErrorCodes.CANNOT_CHECK, `Player ${action.playerId} cannot check with ${toCall} to call`, { playerId: action.playerId, toCall });
75
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.CANNOT_CHECK, `Player ${action.playerId} cannot check with ${toCall} to call`, { playerId: action.playerId, toCall });
81
76
  }
82
77
  break;
83
78
  case "CALL" /* ActionType.CALL */:
84
79
  if (toCall === 0) {
85
- throw new illegal_action_error_1.IllegalActionError(error_codes_1.ErrorCodes.NOTHING_TO_CALL, `Player ${action.playerId} has nothing to call`, { playerId: action.playerId });
80
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.NOTHING_TO_CALL, `Player ${action.playerId} has nothing to call`, { playerId: action.playerId });
86
81
  }
87
82
  break;
88
83
  case "BET" /* ActionType.BET */:
@@ -95,11 +90,11 @@ function validateBettingAction(state, action) {
95
90
  }
96
91
  // Reject bets below the current bet (string bet exploit)
97
92
  if (action.amount < currentBet) {
98
- throw new illegal_action_error_1.IllegalActionError(error_codes_1.ErrorCodes.BET_TOO_SMALL, `Bet of ${action.amount} is below current bet ${currentBet}`, { amount: action.amount, currentBet });
93
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.BET_TOO_SMALL, `Bet of ${action.amount} is below current bet ${currentBet}`, { amount: action.amount, currentBet });
99
94
  }
100
95
  // Reject bets below big blind (when no current bet)
101
96
  if (action.amount < state.bigBlind && action.amount < player.stack) {
102
- throw new illegal_action_error_1.IllegalActionError(error_codes_1.ErrorCodes.BET_TOO_SMALL, `Bet of ${action.amount} is below minimum ${state.bigBlind}`, { amount: action.amount, minimum: state.bigBlind });
97
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.BET_TOO_SMALL, `Bet of ${action.amount} is below minimum ${state.bigBlind}`, { amount: action.amount, minimum: state.bigBlind });
103
98
  }
104
99
  }
105
100
  break;
@@ -108,26 +103,26 @@ function validateBettingAction(state, action) {
108
103
  // intermediate actions (like calls or incomplete all-in raises) did NOT
109
104
  // reopen the betting. Therefore, they cannot re-raise their own bet.
110
105
  if (state.lastAggressorSeat === seat) {
111
- throw new illegal_action_error_1.IllegalActionError(error_codes_1.ErrorCodes.CANNOT_RERAISE, "Betting has not been re-opened to you (incomplete raise or no action)", {
106
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.CANNOT_RERAISE, "Betting has not been re-opened to you (incomplete raise or no action)", {
112
107
  playerId: action.playerId,
113
108
  seat,
114
109
  lastAggressor: state.lastAggressorSeat,
115
110
  });
116
111
  }
117
112
  if (currentBet === 0) {
118
- throw new illegal_action_error_1.IllegalActionError(error_codes_1.ErrorCodes.CANNOT_RAISE, `Player ${action.playerId} cannot raise when there's no bet`, { playerId: action.playerId });
113
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.CANNOT_RAISE, `Player ${action.playerId} cannot raise when there's no bet`, { playerId: action.playerId });
119
114
  }
120
115
  if ("amount" in action) {
121
116
  // Check if player is going all-in (incomplete raise exception)
122
117
  const isAllIn = action.amount >= playerBet + player.stack;
123
118
  // Reject raises that don't exceed current bet (unless all-in)
124
119
  if (action.amount <= currentBet && !isAllIn) {
125
- throw new illegal_action_error_1.IllegalActionError(error_codes_1.ErrorCodes.RAISE_TOO_SMALL, `Raise to ${action.amount} must be greater than current bet ${currentBet}`, { amount: action.amount, currentBet });
120
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.RAISE_TOO_SMALL, `Raise to ${action.amount} must be greater than current bet ${currentBet}`, { amount: action.amount, currentBet });
126
121
  }
127
122
  // Check minimum raise requirement (unless player is going all-in)
128
123
  const raiseIncrement = action.amount - currentBet;
129
124
  if (!isAllIn && action.amount < state.minRaise) {
130
- throw new illegal_action_error_1.IllegalActionError(error_codes_1.ErrorCodes.RAISE_TOO_SMALL, `Raise to ${action.amount} is below minimum ${state.minRaise}`, {
125
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.RAISE_TOO_SMALL, `Raise to ${action.amount} is below minimum ${state.minRaise}`, {
131
126
  amount: action.amount,
132
127
  currentBet,
133
128
  raiseIncrement,
@@ -145,73 +140,73 @@ function validateBetAsRaise(state, action, seat, playerBet, currentBet, playerSt
145
140
  if (typeof amount !== "number")
146
141
  return;
147
142
  if (state.lastAggressorSeat === seat) {
148
- throw new illegal_action_error_1.IllegalActionError(error_codes_1.ErrorCodes.CANNOT_RERAISE, "Betting has not been re-opened to you (incomplete raise or no action)", { playerId: action.playerId, seat, lastAggressor: state.lastAggressorSeat });
143
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.CANNOT_RERAISE, "Betting has not been re-opened to you (incomplete raise or no action)", { playerId: action.playerId, seat, lastAggressor: state.lastAggressorSeat });
149
144
  }
150
145
  const isAllIn = amount >= playerBet + playerStack;
151
146
  if (amount <= currentBet && !isAllIn) {
152
- throw new illegal_action_error_1.IllegalActionError(error_codes_1.ErrorCodes.RAISE_TOO_SMALL, `Raise to ${amount} must be greater than current bet ${currentBet}`, { amount, currentBet });
147
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.RAISE_TOO_SMALL, `Raise to ${amount} must be greater than current bet ${currentBet}`, { amount, currentBet });
153
148
  }
154
149
  if (!isAllIn && amount < state.minRaise) {
155
- throw new illegal_action_error_1.IllegalActionError(error_codes_1.ErrorCodes.RAISE_TOO_SMALL, `Raise to ${amount} is below minimum ${state.minRaise}`, { amount, currentBet, minRaise: state.minRaise });
150
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.RAISE_TOO_SMALL, `Raise to ${amount} is below minimum ${state.minRaise}`, { amount, currentBet, minRaise: state.minRaise });
156
151
  }
157
152
  }
158
153
  function validateDealAction(state) {
159
154
  if (state.street !== "PREFLOP" /* Street.PREFLOP */ || state.handNumber > 0) {
160
155
  // Allow dealing if we're at showdown (hand complete) or haven't started
161
156
  if (state.street !== "SHOWDOWN" /* Street.SHOWDOWN */ && state.handNumber > 0) {
162
- throw new illegal_action_error_1.IllegalActionError(error_codes_1.ErrorCodes.CANNOT_DEAL, "Cannot deal while hand is in progress", { street: state.street });
157
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.CANNOT_DEAL, "Cannot deal while hand is in progress", { street: state.street });
163
158
  }
164
159
  }
165
- // Check we have enough players
166
- const activePlayers = state.players.filter((p) => p && p.stack > 0 && !p.isSittingOut);
160
+ const isTournament = !!state.config.blindStructure;
161
+ const activePlayers = state.players.filter((p) => p && p.stack > 0 && (isTournament || !p.isSittingOut));
167
162
  if (activePlayers.length < 2) {
168
- throw new illegal_action_error_1.IllegalActionError(error_codes_1.ErrorCodes.NOT_ENOUGH_PLAYERS, `Need at least 2 players to deal, found ${activePlayers.length}`, { playerCount: activePlayers.length });
163
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.NOT_ENOUGH_PLAYERS, `Need at least 2 players to deal, found ${activePlayers.length}`, { playerCount: activePlayers.length });
169
164
  }
170
165
  }
171
166
  function validateSitAction(state, action) {
172
167
  if (action.seat < 0 || action.seat >= state.maxPlayers) {
173
- throw new illegal_action_error_1.IllegalActionError(error_codes_1.ErrorCodes.INVALID_SEAT, `Seat ${action.seat} is invalid (max: ${state.maxPlayers - 1})`, { seat: action.seat, maxPlayers: state.maxPlayers });
168
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.INVALID_SEAT, `Seat ${action.seat} is invalid (max: ${state.maxPlayers - 1})`, { seat: action.seat, maxPlayers: state.maxPlayers });
174
169
  }
175
170
  const existingPlayer = state.players[action.seat];
176
171
  if (existingPlayer !== null) {
177
172
  // Allow claiming the seat if it is RESERVED by THIS player
178
173
  const isMyReservation = existingPlayer.status === "RESERVED" /* PlayerStatus.RESERVED */ && existingPlayer.id === action.playerId;
179
174
  if (!isMyReservation) {
180
- throw new illegal_action_error_1.IllegalActionError(error_codes_1.ErrorCodes.SEAT_OCCUPIED, `Seat ${action.seat} is already occupied`, { seat: action.seat });
175
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.SEAT_OCCUPIED, `Seat ${action.seat} is already occupied`, { seat: action.seat });
181
176
  }
182
177
  }
183
178
  if (action.stack <= 0) {
184
- throw new illegal_action_error_1.IllegalActionError(error_codes_1.ErrorCodes.INVALID_STACK, `Stack must be positive, got ${action.stack}`, { stack: action.stack });
179
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.INVALID_STACK, `Stack must be positive, got ${action.stack}`, { stack: action.stack });
185
180
  }
186
181
  }
187
182
  function validateStandAction(state, action) {
188
183
  const result = (0, positioning_1.getPlayerById)(state, action.playerId);
189
184
  if (!result) {
190
- throw new illegal_action_error_1.IllegalActionError(error_codes_1.ErrorCodes.PLAYER_NOT_FOUND, `Player ${action.playerId} not found`, { playerId: action.playerId });
185
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.PLAYER_NOT_FOUND, `Player ${action.playerId} not found`, { playerId: action.playerId });
191
186
  }
192
187
  }
193
188
  function validateTimeAction(state, action) {
194
189
  const result = (0, positioning_1.getPlayerById)(state, action.playerId);
195
190
  if (!result) {
196
- throw new illegal_action_error_1.IllegalActionError(error_codes_1.ErrorCodes.PLAYER_NOT_FOUND, `Player ${action.playerId} not found`, { playerId: action.playerId });
191
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.PLAYER_NOT_FOUND, `Player ${action.playerId} not found`, { playerId: action.playerId });
197
192
  }
198
193
  const { seat } = result;
199
194
  if (state.actionTo !== seat) {
200
- throw new illegal_action_error_1.IllegalActionError(error_codes_1.ErrorCodes.NOT_YOUR_TURN, `Player ${action.playerId} cannot use time action when it's not their turn`, { playerId: action.playerId, actionTo: state.actionTo });
195
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.NOT_YOUR_TURN, `Player ${action.playerId} cannot use time action when it's not their turn`, { playerId: action.playerId, actionTo: state.actionTo });
201
196
  }
202
197
  }
203
198
  function validateAddChipsAction(state, action) {
204
199
  const result = (0, positioning_1.getPlayerById)(state, action.playerId);
205
200
  if (!result) {
206
- throw new illegal_action_error_1.IllegalActionError(error_codes_1.ErrorCodes.PLAYER_NOT_FOUND, `Player ${action.playerId} not found`, { playerId: action.playerId });
201
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.PLAYER_NOT_FOUND, `Player ${action.playerId} not found`, { playerId: action.playerId });
207
202
  }
208
203
  }
209
204
  function validateReserveSeatAction(state, action) {
210
205
  if (action.seat < 0 || action.seat >= state.maxPlayers) {
211
- throw new illegal_action_error_1.IllegalActionError(error_codes_1.ErrorCodes.INVALID_SEAT, `Seat ${action.seat} is invalid (max: ${state.maxPlayers - 1})`, { seat: action.seat, maxPlayers: state.maxPlayers });
206
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.INVALID_SEAT, `Seat ${action.seat} is invalid (max: ${state.maxPlayers - 1})`, { seat: action.seat, maxPlayers: state.maxPlayers });
212
207
  }
213
208
  if (state.players[action.seat] !== null) {
214
- throw new illegal_action_error_1.IllegalActionError(error_codes_1.ErrorCodes.SEAT_OCCUPIED, `Seat ${action.seat} is already occupied`, { seat: action.seat });
209
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.SEAT_OCCUPIED, `Seat ${action.seat} is already occupied`, { seat: action.seat });
215
210
  }
216
211
  }
217
212
  /**
package/dist/browser.js CHANGED
@@ -28,7 +28,6 @@ const poker_engine_1 = require("./engine/poker-engine");
28
28
  * Falls back to Math.random() only in environments without crypto
29
29
  */
30
30
  function getBrowserRNG() {
31
- // Check for Web Crypto API
32
31
  if (typeof window !== "undefined" && window.crypto?.getRandomValues) {
33
32
  return () => {
34
33
  const buffer = new Uint32Array(1);
@@ -22,12 +22,9 @@ const constants_1 = require("../utils/constants");
22
22
  * @returns New game state
23
23
  */
24
24
  function gameReducer(state, action) {
25
- // Validate action
26
25
  (0, validation_1.validateAction)(state, action);
27
- // Apply action based on type
28
26
  let newState;
29
27
  switch (action.type) {
30
- // Management actions
31
28
  case "SIT" /* ActionType.SIT */:
32
29
  newState = (0, management_1.handleSit)(state, action);
33
30
  break;
@@ -40,11 +37,9 @@ function gameReducer(state, action) {
40
37
  case "RESERVE_SEAT" /* ActionType.RESERVE_SEAT */:
41
38
  newState = (0, management_1.handleReserveSeat)(state, action);
42
39
  break;
43
- // Dealing
44
40
  case "DEAL" /* ActionType.DEAL */:
45
41
  newState = (0, dealing_1.handleDeal)(state, action);
46
42
  break;
47
- // Betting actions
48
43
  case "FOLD" /* ActionType.FOLD */:
49
44
  newState = (0, betting_1.handleFold)(state, action);
50
45
  break;
@@ -54,14 +49,12 @@ function gameReducer(state, action) {
54
49
  case "CALL" /* ActionType.CALL */:
55
50
  newState = (0, betting_1.handleCall)(state, action);
56
51
  break;
52
+ // Auto-convert BET to RAISE/CALL when there's already a wager.
53
+ // Note: action.amount is the TOTAL bet, not the increment.
57
54
  case "BET" /* ActionType.BET */:
58
- // Auto-convert BET to appropriate action if there's already a bet
59
- // This handles UI implementations that don't distinguish between BET/CALL/RAISE buttons
60
- // Note: action.amount is the TOTAL bet size, not the amount to add
61
55
  const currentBet = Math.max(...Array.from(state.currentBets.values()), 0);
62
56
  if (currentBet > 0 && "amount" in action) {
63
57
  if (action.amount === currentBet) {
64
- // Amount equals current bet -> Convert to CALL
65
58
  const callAction = {
66
59
  type: "CALL" /* ActionType.CALL */,
67
60
  playerId: action.playerId,
@@ -70,7 +63,6 @@ function gameReducer(state, action) {
70
63
  newState = (0, betting_1.handleCall)(state, callAction);
71
64
  }
72
65
  else if (action.amount > currentBet) {
73
- // Amount exceeds current bet -> Convert to RAISE
74
66
  const raiseAction = {
75
67
  type: "RAISE" /* ActionType.RAISE */,
76
68
  playerId: action.playerId,
@@ -80,7 +72,7 @@ function gameReducer(state, action) {
80
72
  newState = (0, betting_1.handleRaise)(state, raiseAction);
81
73
  }
82
74
  else {
83
- // Amount is less than current bet -> Keep as BET (will fail validation)
75
+ // amount < currentBet: pass through (will fail validation)
84
76
  newState = (0, betting_1.handleBet)(state, action);
85
77
  }
86
78
  }
@@ -91,14 +83,12 @@ function gameReducer(state, action) {
91
83
  case "RAISE" /* ActionType.RAISE */:
92
84
  newState = (0, betting_1.handleRaise)(state, action);
93
85
  break;
94
- // Showdown actions
95
86
  case "SHOW" /* ActionType.SHOW */:
96
87
  newState = (0, showdown_actions_1.handleShow)(state, action);
97
88
  break;
98
89
  case "MUCK" /* ActionType.MUCK */:
99
90
  newState = (0, showdown_actions_1.handleMuck)(state, action);
100
91
  break;
101
- // Special actions
102
92
  case "TIMEOUT" /* ActionType.TIMEOUT */:
103
93
  newState = (0, special_1.handleTimeout)(state, action);
104
94
  break;
@@ -109,33 +99,24 @@ function gameReducer(state, action) {
109
99
  newState = (0, tournament_1.handleNextBlindLevel)(state, action);
110
100
  break;
111
101
  default:
112
- // Unknown action type
113
102
  newState = state;
114
103
  break;
115
104
  }
116
- // Check if we should progress to next street
105
+ // Recalculate pots, then progress street.
117
106
  if ((0, street_progression_1.shouldProgressStreet)(newState)) {
118
- // Recalculate pots before progressing
119
107
  newState = (0, side_pots_1.recalculatePots)(newState);
120
108
  newState = (0, street_progression_1.progressStreet)(newState);
121
109
  }
122
- // Check if we should go to showdown
123
110
  if ((0, showdown_1.shouldShowdown)(newState)) {
124
- newState = {
125
- ...newState,
126
- street: newState.street, // Keep current street for showdown
127
- };
128
111
  newState = (0, showdown_1.determineWinners)(newState);
129
112
  }
130
113
  if (newState.actionTo !== null && newState.players[newState.actionTo]?.isSittingOut) {
131
114
  newState = { ...newState, actionTo: null };
132
115
  }
133
- // Audit chip conservation (throws on failure)
134
- // Enabled by default, can be disabled via config (not recommended for production)
116
+ // Integrity check; can be disabled via config (not recommended for production).
135
117
  if (newState.config.validateIntegrity !== false) {
136
118
  (0, invariants_1.validateGameStateIntegrity)(newState);
137
119
  }
138
- // Add to previous states for undo
139
120
  const previousStates = [...newState.previousStates, state].slice(-constants_1.MAX_UNDO_HISTORY);
140
121
  return {
141
122
  ...newState,
@@ -12,20 +12,18 @@ const validation_1 = require("../utils/validation");
12
12
  * Wraps the pure reducer with a stateful API
13
13
  */
14
14
  class PokerEngine {
15
+ currentState;
16
+ listeners = [];
17
+ timeProvider;
15
18
  constructor(config, timeProvider = () => Date.now()) {
16
- this.listeners = [];
17
- // Validate config
18
19
  this.validateConfig(config);
19
- // Initialize time provider
20
20
  this.timeProvider = timeProvider;
21
- // Initialize state
22
21
  this.currentState = this.createInitialState(config);
23
22
  }
24
23
  /**
25
24
  * Add a player to the table
26
25
  */
27
26
  sit(seat, id, name, stack) {
28
- // Validate chip amount is a non-negative integer
29
27
  (0, validation_1.validateChipAmount)(stack, "Sit stack");
30
28
  const action = {
31
29
  type: "SIT" /* ActionType.SIT */,
@@ -63,13 +61,10 @@ class PokerEngine {
63
61
  * If action.timestamp is not provided, the engine will automatically set it
64
62
  */
65
63
  act(action) {
66
- // Ensure timestamp is set
67
64
  const timestamp = action.timestamp ?? this.timeProvider();
68
- // Validate timestamp if provided by caller
69
65
  if (action.timestamp !== undefined) {
70
66
  (0, validation_1.validateTimestamp)(timestamp, this.currentState.timestamp);
71
67
  }
72
- // Validate chip amounts for betting actions
73
68
  if ("amount" in action && typeof action.amount === "number") {
74
69
  (0, validation_1.validateChipAmount)(action.amount, `${action.type} amount`);
75
70
  }
@@ -86,9 +81,7 @@ class PokerEngine {
86
81
  */
87
82
  validate(action) {
88
83
  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.
84
+ // Dry-run the reducer; we discard the result since it's immutable and pure.
92
85
  (0, game_reducer_1.gameReducer)(this.currentState, action);
93
86
  return { valid: true };
94
87
  }
@@ -110,11 +103,8 @@ class PokerEngine {
110
103
  // Hydrate PublicState into GameState if needed
111
104
  const newState = {
112
105
  ...serverState,
113
- // Ensure deck exists (empty for client/public state)
114
106
  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
107
+ players: serverState.players,
118
108
  config: {
119
109
  ...serverState.config,
120
110
  isClient: this.currentState.config.isClient,
@@ -174,7 +164,6 @@ class PokerEngine {
174
164
  */
175
165
  on(callback) {
176
166
  this.listeners.push(callback);
177
- // Return unsubscribe function
178
167
  return () => {
179
168
  const index = this.listeners.indexOf(callback);
180
169
  if (index > -1) {
@@ -191,9 +180,8 @@ class PokerEngine {
191
180
  }
192
181
  const nextLevel = this.currentState.blindLevel + 1;
193
182
  if (nextLevel >= this.currentState.config.blindStructure.length) {
194
- return; // At max level
183
+ return;
195
184
  }
196
- // Dispatch NEXT_BLIND_LEVEL action to notify listeners
197
185
  this.dispatch({
198
186
  type: "NEXT_BLIND_LEVEL" /* ActionType.NEXT_BLIND_LEVEL */,
199
187
  timestamp: this.timeProvider(),
@@ -223,13 +211,11 @@ class PokerEngine {
223
211
  const oldState = this.currentState;
224
212
  try {
225
213
  this.currentState = (0, game_reducer_1.gameReducer)(this.currentState, action);
226
- // Notify listeners
227
214
  for (const listener of this.listeners) {
228
215
  listener(action, oldState, this.currentState);
229
216
  }
230
217
  }
231
218
  catch (error) {
232
- // Re-throw error but keep old state
233
219
  throw error;
234
220
  }
235
221
  }
@@ -1,5 +1,5 @@
1
1
  import { PokerEngineError } from "./poker-engine-error";
2
- import { ErrorCode } from "./error-codes";
2
+ import { ErrorCode } from "@pokertools/types";
3
3
  /**
4
4
  * Error indicating an illegal or invalid action
5
5
  * Action should be rejected and error sent to client
@@ -14,9 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- // Export all errors
18
17
  __exportStar(require("./poker-engine-error"), exports);
19
18
  __exportStar(require("./critical-state-error"), exports);
20
19
  __exportStar(require("./illegal-action-error"), exports);
21
20
  __exportStar(require("./config-error"), exports);
22
- // ErrorCodes now exported from @pokertools/types
@@ -5,6 +5,8 @@ exports.PokerEngineError = void 0;
5
5
  * Base error class for all poker engine errors
6
6
  */
7
7
  class PokerEngineError extends Error {
8
+ code;
9
+ context;
8
10
  constructor(code, message, context = {}) {
9
11
  super(message);
10
12
  this.name = "PokerEngineError";
@@ -11,25 +11,19 @@ exports.exportToPokerStars = exportToPokerStars;
11
11
  */
12
12
  function exportToPokerStars(history, options = { format: "pokerstars" }) {
13
13
  const lines = [];
14
- // Header line
15
14
  lines.push(buildHeader(history));
16
15
  lines.push(buildTableInfo(history));
17
- // Button and seats
18
16
  lines.push(`Button: seat #${history.buttonSeat + 1}`);
19
17
  lines.push("");
20
- // Player stacks
21
18
  for (const player of history.players) {
22
19
  lines.push(`Seat ${player.seat + 1}: ${player.name} ($${player.startingStack.toFixed(2)} in chips)`);
23
20
  }
24
21
  lines.push("");
25
- // Blinds and antes
26
22
  lines.push(buildBlindsLine(history));
27
23
  if (history.stakes.ante > 0) {
28
24
  lines.push(buildAntesLine(history));
29
25
  }
30
- // Hole cards (dealt to)
31
26
  lines.push(buildHoleCardsLine(history, options));
32
- // Each street
33
27
  for (const street of history.streets) {
34
28
  lines.push("");
35
29
  lines.push(buildStreetHeader(street));
@@ -37,7 +31,6 @@ function exportToPokerStars(history, options = { format: "pokerstars" }) {
37
31
  lines.push(buildActionLine(action, history));
38
32
  }
39
33
  }
40
- // Summary
41
34
  lines.push("");
42
35
  lines.push(buildSummary(history));
43
36
  return lines.join("\n");
@@ -61,7 +54,6 @@ function buildTableInfo(history) {
61
54
  */
62
55
  function buildBlindsLine(history) {
63
56
  const lines = [];
64
- // Find SB and BB seats (button+1 and button+2)
65
57
  const sbSeat = (history.buttonSeat + 1) % history.maxPlayers;
66
58
  const bbSeat = (history.buttonSeat + 2) % history.maxPlayers;
67
59
  const sbPlayer = history.players.find((p) => p.seat === sbSeat);
@@ -84,10 +84,9 @@ function groupActionsByStreet(state) {
84
84
  grouped.set("TURN" /* Street.TURN */, []);
85
85
  grouped.set("RIVER" /* Street.RIVER */, []);
86
86
  grouped.set("SHOWDOWN" /* Street.SHOWDOWN */, []);
87
- // Group action history by street
88
87
  for (const record of state.actionHistory) {
89
88
  if (record.seat === null)
90
- continue; // Skip table-level actions
89
+ continue;
91
90
  const street = record.street ?? state.street;
92
91
  const existing = grouped.get(street) ?? [];
93
92
  const actionRecord = {
package/dist/index.js CHANGED
@@ -15,7 +15,6 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.exportMultipleHands = exports.getHandHistory = exports.exportHandHistory = exports.auditChipConservation = exports.calculateTotalChips = exports.createPublicView = exports.restoreFromSnapshot = exports.createSnapshot = exports.PokerEngine = void 0;
18
- // Main Engine Class
19
18
  var poker_engine_1 = require("./engine/poker-engine");
20
19
  Object.defineProperty(exports, "PokerEngine", { enumerable: true, get: function () { return poker_engine_1.PokerEngine; } });
21
20
  // Types (re-exported from @pokertools/types for convenience)