@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
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.calculateSidePots = calculateSidePots;
4
+ exports.calculateUncalledBet = calculateUncalledBet;
5
+ exports.returnUncalledBet = returnUncalledBet;
6
+ exports.recalculatePots = recalculatePots;
7
+ const CriticalStateError_1 = require("../errors/CriticalStateError");
8
+ /**
9
+ * Calculate side pots using iterative subtraction method
10
+ *
11
+ * Algorithm:
12
+ * 1. Combine all player investments (bets + total invested)
13
+ * 2. Sort by investment amount (ascending)
14
+ * 3. For each player from smallest to largest:
15
+ * - Create pot = (player investment - previous) × remaining players
16
+ * - Add player + all higher investors to eligible list
17
+ * 4. Return pots array (main + sides)
18
+ *
19
+ * @param state Current game state
20
+ * @returns Array of pots (main pot first, then side pots)
21
+ */
22
+ function calculateSidePots(state) {
23
+ // Collect all investments (including folded players - their chips stay in the pot)
24
+ const investments = [];
25
+ for (let seat = 0; seat < state.players.length; seat++) {
26
+ const player = state.players[seat];
27
+ if (!player)
28
+ continue;
29
+ // totalInvestedThisHand already includes all bets (current and previous streets)
30
+ const totalInvestment = player.totalInvestedThisHand;
31
+ if (totalInvestment > 0) {
32
+ investments.push({
33
+ seat,
34
+ amount: totalInvestment,
35
+ folded: player.status === "FOLDED" /* PlayerStatus.FOLDED */,
36
+ });
37
+ }
38
+ }
39
+ // If no investments, return empty
40
+ if (investments.length === 0) {
41
+ return [];
42
+ }
43
+ // Sort by investment (ascending)
44
+ investments.sort((a, b) => a.amount - b.amount);
45
+ const pots = [];
46
+ let prevAmount = 0;
47
+ for (let i = 0; i < investments.length; i++) {
48
+ const current = investments[i];
49
+ const allAtThisLevel = investments.slice(i); // Current + all higher investors
50
+ const increment = current.amount - prevAmount;
51
+ // Create pot for this level
52
+ if (increment > 0) {
53
+ // Pot includes chips from ALL players at this level (including folded)
54
+ const potAmount = increment * allAtThisLevel.length;
55
+ // But only non-folded players are eligible to win
56
+ const eligibleSeats = allAtThisLevel.filter((inv) => !inv.folded).map((inv) => inv.seat);
57
+ // Must have at least one eligible player
58
+ // If everyone folded at this level, something went wrong in the game logic
59
+ if (eligibleSeats.length === 0) {
60
+ throw new CriticalStateError_1.CriticalStateError(`Side pot has no eligible players - all ${allAtThisLevel.length} players at this level have folded`, {
61
+ potAmount,
62
+ potLevel: i,
63
+ investmentLevel: current.amount,
64
+ allInvestors: allAtThisLevel.map((inv) => ({
65
+ seat: inv.seat,
66
+ amount: inv.amount,
67
+ folded: inv.folded,
68
+ })),
69
+ });
70
+ }
71
+ pots.push({
72
+ amount: potAmount,
73
+ eligibleSeats,
74
+ type: i === 0 ? "MAIN" : "SIDE",
75
+ capPerPlayer: current.amount,
76
+ });
77
+ }
78
+ prevAmount = current.amount;
79
+ }
80
+ return pots;
81
+ }
82
+ /**
83
+ * Calculate uncalled bet (when highest better has no callers)
84
+ *
85
+ * @param state Current game state
86
+ * @returns Tuple of [uncalled amount, seat to return to]
87
+ */
88
+ function calculateUncalledBet(state) {
89
+ if (state.currentBets.size === 0) {
90
+ return null;
91
+ }
92
+ // Find highest bet
93
+ let maxBet = 0;
94
+ let maxBetSeat = -1;
95
+ let secondMaxBet = 0;
96
+ for (const [seat, bet] of state.currentBets.entries()) {
97
+ if (bet > maxBet) {
98
+ secondMaxBet = maxBet;
99
+ maxBet = bet;
100
+ maxBetSeat = seat;
101
+ }
102
+ else if (bet > secondMaxBet) {
103
+ secondMaxBet = bet;
104
+ }
105
+ }
106
+ const uncalled = maxBet - secondMaxBet;
107
+ if (uncalled > 0 && maxBetSeat >= 0) {
108
+ return [uncalled, maxBetSeat];
109
+ }
110
+ return null;
111
+ }
112
+ /**
113
+ * Return uncalled bet to player
114
+ */
115
+ function returnUncalledBet(state) {
116
+ const uncalled = calculateUncalledBet(state);
117
+ if (!uncalled) {
118
+ return state;
119
+ }
120
+ const [amount, seat] = uncalled;
121
+ const player = state.players[seat];
122
+ if (!player) {
123
+ return state;
124
+ }
125
+ // Return chips to player
126
+ const newPlayers = [...state.players];
127
+ newPlayers[seat] = {
128
+ ...player,
129
+ stack: player.stack + amount,
130
+ totalInvestedThisHand: player.totalInvestedThisHand - amount,
131
+ };
132
+ // Reduce current bet
133
+ const newCurrentBets = new Map(state.currentBets);
134
+ const currentBet = newCurrentBets.get(seat) ?? 0;
135
+ newCurrentBets.set(seat, currentBet - amount);
136
+ // Record to action history
137
+ const actionRecord = {
138
+ action: {
139
+ type: "UNCALLED_BET_RETURNED" /* ActionType.UNCALLED_BET_RETURNED */,
140
+ playerId: player.id,
141
+ amount,
142
+ timestamp: state.timestamp,
143
+ },
144
+ seat,
145
+ resultingPot: state.pots.reduce((sum, pot) => sum + pot.amount, 0),
146
+ resultingStack: player.stack + amount,
147
+ street: state.street,
148
+ };
149
+ return {
150
+ ...state,
151
+ players: newPlayers,
152
+ currentBets: newCurrentBets,
153
+ actionHistory: [...state.actionHistory, actionRecord],
154
+ };
155
+ }
156
+ /**
157
+ * Recalculate pots after street action completes
158
+ * This is called before progressing to next street
159
+ */
160
+ function recalculatePots(state) {
161
+ // First, return any uncalled bet
162
+ const newState = returnUncalledBet(state);
163
+ // Calculate side pots based on all investments
164
+ const pots = calculateSidePots(newState);
165
+ // Reset betThisStreet for all players (bets collected into pots)
166
+ const newPlayers = newState.players.map((p) => (p ? { ...p, betThisStreet: 0 } : null));
167
+ return {
168
+ ...newState,
169
+ players: newPlayers,
170
+ pots,
171
+ currentBets: new Map(), // Bets collected into pots
172
+ };
173
+ }
@@ -0,0 +1 @@
1
+ {"root":["../src/index.ts","../src/actions/betting.ts","../src/actions/dealing.ts","../src/actions/management.ts","../src/actions/showdownActions.ts","../src/actions/special.ts","../src/actions/streetProgression.ts","../src/actions/tournament.ts","../src/actions/validation.ts","../src/engine/PokerEngine.ts","../src/engine/gameReducer.ts","../src/errors/ConfigError.ts","../src/errors/CriticalStateError.ts","../src/errors/ErrorCodes.ts","../src/errors/IllegalActionError.ts","../src/errors/PokerEngineError.ts","../src/errors/index.ts","../src/history/exporter.ts","../src/history/handHistoryBuilder.ts","../src/history/types.ts","../src/history/formats/json.ts","../src/history/formats/pokerstars.ts","../src/rules/actionOrder.ts","../src/rules/blinds.ts","../src/rules/headsUp.ts","../src/rules/showdown.ts","../src/rules/sidePots.ts","../src/utils/cardUtils.ts","../src/utils/constants.ts","../src/utils/deck.ts","../src/utils/invariants.ts","../src/utils/positioning.ts","../src/utils/rake.ts","../src/utils/serialization.ts","../src/utils/validation.ts","../src/utils/viewMasking.ts"],"version":"5.9.3"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Convert integer card codes to string array
3
+ */
4
+ export declare function cardCodesToStrings(codes: readonly number[]): string[];
5
+ /**
6
+ * Convert string card array to integer codes
7
+ */
8
+ export declare function cardStringsToCards(cards: readonly string[]): number[];
9
+ /**
10
+ * Validate card string format
11
+ */
12
+ export declare function isValidCard(card: string): boolean;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cardCodesToStrings = cardCodesToStrings;
4
+ exports.cardStringsToCards = cardStringsToCards;
5
+ exports.isValidCard = isValidCard;
6
+ const evaluator_1 = require("@pokertools/evaluator");
7
+ /**
8
+ * Convert integer card codes to string array
9
+ */
10
+ function cardCodesToStrings(codes) {
11
+ return codes.map((code) => (0, evaluator_1.stringifyCardCode)(code));
12
+ }
13
+ /**
14
+ * Convert string card array to integer codes
15
+ */
16
+ function cardStringsToCards(cards) {
17
+ return cards.map((card) => (0, evaluator_1.getCardCode)(card));
18
+ }
19
+ /**
20
+ * Validate card string format
21
+ */
22
+ function isValidCard(card) {
23
+ if (card.length !== 2)
24
+ return false;
25
+ const rank = card[0];
26
+ const suit = card[1];
27
+ const validRanks = ["2", "3", "4", "5", "6", "7", "8", "9", "T", "J", "Q", "K", "A"];
28
+ const validSuits = ["s", "h", "d", "c"];
29
+ return validRanks.includes(rank) && validSuits.includes(suit);
30
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Game constants and configuration values
3
+ * Centralizes magic numbers for maintainability
4
+ */
5
+ /**
6
+ * Maximum number of previous states to keep for undo functionality
7
+ */
8
+ export declare const MAX_UNDO_HISTORY = 50;
9
+ /**
10
+ * Percentage divisor for converting percentages to decimal
11
+ * e.g., 5% = 5 / PERCENTAGE_DIVISOR = 0.05
12
+ */
13
+ export declare const PERCENTAGE_DIVISOR = 100;
14
+ /**
15
+ * Number of cards to burn before dealing community cards on each street
16
+ */
17
+ export declare const BURN_CARDS_PER_STREET = 1;
18
+ /**
19
+ * Number of cards to deal on the flop
20
+ */
21
+ export declare const FLOP_CARD_COUNT = 3;
22
+ /**
23
+ * Number of cards to deal on the turn
24
+ */
25
+ export declare const TURN_CARD_COUNT = 1;
26
+ /**
27
+ * Number of cards to deal on the river
28
+ */
29
+ export declare const RIVER_CARD_COUNT = 1;
30
+ /**
31
+ * Number of hole cards dealt to each player in Texas Hold'em
32
+ */
33
+ export declare const HOLE_CARDS_PER_PLAYER = 2;
34
+ /**
35
+ * Maximum clock drift tolerance for timestamp validation (milliseconds)
36
+ * Allows for small differences in server clocks
37
+ */
38
+ export declare const TIMESTAMP_FUTURE_TOLERANCE_MS = 1000;
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ /**
3
+ * Game constants and configuration values
4
+ * Centralizes magic numbers for maintainability
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.TIMESTAMP_FUTURE_TOLERANCE_MS = exports.HOLE_CARDS_PER_PLAYER = exports.RIVER_CARD_COUNT = exports.TURN_CARD_COUNT = exports.FLOP_CARD_COUNT = exports.BURN_CARDS_PER_STREET = exports.PERCENTAGE_DIVISOR = exports.MAX_UNDO_HISTORY = void 0;
8
+ /**
9
+ * Maximum number of previous states to keep for undo functionality
10
+ */
11
+ exports.MAX_UNDO_HISTORY = 50;
12
+ /**
13
+ * Percentage divisor for converting percentages to decimal
14
+ * e.g., 5% = 5 / PERCENTAGE_DIVISOR = 0.05
15
+ */
16
+ exports.PERCENTAGE_DIVISOR = 100;
17
+ /**
18
+ * Number of cards to burn before dealing community cards on each street
19
+ */
20
+ exports.BURN_CARDS_PER_STREET = 1;
21
+ /**
22
+ * Number of cards to deal on the flop
23
+ */
24
+ exports.FLOP_CARD_COUNT = 3;
25
+ /**
26
+ * Number of cards to deal on the turn
27
+ */
28
+ exports.TURN_CARD_COUNT = 1;
29
+ /**
30
+ * Number of cards to deal on the river
31
+ */
32
+ exports.RIVER_CARD_COUNT = 1;
33
+ /**
34
+ * Number of hole cards dealt to each player in Texas Hold'em
35
+ */
36
+ exports.HOLE_CARDS_PER_PLAYER = 2;
37
+ /**
38
+ * Maximum clock drift tolerance for timestamp validation (milliseconds)
39
+ * Allows for small differences in server clocks
40
+ */
41
+ exports.TIMESTAMP_FUTURE_TOLERANCE_MS = 1000;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Create a standard 52-card deck
3
+ * Returns array of integer card codes (0-51)
4
+ */
5
+ export declare function createDeck(): number[];
6
+ /**
7
+ * Shuffle deck using Fisher-Yates algorithm with injectable RNG
8
+ *
9
+ * @param deck - Deck to shuffle (not modified, returns new array)
10
+ * @param rng - Random number generator function (0-1). MUST be cryptographically
11
+ * secure for production use (e.g., use crypto.randomBytes)
12
+ * @returns New shuffled deck
13
+ *
14
+ * @security For production poker games, ALWAYS provide a cryptographically secure RNG.
15
+ * The default RNG uses Node.js crypto module if available, otherwise falls back to
16
+ * Math.random() which is NOT suitable for real-money games as it can be predicted.
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * // Production: Use crypto for secure shuffling
21
+ * import { randomBytes } from 'crypto';
22
+ * const secureRng = () => randomBytes(4).readUInt32BE(0) / 0x100000000;
23
+ * const deck = shuffle(createDeck(), secureRng);
24
+ *
25
+ * // Development/Testing: Default is acceptable
26
+ * const deck = shuffle(createDeck()); // Uses crypto if available
27
+ * ```
28
+ */
29
+ export declare function shuffle(deck: readonly number[], rng?: () => number): number[];
30
+ /**
31
+ * Deal cards from deck
32
+ *
33
+ * @param deck - Deck to deal from
34
+ * @param count - Number of cards to deal
35
+ * @returns Tuple of [dealt cards, remaining deck]
36
+ */
37
+ export declare function dealCards(deck: readonly number[], count: number): [cards: number[], remaining: number[]];
38
+ /**
39
+ * Burn one card and deal specified number
40
+ * (Standard poker procedure)
41
+ *
42
+ * @param deck - Deck to deal from
43
+ * @param count - Number of cards to deal after burn
44
+ * @returns Tuple of [dealt cards, remaining deck]
45
+ */
46
+ export declare function burnAndDeal(deck: readonly number[], count: number): [cards: number[], remaining: number[]];
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createDeck = createDeck;
4
+ exports.shuffle = shuffle;
5
+ exports.dealCards = dealCards;
6
+ exports.burnAndDeal = burnAndDeal;
7
+ /**
8
+ * Create a standard 52-card deck
9
+ * Returns array of integer card codes (0-51)
10
+ */
11
+ function createDeck() {
12
+ const deck = [];
13
+ // For each rank (0-12: 2 through A)
14
+ for (let rank = 0; rank < 13; rank++) {
15
+ // For each suit (0-3: spades, hearts, diamonds, clubs)
16
+ for (let suit = 0; suit < 4; suit++) {
17
+ // Card code = (rank << 2) | suit
18
+ deck.push((rank << 2) | suit);
19
+ }
20
+ }
21
+ return deck;
22
+ }
23
+ /**
24
+ * Cryptographically secure RNG using Node.js crypto module
25
+ * Falls back to Math.random() only in browser/test environments
26
+ *
27
+ * @warning Math.random() is NOT cryptographically secure and should NEVER
28
+ * be used for production poker games. Always provide a secure RNG.
29
+ */
30
+ function getSecureRandom() {
31
+ // Check if we're in Node.js environment
32
+ if (typeof process !== "undefined" && process.versions?.node) {
33
+ try {
34
+ // Use Node.js crypto for production
35
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
36
+ const crypto = require("crypto");
37
+ return () => {
38
+ // Generate cryptographically secure random number [0, 1)
39
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
40
+ const buffer = crypto.randomBytes(4);
41
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
42
+ const value = buffer.readUInt32BE(0);
43
+ return value / 0x100000000;
44
+ };
45
+ }
46
+ catch (_error) {
47
+ console.warn("[SECURITY WARNING] crypto module not available. Falling back to Math.random(). " +
48
+ "DO NOT use in production for real-money games!");
49
+ }
50
+ }
51
+ // Fallback for browser/test environments - emit warning
52
+ if (process.env.NODE_ENV === "production") {
53
+ console.error("[CRITICAL SECURITY WARNING] Using Math.random() in production! " +
54
+ "This is NOT cryptographically secure and games can be predicted. " +
55
+ "Provide a secure RNG via the rng parameter.");
56
+ }
57
+ return Math.random;
58
+ }
59
+ /**
60
+ * Shuffle deck using Fisher-Yates algorithm with injectable RNG
61
+ *
62
+ * @param deck - Deck to shuffle (not modified, returns new array)
63
+ * @param rng - Random number generator function (0-1). MUST be cryptographically
64
+ * secure for production use (e.g., use crypto.randomBytes)
65
+ * @returns New shuffled deck
66
+ *
67
+ * @security For production poker games, ALWAYS provide a cryptographically secure RNG.
68
+ * The default RNG uses Node.js crypto module if available, otherwise falls back to
69
+ * Math.random() which is NOT suitable for real-money games as it can be predicted.
70
+ *
71
+ * @example
72
+ * ```typescript
73
+ * // Production: Use crypto for secure shuffling
74
+ * import { randomBytes } from 'crypto';
75
+ * const secureRng = () => randomBytes(4).readUInt32BE(0) / 0x100000000;
76
+ * const deck = shuffle(createDeck(), secureRng);
77
+ *
78
+ * // Development/Testing: Default is acceptable
79
+ * const deck = shuffle(createDeck()); // Uses crypto if available
80
+ * ```
81
+ */
82
+ function shuffle(deck, rng) {
83
+ const random = rng ?? getSecureRandom();
84
+ const shuffled = [...deck]; // Create mutable copy
85
+ // Fisher-Yates shuffle
86
+ for (let i = shuffled.length - 1; i > 0; i--) {
87
+ const j = Math.floor(random() * (i + 1));
88
+ // Swap elements
89
+ const temp = shuffled[i];
90
+ shuffled[i] = shuffled[j];
91
+ shuffled[j] = temp;
92
+ }
93
+ return shuffled;
94
+ }
95
+ /**
96
+ * Deal cards from deck
97
+ *
98
+ * @param deck - Deck to deal from
99
+ * @param count - Number of cards to deal
100
+ * @returns Tuple of [dealt cards, remaining deck]
101
+ */
102
+ function dealCards(deck, count) {
103
+ if (count > deck.length) {
104
+ throw new Error(`Cannot deal ${count} cards from deck of ${deck.length}`);
105
+ }
106
+ const cards = deck.slice(0, count);
107
+ const remaining = deck.slice(count);
108
+ return [Array.from(cards), Array.from(remaining)];
109
+ }
110
+ /**
111
+ * Burn one card and deal specified number
112
+ * (Standard poker procedure)
113
+ *
114
+ * @param deck - Deck to deal from
115
+ * @param count - Number of cards to deal after burn
116
+ * @returns Tuple of [dealt cards, remaining deck]
117
+ */
118
+ function burnAndDeal(deck, count) {
119
+ if (count + 1 > deck.length) {
120
+ throw new Error(`Cannot burn and deal ${count} cards from deck of ${deck.length}`);
121
+ }
122
+ // Skip first card (burn), deal next 'count' cards
123
+ const cards = deck.slice(1, count + 1);
124
+ const remaining = deck.slice(count + 1);
125
+ return [Array.from(cards), Array.from(remaining)];
126
+ }
@@ -0,0 +1,39 @@
1
+ import { GameState } from "@pokertools/types";
2
+ /**
3
+ * Audit chip conservation
4
+ * Formula: ∑(player.stack) + ∑(pot.amount) + ∑(currentBets) = initialChips
5
+ *
6
+ * @param state Current game state
7
+ * @param initialChips Total chips that should be in the game
8
+ * @throws CriticalStateError if chips don't match
9
+ */
10
+ export declare function auditChipConservation(state: GameState, initialChips: number): void;
11
+ /**
12
+ * Calculate total chips in the game
13
+ */
14
+ export declare function calculateTotalChips(state: GameState): number;
15
+ /**
16
+ * Calculate total chips in player stacks
17
+ */
18
+ export declare function calculateStackTotal(state: GameState): number;
19
+ /**
20
+ * Calculate total chips in pots
21
+ */
22
+ export declare function calculatePotTotal(state: GameState): number;
23
+ /**
24
+ * Calculate total chips in current bets
25
+ */
26
+ export declare function calculateBetTotal(state: GameState): number;
27
+ /**
28
+ * Get initial chips (sum of all starting stacks)
29
+ * This calculates total chips in the game, which should remain constant (minus rake).
30
+ *
31
+ * - During a hand: stack + totalInvestedThisHand (chips in play + chips invested)
32
+ * - After hand complete: stack + rake (all chips have been distributed, rake removed)
33
+ */
34
+ export declare function getInitialChips(state: GameState): number;
35
+ /**
36
+ * Validate game state integrity
37
+ * Checks multiple invariants beyond just chip conservation
38
+ */
39
+ export declare function validateGameStateIntegrity(state: GameState): void;
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.auditChipConservation = auditChipConservation;
4
+ exports.calculateTotalChips = calculateTotalChips;
5
+ exports.calculateStackTotal = calculateStackTotal;
6
+ exports.calculatePotTotal = calculatePotTotal;
7
+ exports.calculateBetTotal = calculateBetTotal;
8
+ exports.getInitialChips = getInitialChips;
9
+ exports.validateGameStateIntegrity = validateGameStateIntegrity;
10
+ const CriticalStateError_1 = require("../errors/CriticalStateError");
11
+ /**
12
+ * Audit chip conservation
13
+ * Formula: ∑(player.stack) + ∑(pot.amount) + ∑(currentBets) = initialChips
14
+ *
15
+ * @param state Current game state
16
+ * @param initialChips Total chips that should be in the game
17
+ * @throws CriticalStateError if chips don't match
18
+ */
19
+ function auditChipConservation(state, initialChips) {
20
+ const currentChips = getInitialChips(state);
21
+ if (currentChips !== initialChips) {
22
+ throw new CriticalStateError_1.CriticalStateError(`Chip conservation violated: expected ${initialChips}, found ${currentChips}`, {
23
+ expected: initialChips,
24
+ actual: currentChips,
25
+ difference: currentChips - initialChips,
26
+ stacks: calculateStackTotal(state),
27
+ pots: calculatePotTotal(state),
28
+ bets: calculateBetTotal(state),
29
+ street: state.street,
30
+ handId: state.handId,
31
+ });
32
+ }
33
+ }
34
+ /**
35
+ * Calculate total chips in the game
36
+ */
37
+ function calculateTotalChips(state) {
38
+ return calculateStackTotal(state) + calculatePotTotal(state) + calculateBetTotal(state);
39
+ }
40
+ /**
41
+ * Calculate total chips in player stacks
42
+ */
43
+ function calculateStackTotal(state) {
44
+ let total = 0;
45
+ for (const player of state.players) {
46
+ if (player) {
47
+ total += player.stack;
48
+ }
49
+ }
50
+ return total;
51
+ }
52
+ /**
53
+ * Calculate total chips in pots
54
+ */
55
+ function calculatePotTotal(state) {
56
+ let total = 0;
57
+ for (const pot of state.pots) {
58
+ total += pot.amount;
59
+ }
60
+ return total;
61
+ }
62
+ /**
63
+ * Calculate total chips in current bets
64
+ */
65
+ function calculateBetTotal(state) {
66
+ let total = 0;
67
+ for (const bet of state.currentBets.values()) {
68
+ total += bet;
69
+ }
70
+ return total;
71
+ }
72
+ /**
73
+ * Get initial chips (sum of all starting stacks)
74
+ * This calculates total chips in the game, which should remain constant (minus rake).
75
+ *
76
+ * - During a hand: stack + totalInvestedThisHand (chips in play + chips invested)
77
+ * - After hand complete: stack + rake (all chips have been distributed, rake removed)
78
+ */
79
+ function getInitialChips(state) {
80
+ let total = 0;
81
+ // Hand is complete if winners are declared AND pots/bets have been distributed
82
+ // This ensures we don't switch modes mid-hand
83
+ const handComplete = state.winners !== null && state.pots.length === 0 && state.currentBets.size === 0;
84
+ for (const player of state.players) {
85
+ if (player) {
86
+ if (handComplete) {
87
+ // Hand complete: only count current stacks
88
+ total += player.stack;
89
+ }
90
+ else {
91
+ // Hand in progress: stack + invested
92
+ total += player.stack + player.totalInvestedThisHand;
93
+ }
94
+ }
95
+ }
96
+ // Add rake back to total after hand is complete (cash games only)
97
+ // Rake is removed from the game, so we need to account for it
98
+ if (handComplete) {
99
+ total += state.rakeThisHand;
100
+ }
101
+ return total;
102
+ }
103
+ /**
104
+ * Validate game state integrity
105
+ * Checks multiple invariants beyond just chip conservation
106
+ */
107
+ function validateGameStateIntegrity(state) {
108
+ // 1. Chip conservation
109
+ const initialChips = getInitialChips(state);
110
+ auditChipConservation(state, initialChips);
111
+ // 2. No negative stacks
112
+ for (const player of state.players) {
113
+ if (player && player.stack < 0) {
114
+ throw new CriticalStateError_1.CriticalStateError(`Player ${player.id} has negative stack: ${player.stack}`, {
115
+ playerId: player.id,
116
+ stack: player.stack,
117
+ });
118
+ }
119
+ }
120
+ // 3. No negative bets
121
+ for (const [seat, bet] of state.currentBets.entries()) {
122
+ if (bet < 0) {
123
+ throw new CriticalStateError_1.CriticalStateError(`Seat ${seat} has negative bet: ${bet}`, {
124
+ seat,
125
+ bet,
126
+ });
127
+ }
128
+ }
129
+ // 4. No negative pots
130
+ for (let i = 0; i < state.pots.length; i++) {
131
+ const pot = state.pots[i];
132
+ if (pot.amount < 0) {
133
+ throw new CriticalStateError_1.CriticalStateError(`Pot ${i} has negative amount: ${pot.amount}`, {
134
+ potIndex: i,
135
+ amount: pot.amount,
136
+ });
137
+ }
138
+ }
139
+ // 5. ActionTo must be valid seat or null
140
+ if (state.actionTo !== null) {
141
+ if (state.actionTo < 0 || state.actionTo >= state.maxPlayers) {
142
+ throw new CriticalStateError_1.CriticalStateError(`Invalid actionTo: ${state.actionTo}`, {
143
+ actionTo: state.actionTo,
144
+ maxPlayers: state.maxPlayers,
145
+ });
146
+ }
147
+ const player = state.players[state.actionTo];
148
+ if (!player) {
149
+ throw new CriticalStateError_1.CriticalStateError(`ActionTo points to empty seat: ${state.actionTo}`, {
150
+ actionTo: state.actionTo,
151
+ });
152
+ }
153
+ }
154
+ // 6. Button must be valid or null
155
+ if (state.buttonSeat !== null) {
156
+ if (state.buttonSeat < 0 || state.buttonSeat >= state.maxPlayers) {
157
+ throw new CriticalStateError_1.CriticalStateError(`Invalid buttonSeat: ${state.buttonSeat}`, {
158
+ buttonSeat: state.buttonSeat,
159
+ maxPlayers: state.maxPlayers,
160
+ });
161
+ }
162
+ }
163
+ }