@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
@@ -2,48 +2,39 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.handleShow = handleShow;
4
4
  exports.handleMuck = handleMuck;
5
- const IllegalActionError_1 = require("../errors/IllegalActionError");
6
- const ErrorCodes_1 = require("../errors/ErrorCodes");
5
+ const illegal_action_error_1 = require("../errors/illegal-action-error");
6
+ const types_1 = require("@pokertools/types");
7
7
  /**
8
8
  * Handle SHOW action - player reveals their cards at showdown
9
9
  */
10
10
  function handleShow(state, action) {
11
- // Find player
12
11
  const player = state.players.find((p) => p?.id === action.playerId);
13
12
  if (!player) {
14
- throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.PLAYER_NOT_FOUND, `Player ${action.playerId} not found`, { playerId: action.playerId });
13
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.PLAYER_NOT_FOUND, `Player ${action.playerId} not found`, { playerId: action.playerId });
15
14
  }
16
- // Can only show at showdown
17
15
  if (state.street !== "SHOWDOWN" /* Street.SHOWDOWN */) {
18
- throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.INVALID_ACTION, `Can only show cards at showdown (current street: ${state.street})`, { playerId: action.playerId, street: state.street });
16
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.INVALID_ACTION, `Can only show cards at showdown (current street: ${state.street})`, { playerId: action.playerId, street: state.street });
19
17
  }
20
- // Player must not have folded
21
18
  if (player.status === "FOLDED" /* PlayerStatus.FOLDED */) {
22
- throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.INVALID_ACTION, `Cannot show cards after folding`, {
19
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.INVALID_ACTION, `Cannot show cards after folding`, {
23
20
  playerId: action.playerId,
24
21
  status: player.status,
25
22
  });
26
23
  }
27
- // Player must have cards
28
24
  if (!player.hand) {
29
- throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.INVALID_ACTION, `Player has no cards to show`, {
25
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.INVALID_ACTION, `Player has no cards to show`, {
30
26
  playerId: action.playerId,
31
27
  });
32
28
  }
33
- // Determine which cards to show
34
29
  let cardIndices;
35
30
  if (action.cardIndices && action.cardIndices.length > 0) {
36
- // Validate indices are within bounds
37
31
  cardIndices = action.cardIndices.filter((i) => i >= 0 && i < player.hand.length);
38
- if (cardIndices.length === 0) {
39
- return state; // Invalid indices
40
- }
32
+ if (cardIndices.length === 0)
33
+ return state;
41
34
  }
42
35
  else {
43
- // Default: show all cards
44
36
  cardIndices = Array.from({ length: player.hand.length }, (_, i) => i);
45
37
  }
46
- // Update player's shown cards
47
38
  const newPlayers = [...state.players];
48
39
  newPlayers[player.seat] = {
49
40
  ...player,
@@ -72,32 +63,27 @@ function handleShow(state, action) {
72
63
  * Handle MUCK action - player hides their cards at showdown
73
64
  */
74
65
  function handleMuck(state, action) {
75
- // Find player
76
66
  const player = state.players.find((p) => p?.id === action.playerId);
77
67
  if (!player) {
78
- throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.PLAYER_NOT_FOUND, `Player ${action.playerId} not found`, { playerId: action.playerId });
68
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.PLAYER_NOT_FOUND, `Player ${action.playerId} not found`, { playerId: action.playerId });
79
69
  }
80
- // Can only muck at showdown
81
70
  if (state.street !== "SHOWDOWN" /* Street.SHOWDOWN */) {
82
- throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.INVALID_ACTION, `Can only muck cards at showdown (current street: ${state.street})`, { playerId: action.playerId, street: state.street });
71
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.INVALID_ACTION, `Can only muck cards at showdown (current street: ${state.street})`, { playerId: action.playerId, street: state.street });
83
72
  }
84
- // Player must not have folded
85
73
  if (player.status === "FOLDED" /* PlayerStatus.FOLDED */) {
86
- throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.INVALID_ACTION, `Cannot muck cards after folding`, {
74
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.INVALID_ACTION, `Cannot muck cards after folding`, {
87
75
  playerId: action.playerId,
88
76
  status: player.status,
89
77
  });
90
78
  }
91
- // Cannot muck if you're a winner (winners must show)
92
79
  const isWinner = state.winners?.some((w) => w.seat === player.seat);
93
80
  if (isWinner) {
94
- throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.INVALID_ACTION, `Winners cannot muck - cards must be shown`, { playerId: action.playerId });
81
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.INVALID_ACTION, `Winners cannot muck - cards must be shown`, { playerId: action.playerId });
95
82
  }
