@pokertools/engine 1.0.0

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 (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +607 -0
  3. package/dist/actions/betting.d.ts +21 -0
  4. package/dist/actions/betting.js +410 -0
  5. package/dist/actions/dealing.d.ts +9 -0
  6. package/dist/actions/dealing.js +206 -0
  7. package/dist/actions/management.d.ts +9 -0
  8. package/dist/actions/management.js +58 -0
  9. package/dist/actions/showdownActions.d.ts +9 -0
  10. package/dist/actions/showdownActions.js +119 -0
  11. package/dist/actions/special.d.ts +14 -0
  12. package/dist/actions/special.js +98 -0
  13. package/dist/actions/streetProgression.d.ts +13 -0
  14. package/dist/actions/streetProgression.js +157 -0
  15. package/dist/actions/tournament.d.ts +5 -0
  16. package/dist/actions/tournament.js +38 -0
  17. package/dist/actions/validation.d.ts +6 -0
  18. package/dist/actions/validation.js +182 -0
  19. package/dist/engine/PokerEngine.d.ts +92 -0
  20. package/dist/engine/PokerEngine.js +246 -0
  21. package/dist/engine/gameReducer.d.ts +10 -0
  22. package/dist/engine/gameReducer.js +135 -0
  23. package/dist/errors/ConfigError.d.ts +8 -0
  24. package/dist/errors/ConfigError.js +15 -0
  25. package/dist/errors/CriticalStateError.d.ts +8 -0
  26. package/dist/errors/CriticalStateError.js +15 -0
  27. package/dist/errors/ErrorCodes.d.ts +38 -0
  28. package/dist/errors/ErrorCodes.js +46 -0
  29. package/dist/errors/IllegalActionError.d.ts +9 -0
  30. package/dist/errors/IllegalActionError.js +15 -0
  31. package/dist/errors/PokerEngineError.d.ts +8 -0
  32. package/dist/errors/PokerEngineError.js +19 -0
  33. package/dist/errors/index.d.ts +5 -0
  34. package/dist/errors/index.js +22 -0
  35. package/dist/history/exporter.d.ts +28 -0
  36. package/dist/history/exporter.js +60 -0
  37. package/dist/history/formats/json.d.ts +14 -0
  38. package/dist/history/formats/json.js +46 -0
  39. package/dist/history/formats/pokerstars.d.ts +10 -0
  40. package/dist/history/formats/pokerstars.js +188 -0
  41. package/dist/history/handHistoryBuilder.d.ts +10 -0
  42. package/dist/history/handHistoryBuilder.js +179 -0
  43. package/dist/history/types.d.ts +73 -0
  44. package/dist/history/types.js +5 -0
  45. package/dist/index.d.ts +8 -0
  46. package/dist/index.js +38 -0
  47. package/dist/rules/actionOrder.d.ts +14 -0
  48. package/dist/rules/actionOrder.js +211 -0
  49. package/dist/rules/blinds.d.ts +24 -0
  50. package/dist/rules/blinds.js +64 -0
  51. package/dist/rules/headsUp.d.ts +15 -0
  52. package/dist/rules/headsUp.js +44 -0
  53. package/dist/rules/showdown.d.ts +9 -0
  54. package/dist/rules/showdown.js +164 -0
  55. package/dist/rules/sidePots.d.ts +32 -0
  56. package/dist/rules/sidePots.js +173 -0
  57. package/dist/tsconfig.tsbuildinfo +1 -0
  58. package/dist/utils/cardUtils.d.ts +12 -0
  59. package/dist/utils/cardUtils.js +30 -0
  60. package/dist/utils/constants.d.ts +38 -0
  61. package/dist/utils/constants.js +41 -0
  62. package/dist/utils/deck.d.ts +46 -0
  63. package/dist/utils/deck.js +126 -0
  64. package/dist/utils/invariants.d.ts +39 -0
  65. package/dist/utils/invariants.js +163 -0
  66. package/dist/utils/positioning.d.ts +36 -0
  67. package/dist/utils/positioning.js +97 -0
  68. package/dist/utils/rake.d.ts +13 -0
  69. package/dist/utils/rake.js +45 -0
  70. package/dist/utils/serialization.d.ts +53 -0
  71. package/dist/utils/serialization.js +106 -0
  72. package/dist/utils/validation.d.ts +20 -0
  73. package/dist/utils/validation.js +52 -0
  74. package/dist/utils/viewMasking.d.ts +20 -0
  75. package/dist/utils/viewMasking.js +90 -0
  76. package/package.json +58 -0
package/dist/index.js ADDED
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
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
+ var PokerEngine_1 = require("./engine/PokerEngine");
20
+ Object.defineProperty(exports, "PokerEngine", { enumerable: true, get: function () { return PokerEngine_1.PokerEngine; } });
21
+ // Types (re-exported from @pokertools/types for convenience)
22
+ __exportStar(require("@pokertools/types"), exports);
23
+ // Errors
24
+ __exportStar(require("./errors"), exports);
25
+ // Utilities (for advanced usage)
26
+ var serialization_1 = require("./utils/serialization");
27
+ Object.defineProperty(exports, "createSnapshot", { enumerable: true, get: function () { return serialization_1.createSnapshot; } });
28
+ Object.defineProperty(exports, "restoreFromSnapshot", { enumerable: true, get: function () { return serialization_1.restoreFromSnapshot; } });
29
+ var viewMasking_1 = require("./utils/viewMasking");
30
+ Object.defineProperty(exports, "createPublicView", { enumerable: true, get: function () { return viewMasking_1.createPublicView; } });
31
+ var invariants_1 = require("./utils/invariants");
32
+ Object.defineProperty(exports, "calculateTotalChips", { enumerable: true, get: function () { return invariants_1.calculateTotalChips; } });
33
+ Object.defineProperty(exports, "auditChipConservation", { enumerable: true, get: function () { return invariants_1.auditChipConservation; } });
34
+ // Hand History
35
+ var exporter_1 = require("./history/exporter");
36
+ Object.defineProperty(exports, "exportHandHistory", { enumerable: true, get: function () { return exporter_1.exportHandHistory; } });
37
+ Object.defineProperty(exports, "getHandHistory", { enumerable: true, get: function () { return exporter_1.getHandHistory; } });
38
+ Object.defineProperty(exports, "exportMultipleHands", { enumerable: true, get: function () { return exporter_1.exportMultipleHands; } });
@@ -0,0 +1,14 @@
1
+ import { GameState } from "@pokertools/types";
2
+ /**
3
+ * Determine the next player to act
4
+ * Returns seat number or null if action is complete
5
+ */
6
+ export declare function getNextToAct(state: GameState): number | null;
7
+ /**
8
+ * Get first player to act for the current street
9
+ */
10
+ export declare function getFirstToAct(state: GameState): number | null;
11
+ /**
12
+ * Check if action is complete (everyone has acted and matched bets or folded/all-in)
13
+ */
14
+ export declare function isActionComplete(state: GameState): boolean;
@@ -0,0 +1,211 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getNextToAct = getNextToAct;
4
+ exports.getFirstToAct = getFirstToAct;
5
+ exports.isActionComplete = isActionComplete;
6
+ const positioning_1 = require("../utils/positioning");
7
+ const headsUp_1 = require("./headsUp");
8
+ const blinds_1 = require("./blinds");
9
+ /**
10
+ * Determine the next player to act
11
+ * Returns seat number or null if action is complete
12
+ */
13
+ function getNextToAct(state) {
14
+ // Special case: heads-up has different rules
15
+ if ((0, headsUp_1.isHeadsUp)(state)) {
16
+ return getNextToActHeadsUp(state);
17
+ }
18
+ return getNextToActNormal(state);
19
+ }
20
+ /**
21
+ * Get next to act in normal (3+ player) game
22
+ */
23
+ function getNextToActNormal(state) {
24
+ if (state.actionTo === null) {
25
+ // Action not started, find first to act
26
+ return getFirstToAct(state);
27
+ }
28
+ const currentBet = getCurrentBet(state);
29
+ let seat = (0, positioning_1.getNextSeat)(state.actionTo, state.maxPlayers);
30
+ const startSeat = state.actionTo;
31
+ let foundActionable = false;
32
+ // Search for next player who can act
33
+ while (seat !== startSeat) {
34
+ const player = state.players[seat];
35
+ // Skip if: no player, folded, all-in, or busted
36
+ if (!player || player.status !== "ACTIVE" /* PlayerStatus.ACTIVE */ || player.stack === 0) {
37
+ seat = (0, positioning_1.getNextSeat)(seat, state.maxPlayers);
38
+ continue;
39
+ }
40
+ // Player can act if:
41
+ // 1. They haven't acted this street yet, OR
42
+ // 2. Current bet is higher than their bet
43
+ const playerBet = state.currentBets.get(seat) ?? 0;
44
+ if (playerBet < currentBet) {
45
+ return seat; // Player needs to respond to bet
46
+ }
47
+ // Player has matched current bet
48
+ // Check if they've already acted
49
+ if (!hasActedThisStreet(state, seat)) {
50
+ return seat; // Player hasn't acted yet
51
+ }
52
+ foundActionable = true;
53
+ seat = (0, positioning_1.getNextSeat)(seat, state.maxPlayers);
54
+ }
55
+ // Full circle - check if everyone has acted and matched bets
56
+ if (foundActionable && isActionComplete(state)) {
57
+ return null; // Action complete
58
+ }
59
+ return null;
60
+ }
61
+ /**
62
+ * Get next to act in heads-up game
63
+ */
64
+ function getNextToActHeadsUp(state) {
65
+ if (state.buttonSeat === null) {
66
+ return null;
67
+ }
68
+ const actionOrder = (0, headsUp_1.getHeadsUpActionOrder)(state, state.street);
69
+ if (actionOrder.length === 0) {
70
+ return null;
71
+ }
72
+ // If action hasn't started, return first player
73
+ if (state.actionTo === null) {
74
+ return actionOrder[0];
75
+ }
76
+ const currentBet = getCurrentBet(state);
77
+ // Check both players
78
+ for (const seat of actionOrder) {
79
+ const player = state.players[seat];
80
+ if (!player || player.status !== "ACTIVE" /* PlayerStatus.ACTIVE */ || player.stack === 0) {
81
+ continue;
82
+ }
83
+ const playerBet = state.currentBets.get(seat) ?? 0;
84
+ if (playerBet < currentBet || !hasActedThisStreet(state, seat)) {
85
+ return seat;
86
+ }
87
+ }
88
+ return null; // Action complete
89
+ }
90
+ /**
91
+ * Get first player to act for the current street
92
+ */
93
+ function getFirstToAct(state) {
94
+ if (state.buttonSeat === null) {
95
+ return null;
96
+ }
97
+ if ((0, headsUp_1.isHeadsUp)(state)) {
98
+ const order = (0, headsUp_1.getHeadsUpActionOrder)(state, state.street);
99
+ if (order.length > 0) {
100
+ return order[0];
101
+ }
102
+ return null;
103
+ }
104
+ if (state.street === "PREFLOP" /* Street.PREFLOP */) {
105
+ // Preflop: First to act is UTG (Left of BB)
106
+ // We use blind positions to find the BB seat
107
+ const blinds = (0, blinds_1.getBlindPositions)(state);
108
+ if (!blinds) {
109
+ // Fallback if no blinds found (shouldn't happen)
110
+ return getNextActionableSeat(state.buttonSeat, state);
111
+ }
112
+ // UTG is the next actionable seat after Big Blind
113
+ return getNextActionableSeat(blinds.bigBlindSeat, state);
114
+ }
115
+ else {
116
+ // Postflop: First to act is left of Button
117
+ // We start scanning immediately after button
118
+ // (Button might be dead/empty, but the position exists)
119
+ return getNextActionableSeat(state.buttonSeat, state);
120
+ }
121
+ }
122
+ /**
123
+ * Find next actionable player starting from the seat AFTER the given startSeat
124
+ */
125
+ function getNextActionableSeat(startSeat, state) {
126
+ let seat = (0, positioning_1.getNextSeat)(startSeat, state.maxPlayers);
127
+ const endSeat = startSeat;
128
+ // Scan full circle
129
+ while (seat !== endSeat) {
130
+ const player = state.players[seat];
131
+ if (player && player.status === "ACTIVE" /* PlayerStatus.ACTIVE */ && player.stack > 0) {
132
+ return seat;
133
+ }
134
+ seat = (0, positioning_1.getNextSeat)(seat, state.maxPlayers);
135
+ }
136
+ return null;
137
+ }
138
+ /**
139
+ * Get current highest bet this street
140
+ */
141
+ function getCurrentBet(state) {
142
+ let maxBet = 0;
143
+ for (const bet of state.currentBets.values()) {
144
+ if (bet > maxBet) {
145
+ maxBet = bet;
146
+ }
147
+ }
148
+ return maxBet;
149
+ }
150
+ /**
151
+ * Check if action is complete (everyone has acted and matched bets or folded/all-in)
152
+ */
153
+ function isActionComplete(state) {
154
+ const currentBet = getCurrentBet(state);
155
+ let activeCount = 0;
156
+ let actedCount = 0;
157
+ for (let seat = 0; seat < state.players.length; seat++) {
158
+ const player = state.players[seat];
159
+ if (!player)
160
+ continue;
161
+ // Count active players who can still act
162
+ if (player.status === "ACTIVE" /* PlayerStatus.ACTIVE */ && player.stack > 0) {
163
+ activeCount++;
164
+ const playerBet = state.currentBets.get(seat) ?? 0;
165
+ // Player has matched bet and acted
166
+ if (playerBet === currentBet && hasActedThisStreet(state, seat)) {
167
+ actedCount++;
168
+ }
169
+ }
170
+ }
171
+ // Action complete if all active players have acted and matched bets
172
+ // OR if there are no active players (all all-in or folded) during an active hand
173
+ if (activeCount === 0) {
174
+ // Only return true if we're in a hand (not pre-deal)
175
+ // Check: Are there all-in players with bets?
176
+ const allInPlayers = state.players.filter((p) => p && p.status === "ALL_IN" /* PlayerStatus.ALL_IN */);
177
+ return allInPlayers.length > 0 && state.currentBets.size > 0;
178
+ }
179
+ return activeCount > 0 && activeCount === actedCount;
180
+ }
181
+ /**
182
+ * Check if a player has acted this street
183
+ * This is tracked by checking if they appear in the action history for this street
184
+ */
185
+ function hasActedThisStreet(state, seat) {
186
+ // Find actions from current street
187
+ const streetStartIndex = findStreetStartIndex(state);
188
+ for (let i = streetStartIndex; i < state.actionHistory.length; i++) {
189
+ if (state.actionHistory[i].seat === seat) {
190
+ return true;
191
+ }
192
+ }
193
+ return false;
194
+ }
195
+ /**
196
+ * Find the index in action history where current street started
197
+ * Counts backwards from the end until we find a different street
198
+ */
199
+ function findStreetStartIndex(state) {
200
+ const currentStreet = state.street;
201
+ // Search backwards through action history
202
+ for (let i = state.actionHistory.length - 1; i >= 0; i--) {
203
+ const record = state.actionHistory[i];
204
+ // If we find an action from a different street, the current street starts after it
205
+ if (record.street && record.street !== currentStreet) {
206
+ return i + 1;
207
+ }
208
+ }
209
+ // If all actions are from current street (or no street recorded), start from beginning
210
+ return 0;
211
+ }
@@ -0,0 +1,24 @@
1
+ import { GameState } from "@pokertools/types";
2
+ /**
3
+ * Result of blind posting calculation
4
+ */
5
+ export interface BlindPositions {
6
+ readonly smallBlindSeat: number;
7
+ readonly bigBlindSeat: number;
8
+ }
9
+ /**
10
+ * Determine which seats should post blinds
11
+ *
12
+ * Heads-Up (2 players):
13
+ * - SB = button (button IS small blind)
14
+ * - BB = other player
15
+ *
16
+ * Normal (Dead Button Rule):
17
+ * - SB = Button + 1 (Can be empty -> Dead Small Blind)
18
+ * - BB = Next Occupied Seat after SB
19
+ */
20
+ export declare function getBlindPositions(state: GameState): BlindPositions | null;
21
+ /**
22
+ * Calculate blind amounts for antes
23
+ */
24
+ export declare function calculateAntes(state: GameState): Map<number, number>;
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getBlindPositions = getBlindPositions;
4
+ exports.calculateAntes = calculateAntes;
5
+ const positioning_1 = require("../utils/positioning");
6
+ const headsUp_1 = require("./headsUp");
7
+ /**
8
+ * Determine which seats should post blinds
9
+ *
10
+ * Heads-Up (2 players):
11
+ * - SB = button (button IS small blind)
12
+ * - BB = other player
13
+ *
14
+ * Normal (Dead Button Rule):
15
+ * - SB = Button + 1 (Can be empty -> Dead Small Blind)
16
+ * - BB = Next Occupied Seat after SB
17
+ */
18
+ function getBlindPositions(state) {
19
+ if (state.buttonSeat === null) {
20
+ return null;
21
+ }
22
+ const buttonSeat = state.buttonSeat;
23
+ // Heads-up specific logic (Button is SB)
24
+ if ((0, headsUp_1.isHeadsUp)(state)) {
25
+ const bbSeat = (0, positioning_1.getNextOccupiedSeat)(buttonSeat, state.players, state.maxPlayers);
26
+ if (bbSeat === null) {
27
+ return null;
28
+ }
29
+ return {
30
+ smallBlindSeat: buttonSeat,
31
+ bigBlindSeat: bbSeat,
32
+ };
33
+ }
34
+ // Normal Play (Dead Button / Dead Small Blind Logic)
35
+ // 1. SB is ALWAYS the immediate next seat, even if empty
36
+ const sbSeat = (0, positioning_1.getNextSeat)(buttonSeat, state.maxPlayers);
37
+ // 2. BB is the next ACTIVE/OCCUPIED player after the SB position
38
+ const bbSeat = (0, positioning_1.getNextOccupiedSeat)(sbSeat, state.players, state.maxPlayers);
39
+ if (bbSeat === null) {
40
+ return null;
41
+ }
42
+ return {
43
+ smallBlindSeat: sbSeat,
44
+ bigBlindSeat: bbSeat,
45
+ };
46
+ }
47
+ /**
48
+ * Calculate blind amounts for antes
49
+ */
50
+ function calculateAntes(state) {
51
+ const antes = new Map();
52
+ if (state.ante === 0) {
53
+ return antes;
54
+ }
55
+ // All active players post antes
56
+ for (let seat = 0; seat < state.players.length; seat++) {
57
+ const player = state.players[seat];
58
+ if (player && player.stack > 0) {
59
+ const anteAmount = Math.min(player.stack, state.ante);
60
+ antes.set(seat, anteAmount);
61
+ }
62
+ }
63
+ return antes;
64
+ }
@@ -0,0 +1,15 @@
1
+ import { GameState, Street } from "@pokertools/types";
2
+ /**
3
+ * Determine if the game is heads-up (exactly 2 seated players)
4
+ * This checks seated players, not just active in current hand,
5
+ * because heads-up rules apply to the table structure, not hand state
6
+ */
7
+ export declare function isHeadsUp(state: GameState): boolean;
8
+ /**
9
+ * Get heads-up action order for a given street
10
+ * In heads-up:
11
+ * - Button IS small blind
12
+ * - Button acts FIRST preflop
13
+ * - Button acts LAST postflop
14
+ */
15
+ export declare function getHeadsUpActionOrder(state: GameState, street: Street): number[];
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isHeadsUp = isHeadsUp;
4
+ exports.getHeadsUpActionOrder = getHeadsUpActionOrder;
5
+ /**
6
+ * Determine if the game is heads-up (exactly 2 seated players)
7
+ * This checks seated players, not just active in current hand,
8
+ * because heads-up rules apply to the table structure, not hand state
9
+ */
10
+ function isHeadsUp(state) {
11
+ const seatedPlayers = state.players.filter((p) => p !== null);
12
+ return seatedPlayers.length === 2;
13
+ }
14
+ /**
15
+ * Get heads-up action order for a given street
16
+ * In heads-up:
17
+ * - Button IS small blind
18
+ * - Button acts FIRST preflop
19
+ * - Button acts LAST postflop
20
+ */
21
+ function getHeadsUpActionOrder(state, street) {
22
+ if (state.buttonSeat === null) {
23
+ return [];
24
+ }
25
+ const buttonSeat = state.buttonSeat;
26
+ const activePlayers = state.players
27
+ .map((p, seat) => ({ player: p, seat }))
28
+ .filter(({ player }) => player && (player.status === "ACTIVE" /* PlayerStatus.ACTIVE */ || player.status === "ALL_IN" /* PlayerStatus.ALL_IN */))
29
+ .map(({ seat }) => seat);
30
+ if (activePlayers.length !== 2) {
31
+ return activePlayers;
32
+ }
33
+ // Find the two seats
34
+ const [seat1, seat2] = activePlayers.sort((a, b) => a - b);
35
+ const otherSeat = seat1 === buttonSeat ? seat2 : seat1;
36
+ if (street === "PREFLOP" /* Street.PREFLOP */) {
37
+ // Button acts first preflop
38
+ return [buttonSeat, otherSeat];
39
+ }
40
+ else {
41
+ // Button acts last postflop (other player first)
42
+ return [otherSeat, buttonSeat];
43
+ }
44
+ }
@@ -0,0 +1,9 @@
1
+ import { GameState } from "@pokertools/types";
2
+ /**
3
+ * Determine winners and distribute pots
4
+ */
5
+ export declare function determineWinners(state: GameState): GameState;
6
+ /**
7
+ * Check if hand should go to showdown
8
+ */
9
+ export declare function shouldShowdown(state: GameState): boolean;
@@ -0,0 +1,164 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.determineWinners = determineWinners;
4
+ exports.shouldShowdown = shouldShowdown;
5
+ const evaluator_1 = require("@pokertools/evaluator");
6
+ const positioning_1 = require("../utils/positioning");
7
+ const rake_1 = require("../utils/rake");
8
+ /**
9
+ * Determine winners and distribute pots
10
+ */
11
+ function determineWinners(state) {
12
+ const winners = [];
13
+ const newPlayers = [...state.players];
14
+ let totalRake = 0;
15
+ const winnerSeats = new Set();
16
+ // Process each pot (side pots first, then main)
17
+ const sortedPots = [...state.pots].sort((a, b) => {
18
+ if (a.type === "SIDE" && b.type === "MAIN")
19
+ return -1;
20
+ if (a.type === "MAIN" && b.type === "SIDE")
21
+ return 1;
22
+ return 0;
23
+ });
24
+ for (const pot of sortedPots) {
25
+ const potWinners = evaluatePot(state, pot);
26
+ // Calculate and deduct rake (cash games only)
27
+ // Apply GLOBAL rake cap across all pots (per-hand, not per-pot)
28
+ const { rake } = (0, rake_1.calculateRake)(state, pot.amount, totalRake);
29
+ totalRake += rake;
30
+ const potAfterRake = pot.amount - rake;
31
+ // Distribute pot among winners
32
+ const share = Math.floor(potAfterRake / potWinners.length);
33
+ const remainder = potAfterRake % potWinners.length;
34
+ // Sort winners by position (worst to best) for odd chip distribution
35
+ // TDA Rule: Odd chips go to first player(s) clockwise from button
36
+ const sortedWinners = [...potWinners].sort((a, b) => {
37
+ if (state.buttonSeat === null)
38
+ return 0;
39
+ const distA = (0, positioning_1.getDistanceFromButton)(a.seat, state.buttonSeat, state.maxPlayers);
40
+ const distB = (0, positioning_1.getDistanceFromButton)(b.seat, state.buttonSeat, state.maxPlayers);
41
+ return distA - distB;
42
+ });
43
+ // Distribute chips to all winners
44
+ for (let i = 0; i < sortedWinners.length; i++) {
45
+ const evaluation = sortedWinners[i];
46
+ let award = share;
47
+ // Distribute odd chips one at a time to worst positions
48
+ // (first N winners in sorted order get the extra chips)
49
+ if (i < remainder) {
50
+ award += 1;
51
+ }
52
+ // Track winner seats
53
+ winnerSeats.add(evaluation.seat);
54
+ // Award chips to player
55
+ const player = newPlayers[evaluation.seat];
56
+ newPlayers[evaluation.seat] = {
57
+ ...player,
58
+ stack: player.stack + award,
59
+ };
60
+ // Record winner
61
+ winners.push({
62
+ seat: evaluation.seat,
63
+ amount: award,
64
+ hand: evaluation.hand,
65
+ handRank: evaluation.description,
66
+ });
67
+ }
68
+ }
69
+ // Set shown cards for winners and losers
70
+ for (let seat = 0; seat < newPlayers.length; seat++) {
71
+ const player = newPlayers[seat];
72
+ if (player && player.hand !== null) {
73
+ if (winnerSeats.has(seat)) {
74
+ // Winners must show all cards
75
+ newPlayers[seat] = {
76
+ ...player,
77
+ shownCards: [0, 1], // Show both cards
78
+ };
79
+ }
80
+ else {
81
+ // Losers are mucked by default (can be changed via SHOW action)
82
+ newPlayers[seat] = {
83
+ ...player,
84
+ shownCards: null, // Mucked - hand preserved but not shown
85
+ };
86
+ }
87
+ }
88
+ }
89
+ // NOTE: We do NOT reset totalInvestedThisHand here because it's used by getInitialChips()
90
+ // to calculate total chips in the game. It will be reset when a new hand is dealt.
91
+ return {
92
+ ...state,
93
+ players: newPlayers,
94
+ winners,
95
+ rakeThisHand: totalRake,
96
+ pots: [], // Pots distributed
97
+ actionTo: null,
98
+ };
99
+ }
100
+ /**
101
+ * Evaluate a single pot and return winner(s)
102
+ */
103
+ function evaluatePot(state, pot) {
104
+ // Get eligible players (not folded)
105
+ const eligible = pot.eligibleSeats
106
+ .map((seat) => state.players[seat])
107
+ .filter((player) => player && (player.status === "ACTIVE" /* PlayerStatus.ACTIVE */ || player.status === "ALL_IN" /* PlayerStatus.ALL_IN */));
108
+ // If only one player, they win without showing
109
+ if (eligible.length === 1) {
110
+ const player = eligible[0];
111
+ return [
112
+ {
113
+ seat: player.seat,
114
+ score: 0,
115
+ hand: [],
116
+ description: "Uncontested",
117
+ },
118
+ ];
119
+ }
120
+ // Evaluate all hands
121
+ const evaluations = [];
122
+ for (const player of eligible) {
123
+ if (!player?.hand)
124
+ continue;
125
+ // Combine hole cards + board (7 cards total for river)
126
+ const allCards = [...player.hand, ...state.board];
127
+ if (allCards.length < 5) {
128
+ // Not enough cards (shouldn't happen)
129
+ continue;
130
+ }
131
+ // Evaluate using @pokertools/evaluator
132
+ const cardCodes = (0, evaluator_1.getCardCodes)(allCards);
133
+ const score = (0, evaluator_1.evaluate)(cardCodes);
134
+ const handRank = (0, evaluator_1.rank)(cardCodes);
135
+ const description = (0, evaluator_1.rankDescription)(handRank);
136
+ evaluations.push({
137
+ seat: player.seat,
138
+ score,
139
+ hand: [...player.hand], // Store hole cards (copy to mutable array)
140
+ description,
141
+ });
142
+ }
143
+ // Find best hand(s)
144
+ const bestScore = Math.min(...evaluations.map((e) => e.score));
145
+ const winners = evaluations.filter((e) => e.score === bestScore);
146
+ return winners;
147
+ }
148
+ /**
149
+ * Check if hand should go to showdown
150
+ */
151
+ function shouldShowdown(state) {
152
+ // Showdown if:
153
+ // 1. We're at SHOWDOWN street
154
+ // 2. Multiple players remain (not folded)
155
+ // 3. Winners haven't been determined yet
156
+ if (state.street !== "SHOWDOWN" /* Street.SHOWDOWN */) {
157
+ return false;
158
+ }
159
+ if (state.winners !== null) {
160
+ return false; // Already determined winners
161
+ }
162
+ const activePlayers = state.players.filter((p) => p && (p.status === "ACTIVE" /* PlayerStatus.ACTIVE */ || p.status === "ALL_IN" /* PlayerStatus.ALL_IN */));
163
+ return activePlayers.length >= 2;
164
+ }
@@ -0,0 +1,32 @@
1
+ import { GameState, Pot } from "@pokertools/types";
2
+ /**
3
+ * Calculate side pots using iterative subtraction method
4
+ *
5
+ * Algorithm:
6
+ * 1. Combine all player investments (bets + total invested)
7
+ * 2. Sort by investment amount (ascending)
8
+ * 3. For each player from smallest to largest:
9
+ * - Create pot = (player investment - previous) × remaining players
10
+ * - Add player + all higher investors to eligible list
11
+ * 4. Return pots array (main + sides)
12
+ *
13
+ * @param state Current game state
14
+ * @returns Array of pots (main pot first, then side pots)
15
+ */
16
+ export declare function calculateSidePots(state: GameState): Pot[];
17
+ /**
18
+ * Calculate uncalled bet (when highest better has no callers)
19
+ *
20
+ * @param state Current game state
21
+ * @returns Tuple of [uncalled amount, seat to return to]
22
+ */
23
+ export declare function calculateUncalledBet(state: GameState): [number, number] | null;
24
+ /**
25
+ * Return uncalled bet to player
26
+ */
27
+ export declare function returnUncalledBet(state: GameState): GameState;
28
+ /**
29
+ * Recalculate pots after street action completes
30
+ * This is called before progressing to next street
31
+ */
32
+ export declare function recalculatePots(state: GameState): GameState;