@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.
- package/LICENSE +21 -0
- package/README.md +607 -0
- package/dist/actions/betting.d.ts +21 -0
- package/dist/actions/betting.js +410 -0
- package/dist/actions/dealing.d.ts +9 -0
- package/dist/actions/dealing.js +206 -0
- package/dist/actions/management.d.ts +9 -0
- package/dist/actions/management.js +58 -0
- package/dist/actions/showdownActions.d.ts +9 -0
- package/dist/actions/showdownActions.js +119 -0
- package/dist/actions/special.d.ts +14 -0
- package/dist/actions/special.js +98 -0
- package/dist/actions/streetProgression.d.ts +13 -0
- package/dist/actions/streetProgression.js +157 -0
- package/dist/actions/tournament.d.ts +5 -0
- package/dist/actions/tournament.js +38 -0
- package/dist/actions/validation.d.ts +6 -0
- package/dist/actions/validation.js +182 -0
- package/dist/engine/PokerEngine.d.ts +92 -0
- package/dist/engine/PokerEngine.js +246 -0
- package/dist/engine/gameReducer.d.ts +10 -0
- package/dist/engine/gameReducer.js +135 -0
- package/dist/errors/ConfigError.d.ts +8 -0
- package/dist/errors/ConfigError.js +15 -0
- package/dist/errors/CriticalStateError.d.ts +8 -0
- package/dist/errors/CriticalStateError.js +15 -0
- package/dist/errors/ErrorCodes.d.ts +38 -0
- package/dist/errors/ErrorCodes.js +46 -0
- package/dist/errors/IllegalActionError.d.ts +9 -0
- package/dist/errors/IllegalActionError.js +15 -0
- package/dist/errors/PokerEngineError.d.ts +8 -0
- package/dist/errors/PokerEngineError.js +19 -0
- package/dist/errors/index.d.ts +5 -0
- package/dist/errors/index.js +22 -0
- package/dist/history/exporter.d.ts +28 -0
- package/dist/history/exporter.js +60 -0
- package/dist/history/formats/json.d.ts +14 -0
- package/dist/history/formats/json.js +46 -0
- package/dist/history/formats/pokerstars.d.ts +10 -0
- package/dist/history/formats/pokerstars.js +188 -0
- package/dist/history/handHistoryBuilder.d.ts +10 -0
- package/dist/history/handHistoryBuilder.js +179 -0
- package/dist/history/types.d.ts +73 -0
- package/dist/history/types.js +5 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +38 -0
- package/dist/rules/actionOrder.d.ts +14 -0
- package/dist/rules/actionOrder.js +211 -0
- package/dist/rules/blinds.d.ts +24 -0
- package/dist/rules/blinds.js +64 -0
- package/dist/rules/headsUp.d.ts +15 -0
- package/dist/rules/headsUp.js +44 -0
- package/dist/rules/showdown.d.ts +9 -0
- package/dist/rules/showdown.js +164 -0
- package/dist/rules/sidePots.d.ts +32 -0
- package/dist/rules/sidePots.js +173 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/utils/cardUtils.d.ts +12 -0
- package/dist/utils/cardUtils.js +30 -0
- package/dist/utils/constants.d.ts +38 -0
- package/dist/utils/constants.js +41 -0
- package/dist/utils/deck.d.ts +46 -0
- package/dist/utils/deck.js +126 -0
- package/dist/utils/invariants.d.ts +39 -0
- package/dist/utils/invariants.js +163 -0
- package/dist/utils/positioning.d.ts +36 -0
- package/dist/utils/positioning.js +97 -0
- package/dist/utils/rake.d.ts +13 -0
- package/dist/utils/rake.js +45 -0
- package/dist/utils/serialization.d.ts +53 -0
- package/dist/utils/serialization.js +106 -0
- package/dist/utils/validation.d.ts +20 -0
- package/dist/utils/validation.js +52 -0
- package/dist/utils/viewMasking.d.ts +20 -0
- package/dist/utils/viewMasking.js +90 -0
- 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
|
+
}
|