96
- // Set shown cards to null (mucked) - hand is preserved in player.hand
97
83
  const newPlayers = [...state.players];
98
84
  newPlayers[player.seat] = {
99
85
  ...player,
100
- shownCards: null, // Muck cards (hide them, but preserve hand data)
86
+ shownCards: null,
101
87
  };
102
88
  const actionRecord = {
103
89
  action: {
@@ -3,7 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.handleTimeout = handleTimeout;
4
4
  exports.handleTimeBank = handleTimeBank;
5
5
  const positioning_1 = require("../utils/positioning");
6
- const actionOrder_1 = require("../rules/actionOrder");
6
+ const action_order_1 = require("../rules/action-order");
7
+ function getTotalPot(state) {
8
+ let total = 0;
9
+ for (const pot of state.pots)
10
+ total += pot.amount;
11
+ for (const bet of state.currentBets.values())
12
+ total += bet;
13
+ return total;
14
+ }
7
15
  /**
8
16
  * Handle TIMEOUT action
9
17
  * - Folds player if they have bet to call
@@ -16,13 +24,11 @@ function handleTimeout(state, action) {
16
24
  return state;
17
25
  }
18
26
  const { player, seat } = result;
19
- // Determine if player needs to call
20
27
  const currentBet = getCurrentBet(state);
21
28
  const playerBet = state.currentBets.get(seat) ?? 0;
22
29
  const needsToCall = currentBet > playerBet;
23
30
  const newPlayers = [...state.players];
24
31
  if (needsToCall) {
25
- // Player must fold
26
32
  newPlayers[seat] = {
27
33
  ...player,
28
34
  status: "FOLDED" /* PlayerStatus.FOLDED */,
@@ -30,7 +36,6 @@ function handleTimeout(state, action) {
30
36
  };
31
37
  }
32
38
  else {
33
- // Player can check, but mark as sitting out
34
39
  newPlayers[seat] = {
35
40
  ...player,
36
41
  isSittingOut: true,
@@ -39,17 +44,25 @@ function handleTimeout(state, action) {
39
44
  const newActivePlayers = needsToCall
40
45
  ? state.activePlayers.filter((s) => s !== seat)
41
46
  : state.activePlayers;
47
+ const actionRecord = {
48
+ action,
49
+ seat,
50
+ resultingPot: getTotalPot(state),
51
+ resultingStack: newPlayers[seat]?.stack ?? 0,
52
+ street: state.street,
53
+ };
42
54
  const newState = {
43
55
  ...state,
44
56
  players: newPlayers,
45
57
  activePlayers: newActivePlayers,
58
+ actionHistory: [...state.actionHistory, actionRecord],
46
59
  timestamp: action.timestamp,
47
60
  };
48
- // Move to next player
49
- const nextToAct = (0, actionOrder_1.getNextToAct)(newState);
61
+ const nextToAct = (0, action_order_1.getNextToAct)(newState);
62
+ const actionableNext = nextToAct !== null && !newPlayers[nextToAct]?.isSittingOut ? nextToAct : null;
50
63
  return {
51
64
  ...newState,
52
- actionTo: nextToAct,
65
+ actionTo: actionableNext === seat ? null : actionableNext,
53
66
  };
54
67
  }
55
68
  /**
@@ -3,8 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.progressStreet = progressStreet;
4
4
  exports.shouldProgressStreet = shouldProgressStreet;
5
5
  const deck_1 = require("../utils/deck");
6
- const cardUtils_1 = require("../utils/cardUtils");
7
- const actionOrder_1 = require("../rules/actionOrder");
6
+ const card_utils_1 = require("../utils/card-utils");
7
+ const action_order_1 = require("../rules/action-order");
8
8
  /**
9
9
  * Progress to next street
10
10
  * - Collects bets into pots
@@ -12,33 +12,26 @@ const actionOrder_1 = require("../rules/actionOrder");
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
- const firstToAct = (0, actionOrder_1.getFirstToAct)(newState);
34
+ const firstToAct = (0, action_order_1.getFirstToAct)(newState);
42
35
  return {
43
36
  ...newState,
44
37
  actionTo: firstToAct,
@@ -69,21 +62,21 @@ function dealCommunityCards(state, street) {
69
62
  // Burn 1, deal 3
70
63
  const [flopCards, flopDeck] = (0, deck_1.burnAndDeal)(deck, 3);
71
64
  return {
72
- board: [...currentBoard, ...(0, cardUtils_1.cardCodesToStrings)(flopCards)],
65
+ board: [...currentBoard, ...(0, card_utils_1.cardCodesToStrings)(flopCards)],
73
66
  deck: flopDeck,
74
67
  };
75
68
  case "TURN" /* Street.TURN */:
76
69
  // Burn 1, deal 1
77
70
  const [turnCards, turnDeck] = (0, deck_1.burnAndDeal)(deck, 1);
78
71
  return {
79
- board: [...currentBoard, ...(0, cardUtils_1.cardCodesToStrings)(turnCards)],
72
+ board: [...currentBoard, ...(0, card_utils_1.cardCodesToStrings)(turnCards)],
80
73
  deck: turnDeck,
81
74
  };
82
75
  case "RIVER" /* Street.RIVER */:
83
76
  // Burn 1, deal 1
84
77
  const [riverCards, riverDeck] = (0, deck_1.burnAndDeal)(deck, 1);
85
78
  return {
86
- board: [...currentBoard, ...(0, cardUtils_1.cardCodesToStrings)(riverCards)],
79
+ board: [...currentBoard, ...(0, card_utils_1.cardCodesToStrings)(riverCards)],
87
80
  deck: riverDeck,
88
81
  };
89
82
  default:
@@ -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
156
- return (0, actionOrder_1.isActionComplete)(state);
137
+ if (state.actionTo !== null)
138
+ return false;
139
+ if (state.street === "SHOWDOWN" /* Street.SHOWDOWN */)
140
+ return false;
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 */,
@@ -1,15 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.validateAction = validateAction;
4
- const IllegalActionError_1 = require("../errors/IllegalActionError");
5
- const ErrorCodes_1 = require("../errors/ErrorCodes");
4
+ const illegal_action_error_1 = require("../errors/illegal-action-error");
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,58 +43,58 @@ function validateAction(state, action) {
44
43
  }
45
44
  function validateBettingAction(state, action) {
46
45
  if (!("playerId" in action)) {
47
- throw new IllegalActionError_1.IllegalActionError(ErrorCodes_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 IllegalActionError_1.IllegalActionError(ErrorCodes_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 IllegalActionError_1.IllegalActionError(ErrorCodes_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 IllegalActionError_1.IllegalActionError(ErrorCodes_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 IllegalActionError_1.IllegalActionError(ErrorCodes_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 IllegalActionError_1.IllegalActionError(ErrorCodes_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 IllegalActionError_1.IllegalActionError(ErrorCodes_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 */:
89
84
  // Note: We allow BET even when currentBet > 0 because the reducer will auto-convert it to RAISE or CALL
90
85
  // This handles UI implementations that don't distinguish between BET and RAISE buttons
91
86
  if ("amount" in action) {
87
+ if (currentBet > 0) {
88
+ validateBetAsRaise(state, action, seat, playerBet, currentBet, player.stack);
89
+ break;
90
+ }
92
91
  // Reject bets below the current bet (string bet exploit)
93
- if (currentBet > 0 && action.amount < currentBet) {
94
- throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.BET_TOO_SMALL, `Bet of ${action.amount} is below current bet ${currentBet}`, { amount: action.amount, currentBet });
92
+ if (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 });
95
94
  }
96
95
  // Reject bets below big blind (when no current bet)
97
96
  if (action.amount < state.bigBlind && action.amount < player.stack) {
98
- throw new IllegalActionError_1.IllegalActionError(ErrorCodes_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 });
99
98
  }
100
99
  }
101
100
  break;
@@ -104,26 +103,26 @@ function validateBettingAction(state, action) {
104
103
  // intermediate actions (like calls or incomplete all-in raises) did NOT
105
104
  // reopen the betting. Therefore, they cannot re-raise their own bet.
106
105
  if (state.lastAggressorSeat === seat) {
107
- throw new IllegalActionError_1.IllegalActionError(ErrorCodes_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)", {
108
107
  playerId: action.playerId,
109
108
  seat,
110
109
  lastAggressor: state.lastAggressorSeat,
111
110
  });
112
111
  }
113
112
  if (currentBet === 0) {
114
- throw new IllegalActionError_1.IllegalActionError(ErrorCodes_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 });
115
114
  }
116
115
  if ("amount" in action) {
117
116
  // Check if player is going all-in (incomplete raise exception)
118
117
  const isAllIn = action.amount >= playerBet + player.stack;
119
118
  // Reject raises that don't exceed current bet (unless all-in)
120
119
  if (action.amount <= currentBet && !isAllIn) {
121
- throw new IllegalActionError_1.IllegalActionError(ErrorCodes_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 });
122
121
  }
123
122
  // Check minimum raise requirement (unless player is going all-in)
124
123
  const raiseIncrement = action.amount - currentBet;
125
124
  if (!isAllIn && action.amount < state.minRaise) {
126
- throw new IllegalActionError_1.IllegalActionError(ErrorCodes_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}`, {
127
126
  amount: action.amount,
128
127
  currentBet,
129
128
  raiseIncrement,
@@ -134,63 +133,79 @@ function validateBettingAction(state, action) {
134
133
  break;
135
134
  }
136
135
  }
136
+ function validateBetAsRaise(state, action, seat, playerBet, currentBet, playerStack) {
137
+ if (!("amount" in action))
138
+ return;
139
+ const amount = action.amount;
140
+ if (typeof amount !== "number")
141
+ return;
142
+ if (state.lastAggressorSeat === seat) {
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 });
144
+ }
145
+ const isAllIn = amount >= playerBet + playerStack;
146
+ if (amount <= currentBet && !isAllIn) {
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 });
148
+ }
149
+ if (!isAllIn && amount < 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 });
151
+ }
152
+ }
137
153
  function validateDealAction(state) {
138
154
  if (state.street !== "PREFLOP" /* Street.PREFLOP */ || state.handNumber > 0) {
139
155
  // Allow dealing if we're at showdown (hand complete) or haven't started
140
156
  if (state.street !== "SHOWDOWN" /* Street.SHOWDOWN */ && state.handNumber > 0) {
141
- throw new IllegalActionError_1.IllegalActionError(ErrorCodes_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 });
142
158
  }
143
159
  }
144
- // Check we have enough players
145
160
  const activePlayers = state.players.filter((p) => p && p.stack > 0 && !p.isSittingOut);
146
161
  if (activePlayers.length < 2) {
147
- throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.NOT_ENOUGH_PLAYERS, `Need at least 2 players to deal, found ${activePlayers.length}`, { playerCount: activePlayers.length });
162
+ 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 });
148
163
  }
149
164
  }
150
165
  function validateSitAction(state, action) {
151
166
  if (action.seat < 0 || action.seat >= state.maxPlayers) {
152
- throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.INVALID_SEAT, `Seat ${action.seat} is invalid (max: ${state.maxPlayers - 1})`, { seat: action.seat, maxPlayers: state.maxPlayers });
167
+ 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 });
153
168
  }
154
169
  const existingPlayer = state.players[action.seat];
155
170
  if (existingPlayer !== null) {
156
171
  // Allow claiming the seat if it is RESERVED by THIS player
157
172
  const isMyReservation = existingPlayer.status === "RESERVED" /* PlayerStatus.RESERVED */ && existingPlayer.id === action.playerId;
158
173
  if (!isMyReservation) {
159
- throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.SEAT_OCCUPIED, `Seat ${action.seat} is already occupied`, { seat: action.seat });
174
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.SEAT_OCCUPIED, `Seat ${action.seat} is already occupied`, { seat: action.seat });
160
175
  }
161
176
  }
162
177
  if (action.stack <= 0) {
163
- throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.INVALID_STACK, `Stack must be positive, got ${action.stack}`, { stack: action.stack });
178
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.INVALID_STACK, `Stack must be positive, got ${action.stack}`, { stack: action.stack });
164
179
  }
165
180
  }
166
181
  function validateStandAction(state, action) {
167
182
  const result = (0, positioning_1.getPlayerById)(state, action.playerId);
168
183
  if (!result) {
169
- throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.PLAYER_NOT_FOUND, `Player ${action.playerId} not found`, { playerId: action.playerId });
184
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.PLAYER_NOT_FOUND, `Player ${action.playerId} not found`, { playerId: action.playerId });
170
185
  }
171
186
  }
172
187
  function validateTimeAction(state, action) {
173
188
  const result = (0, positioning_1.getPlayerById)(state, action.playerId);
174
189
  if (!result) {
175
- throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.PLAYER_NOT_FOUND, `Player ${action.playerId} not found`, { playerId: action.playerId });
190
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.PLAYER_NOT_FOUND, `Player ${action.playerId} not found`, { playerId: action.playerId });
176
191
  }
177
192
  const { seat } = result;
178
193
  if (state.actionTo !== seat) {
179
- throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.NOT_YOUR_TURN, `Player ${action.playerId} cannot use time action when it's not their turn`, { playerId: action.playerId, actionTo: state.actionTo });
194
+ 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 });
180
195
  }
181
196
  }
182
197
  function validateAddChipsAction(state, action) {
183
198
  const result = (0, positioning_1.getPlayerById)(state, action.playerId);
184
199
  if (!result) {
185
- throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.PLAYER_NOT_FOUND, `Player ${action.playerId} not found`, { playerId: action.playerId });
200
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.PLAYER_NOT_FOUND, `Player ${action.playerId} not found`, { playerId: action.playerId });
186
201
  }
187
202
  }
188
203
  function validateReserveSeatAction(state, action) {
189
204
  if (action.seat < 0 || action.seat >= state.maxPlayers) {
190
- throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.INVALID_SEAT, `Seat ${action.seat} is invalid (max: ${state.maxPlayers - 1})`, { seat: action.seat, maxPlayers: state.maxPlayers });
205
+ 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 });
191
206
  }
192
207
  if (state.players[action.seat] !== null) {
193
- throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.SEAT_OCCUPIED, `Seat ${action.seat} is already occupied`, { seat: action.seat });
208
+ throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.SEAT_OCCUPIED, `Seat ${action.seat} is already occupied`, { seat: action.seat });
194
209
  }
195
210
  }
196
211
  /**
package/dist/browser.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * This file provides a browser-safe RNG using Web Crypto API
5
5
  * and re-exports all engine functionality for use in web applications.
6
6
  */
7
- import { PokerEngine } from "./engine/PokerEngine";
7
+ import { PokerEngine } from "./engine/poker-engine";
8
8
  import type { TableConfig } from "@pokertools/types";
9
9
  /**
10
10
  * Browser-compatible RNG using Web Crypto API
@@ -15,13 +15,13 @@ export declare function getBrowserRNG(): () => number;
15
15
  * Create a PokerEngine instance with browser-compatible RNG
16
16
  */
17
17
  export declare function createBrowserEngine(config: TableConfig): PokerEngine;
18
- export * from "./engine/PokerEngine";
18
+ export * from "./engine/poker-engine";
19
19
  export * from "./actions/betting";
20
20
  export * from "./actions/dealing";
21
21
  export * from "./actions/management";
22
- export * from "./actions/showdownActions";
22
+ export * from "./actions/showdown-actions";
23
23
  export * from "./actions/special";
24
- export * from "./utils/viewMasking";
24
+ export * from "./utils/view-masking";
25
25
  export * from "./utils/serialization";
26
- export * from "./utils/cardUtils";
26
+ export * from "./utils/card-utils";
27
27
  export * from "./history/exporter";
package/dist/browser.js CHANGED
@@ -22,13 +22,12 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
22
22
  Object.defineProperty(exports, "__esModule", { value: true });
23
23
  exports.getBrowserRNG = getBrowserRNG;
24
24
  exports.createBrowserEngine = createBrowserEngine;
25
- const PokerEngine_1 = require("./engine/PokerEngine");
25
+ const poker_engine_1 = require("./engine/poker-engine");
26
26
  /**
27
27
  * Browser-compatible RNG using Web Crypto API
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);
@@ -55,19 +54,19 @@ function getBrowserRNG() {
55
54
  * Create a PokerEngine instance with browser-compatible RNG
56
55
  */
57
56
  function createBrowserEngine(config) {
58
- return new PokerEngine_1.PokerEngine({
57
+ return new poker_engine_1.PokerEngine({
59
58
  ...config,
60
59
  randomProvider: getBrowserRNG(),
61
60
  });
62
61
  }
63
62
  // Re-export everything from main engine
64
- __exportStar(require("./engine/PokerEngine"), exports);
63
+ __exportStar(require("./engine/poker-engine"), exports);
65
64
  __exportStar(require("./actions/betting"), exports);
66
65
  __exportStar(require("./actions/dealing"), exports);
67
66
  __exportStar(require("./actions/management"), exports);
68
- __exportStar(require("./actions/showdownActions"), exports);
67
+ __exportStar(require("./actions/showdown-actions"), exports);
69
68
  __exportStar(require("./actions/special"), exports);
70
- __exportStar(require("./utils/viewMasking"), exports);
69
+ __exportStar(require("./utils/view-masking"), exports);
71
70
  __exportStar(require("./utils/serialization"), exports);
72
- __exportStar(require("./utils/cardUtils"), exports);
71
+ __exportStar(require("./utils/card-utils"), exports);
73
72
  __exportStar(require("./history/exporter"), exports);