@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.
- package/README.md +8 -5
- package/dist/.tsbuildinfo +1 -1
- package/dist/actions/betting.js +23 -73
- package/dist/actions/dealing.js +29 -56
- package/dist/actions/management.js +3 -1
- package/dist/actions/{showdownActions.js → showdown-actions.js} +13 -27
- package/dist/actions/special.js +20 -7
- package/dist/actions/{streetProgression.js → street-progression.js} +16 -31
- package/dist/actions/tournament.js +1 -4
- package/dist/actions/validation.js +48 -33
- package/dist/browser.d.ts +5 -5
- package/dist/browser.js +6 -7
- package/dist/engine/{gameReducer.js → game-reducer.js} +16 -32
- package/dist/engine/{PokerEngine.js → poker-engine.js} +16 -30
- package/dist/errors/{ConfigError.d.ts → config-error.d.ts} +1 -1
- package/dist/errors/{ConfigError.js → config-error.js} +2 -2
- package/dist/errors/{CriticalStateError.d.ts → critical-state-error.d.ts} +1 -1
- package/dist/errors/{CriticalStateError.js → critical-state-error.js} +2 -2
- package/dist/errors/{IllegalActionError.d.ts → illegal-action-error.d.ts} +2 -2
- package/dist/errors/{IllegalActionError.js → illegal-action-error.js} +2 -2
- package/dist/errors/index.d.ts +4 -4
- package/dist/errors/index.js +4 -6
- package/dist/errors/{PokerEngineError.js → poker-engine-error.js} +2 -0
- package/dist/history/exporter.js +4 -4
- package/dist/history/formats/pokerstars.js +0 -8
- package/dist/history/{handHistoryBuilder.js → hand-history-builder.js} +1 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -5
- package/dist/rules/{actionOrder.js → action-order.js} +12 -17
- package/dist/rules/blinds.js +3 -3
- package/dist/rules/{headsUp.js → heads-up.js} +0 -1
- package/dist/rules/showdown.js +45 -29
- package/dist/rules/{sidePots.js → side-pots.js} +7 -20
- package/dist/utils/deck.js +1 -4
- package/dist/utils/invariants.js +16 -10
- package/dist/utils/rake.js +6 -8
- package/dist/utils/serialization.d.ts +1 -0
- package/dist/utils/serialization.js +10 -5
- package/dist/utils/validation.js +8 -8
- package/dist/utils/{viewMasking.js → view-masking.js} +16 -6
- package/package.json +6 -6
- package/dist/errors/ErrorCodes.d.ts +0 -7
- package/dist/errors/ErrorCodes.js +0 -12
- /package/dist/actions/{showdownActions.d.ts → showdown-actions.d.ts} +0 -0
- /package/dist/actions/{streetProgression.d.ts → street-progression.d.ts} +0 -0
- /package/dist/engine/{gameReducer.d.ts → game-reducer.d.ts} +0 -0
- /package/dist/engine/{PokerEngine.d.ts → poker-engine.d.ts} +0 -0
- /package/dist/errors/{PokerEngineError.d.ts → poker-engine-error.d.ts} +0 -0
- /package/dist/history/{handHistoryBuilder.d.ts → hand-history-builder.d.ts} +0 -0
- /package/dist/rules/{actionOrder.d.ts → action-order.d.ts} +0 -0
- /package/dist/rules/{headsUp.d.ts → heads-up.d.ts} +0 -0
- /package/dist/rules/{sidePots.d.ts → side-pots.d.ts} +0 -0
- /package/dist/utils/{cardUtils.d.ts → card-utils.d.ts} +0 -0
- /package/dist/utils/{cardUtils.js → card-utils.js} +0 -0
- /package/dist/utils/{viewMasking.d.ts → view-masking.d.ts} +0 -0
package/dist/rules/showdown.js
CHANGED
|
@@ -34,8 +34,8 @@ function determineWinners(state) {
|
|
|
34
34
|
// Distribute pot among winners
|
|
35
35
|
const share = Math.floor(potAfterRake / potWinners.length);
|
|
36
36
|
const remainder = potAfterRake % potWinners.length;
|
|
37
|
-
// Sort winners by position
|
|
38
|
-
// TDA Rule:
|
|
37
|
+
// Sort winners by position for odd chip distribution.
|
|
38
|
+
// TDA Rule: odd chips go to first player(s) clockwise from the button.
|
|
39
39
|
const sortedWinners = [...potWinners].sort((a, b) => {
|
|
40
40
|
if (state.buttonSeat === null)
|
|
41
41
|
return 0;
|
|
@@ -47,20 +47,17 @@ function determineWinners(state) {
|
|
|
47
47
|
for (let i = 0; i < sortedWinners.length; i++) {
|
|
48
48
|
const evaluation = sortedWinners[i];
|
|
49
49
|
let award = share;
|
|
50
|
-
//
|
|
51
|
-
// (first N winners in sorted order get the extra chips)
|
|
50
|
+
// First N winners (worst position) each get one extra chip.
|
|
52
51
|
if (i < remainder) {
|
|
53
52
|
award += 1;
|
|
54
53
|
}
|
|
55
|
-
// Track winner seats
|
|
56
54
|
winnerSeats.add(evaluation.seat);
|
|
57
|
-
// Award chips to player
|
|
58
55
|
const player = newPlayers[evaluation.seat];
|
|
59
56
|
newPlayers[evaluation.seat] = {
|
|
60
57
|
...player,
|
|
61
58
|
stack: player.stack + award,
|
|
62
59
|
};
|
|
63
|
-
// Record winner
|
|
60
|
+
// Record winner with hand description.
|
|
64
61
|
winners.push({
|
|
65
62
|
seat: evaluation.seat,
|
|
66
63
|
amount: award,
|
|
@@ -104,11 +101,10 @@ function determineWinners(state) {
|
|
|
104
101
|
* Evaluate a single pot and return winner(s)
|
|
105
102
|
*/
|
|
106
103
|
function evaluatePot(state, pot) {
|
|
107
|
-
// Get eligible players (not folded)
|
|
108
104
|
const eligible = pot.eligibleSeats
|
|
109
105
|
.map((seat) => state.players[seat])
|
|
110
106
|
.filter((player) => player && (player.status === "ACTIVE" /* PlayerStatus.ACTIVE */ || player.status === "ALL_IN" /* PlayerStatus.ALL_IN */));
|
|
111
|
-
//
|
|
107
|
+
// Single remaining player wins uncontested.
|
|
112
108
|
if (eligible.length === 1) {
|
|
113
109
|
const player = eligible[0];
|
|
114
110
|
return [
|
|
@@ -120,22 +116,17 @@ function evaluatePot(state, pot) {
|
|
|
120
116
|
},
|
|
121
117
|
];
|
|
122
118
|
}
|
|
123
|
-
// Evaluate all hands
|
|
124
119
|
const evaluations = [];
|
|
125
120
|
for (const player of eligible) {
|
|
126
121
|
if (!player?.hand)
|
|
127
122
|
continue;
|
|
128
|
-
// Skip masked hands
|
|
123
|
+
// Skip masked hands in client mode.
|
|
129
124
|
if (player.hand.some((c) => c === null)) {
|
|
130
125
|
continue;
|
|
131
126
|
}
|
|
132
|
-
// Combine hole cards + board (7 cards total for river)
|
|
133
127
|
const allCards = [...player.hand, ...state.board];
|
|
134
|
-
if (allCards.length < 5)
|
|
135
|
-
// Not enough cards (shouldn't happen)
|
|
128
|
+
if (allCards.length < 5)
|
|
136
129
|
continue;
|
|
137
|
-
}
|
|
138
|
-
// Evaluate using @pokertools/evaluator
|
|
139
130
|
const cardCodes = (0, evaluator_1.getCardCodes)(allCards);
|
|
140
131
|
const score = (0, evaluator_1.evaluate)(cardCodes);
|
|
141
132
|
const handRank = (0, evaluator_1.rank)(cardCodes);
|
|
@@ -143,32 +134,57 @@ function evaluatePot(state, pot) {
|
|
|
143
134
|
evaluations.push({
|
|
144
135
|
seat: player.seat,
|
|
145
136
|
score,
|
|
146
|
-
hand:
|
|
137
|
+
hand: getBestFiveCardHand(allCards),
|
|
147
138
|
description,
|
|
148
139
|
});
|
|
149
140
|
}
|
|
150
141
|
if (evaluations.length === 0) {
|
|
151
142
|
return [];
|
|
152
143
|
}
|
|
153
|
-
// Find best hand(s)
|
|
154
144
|
const bestScore = Math.min(...evaluations.map((e) => e.score));
|
|
155
|
-
|
|
156
|
-
|
|
145
|
+
return evaluations.filter((e) => e.score === bestScore);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Return the concrete five cards that produce the best evaluator score.
|
|
149
|
+
* The evaluator score is lower-is-better, so enumerate all 5-card subsets and
|
|
150
|
+
* keep the first subset with the minimum score. Hold'em has at most 7 cards,
|
|
151
|
+
* making this deterministic brute-force path only 21 evaluations per player.
|
|
152
|
+
*/
|
|
153
|
+
function getBestFiveCardHand(cards) {
|
|
154
|
+
if (cards.length === 5) {
|
|
155
|
+
return [...cards];
|
|
156
|
+
}
|
|
157
|
+
let bestHand = null;
|
|
158
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
159
|
+
for (let a = 0; a < cards.length - 4; a++) {
|
|
160
|
+
for (let b = a + 1; b < cards.length - 3; b++) {
|
|
161
|
+
for (let c = b + 1; c < cards.length - 2; c++) {
|
|
162
|
+
for (let d = c + 1; d < cards.length - 1; d++) {
|
|
163
|
+
for (let e = d + 1; e < cards.length; e++) {
|
|
164
|
+
const candidate = [cards[a], cards[b], cards[c], cards[d], cards[e]];
|
|
165
|
+
const score = (0, evaluator_1.evaluate)((0, evaluator_1.getCardCodes)(candidate));
|
|
166
|
+
if (score < bestScore) {
|
|
167
|
+
bestScore = score;
|
|
168
|
+
bestHand = candidate;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (bestHand === null) {
|
|
176
|
+
return [...cards.slice(0, 5)];
|
|
177
|
+
}
|
|
178
|
+
return bestHand;
|
|
157
179
|
}
|
|
158
180
|
/**
|
|
159
181
|
* Check if hand should go to showdown
|
|
160
182
|
*/
|
|
161
183
|
function shouldShowdown(state) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
// 3. Winners haven't been determined yet
|
|
166
|
-
if (state.street !== "SHOWDOWN" /* Street.SHOWDOWN */) {
|
|
184
|
+
if (state.street !== "SHOWDOWN" /* Street.SHOWDOWN */)
|
|
185
|
+
return false;
|
|
186
|
+
if (state.winners !== null)
|
|
167
187
|
return false;
|
|
168
|
-
}
|
|
169
|
-
if (state.winners !== null) {
|
|
170
|
-
return false; // Already determined winners
|
|
171
|
-
}
|
|
172
188
|
const activePlayers = state.players.filter((p) => p && (p.status === "ACTIVE" /* PlayerStatus.ACTIVE */ || p.status === "ALL_IN" /* PlayerStatus.ALL_IN */));
|
|
173
189
|
return activePlayers.length >= 2;
|
|
174
190
|
}
|
|
@@ -4,7 +4,7 @@ exports.calculateSidePots = calculateSidePots;
|
|
|
4
4
|
exports.calculateUncalledBet = calculateUncalledBet;
|
|
5
5
|
exports.returnUncalledBet = returnUncalledBet;
|
|
6
6
|
exports.recalculatePots = recalculatePots;
|
|
7
|
-
const
|
|
7
|
+
const critical_state_error_1 = require("../errors/critical-state-error");
|
|
8
8
|
/**
|
|
9
9
|
* Calculate side pots using iterative subtraction method
|
|
10
10
|
*
|
|
@@ -36,11 +36,9 @@ function calculateSidePots(state) {
|
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
// If no investments, return empty
|
|
40
39
|
if (investments.length === 0) {
|
|
41
40
|
return [];
|
|
42
41
|
}
|
|
43
|
-
// Sort by investment (ascending)
|
|
44
42
|
investments.sort((a, b) => a.amount - b.amount);
|
|
45
43
|
const pots = [];
|
|
46
44
|
let prevAmount = 0;
|
|
@@ -48,16 +46,13 @@ function calculateSidePots(state) {
|
|
|
48
46
|
const current = investments[i];
|
|
49
47
|
const allAtThisLevel = investments.slice(i); // Current + all higher investors
|
|
50
48
|
const increment = current.amount - prevAmount;
|
|
51
|
-
// Create pot for this level
|
|
52
49
|
if (increment > 0) {
|
|
53
|
-
// Pot includes
|
|
50
|
+
// Pot includes ALL players at this level (folded players' chips stay in).
|
|
54
51
|
const potAmount = increment * allAtThisLevel.length;
|
|
55
|
-
//
|
|
52
|
+
// Only non-folded players are eligible to win.
|
|
56
53
|
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
54
|
if (eligibleSeats.length === 0) {
|
|
60
|
-
throw new
|
|
55
|
+
throw new critical_state_error_1.CriticalStateError(`Side pot has no eligible players - all ${allAtThisLevel.length} players at this level have folded`, {
|
|
61
56
|
potAmount,
|
|
62
57
|
potLevel: i,
|
|
63
58
|
investmentLevel: current.amount,
|
|
@@ -86,10 +81,8 @@ function calculateSidePots(state) {
|
|
|
86
81
|
* @returns Tuple of [uncalled amount, seat to return to]
|
|
87
82
|
*/
|
|
88
83
|
function calculateUncalledBet(state) {
|
|
89
|
-
if (state.currentBets.size === 0)
|
|
84
|
+
if (state.currentBets.size === 0)
|
|
90
85
|
return null;
|
|
91
|
-
}
|
|
92
|
-
// Find highest bet
|
|
93
86
|
let maxBet = 0;
|
|
94
87
|
let maxBetSeat = -1;
|
|
95
88
|
let secondMaxBet = 0;
|
|
@@ -119,21 +112,17 @@ function returnUncalledBet(state) {
|
|
|
119
112
|
}
|
|
120
113
|
const [amount, seat] = uncalled;
|
|
121
114
|
const player = state.players[seat];
|
|
122
|
-
if (!player)
|
|
115
|
+
if (!player)
|
|
123
116
|
return state;
|
|
124
|
-
}
|
|
125
|
-
// Return chips to player
|
|
126
117
|
const newPlayers = [...state.players];
|
|
127
118
|
newPlayers[seat] = {
|
|
128
119
|
...player,
|
|
129
120
|
stack: player.stack + amount,
|
|
130
121
|
totalInvestedThisHand: player.totalInvestedThisHand - amount,
|
|
131
122
|
};
|
|
132
|
-
// Reduce current bet
|
|
133
123
|
const newCurrentBets = new Map(state.currentBets);
|
|
134
124
|
const currentBet = newCurrentBets.get(seat) ?? 0;
|
|
135
125
|
newCurrentBets.set(seat, currentBet - amount);
|
|
136
|
-
// Record to action history
|
|
137
126
|
const actionRecord = {
|
|
138
127
|
action: {
|
|
139
128
|
type: "UNCALLED_BET_RETURNED" /* ActionType.UNCALLED_BET_RETURNED */,
|
|
@@ -158,11 +147,9 @@ function returnUncalledBet(state) {
|
|
|
158
147
|
* This is called before progressing to next street
|
|
159
148
|
*/
|
|
160
149
|
function recalculatePots(state) {
|
|
161
|
-
// First, return any uncalled bet
|
|
162
150
|
const newState = returnUncalledBet(state);
|
|
163
|
-
// Calculate side pots based on all investments
|
|
164
151
|
const pots = calculateSidePots(newState);
|
|
165
|
-
// Reset betThisStreet
|
|
152
|
+
// Reset betThisStreet after collecting bets into pots.
|
|
166
153
|
const newPlayers = newState.players.map((p) => (p ? { ...p, betThisStreet: 0 } : null));
|
|
167
154
|
return {
|
|
168
155
|
...newState,
|
package/dist/utils/deck.js
CHANGED
|
@@ -28,10 +28,8 @@ function createDeck() {
|
|
|
28
28
|
* be used for production poker games. Always provide a secure RNG.
|
|
29
29
|
*/
|
|
30
30
|
function getSecureRandom() {
|
|
31
|
-
// Check if we're in Node.js environment
|
|
32
31
|
if (typeof process !== "undefined" && process.versions?.node) {
|
|
33
32
|
try {
|
|
34
|
-
// Use Node.js crypto for production
|
|
35
33
|
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
|
|
36
34
|
const crypto = require("crypto");
|
|
37
35
|
return () => {
|
|
@@ -81,11 +79,10 @@ function getSecureRandom() {
|
|
|
81
79
|
*/
|
|
82
80
|
function shuffle(deck, rng) {
|
|
83
81
|
const random = rng ?? getSecureRandom();
|
|
84
|
-
const shuffled = [...deck];
|
|
82
|
+
const shuffled = [...deck];
|
|
85
83
|
// Fisher-Yates shuffle
|
|
86
84
|
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
87
85
|
const j = Math.floor(random() * (i + 1));
|
|
88
|
-
// Swap elements
|
|
89
86
|
const temp = shuffled[i];
|
|
90
87
|
shuffled[i] = shuffled[j];
|
|
91
88
|
shuffled[j] = temp;
|
package/dist/utils/invariants.js
CHANGED
|
@@ -7,7 +7,7 @@ exports.calculatePotTotal = calculatePotTotal;
|
|
|
7
7
|
exports.calculateBetTotal = calculateBetTotal;
|
|
8
8
|
exports.getInitialChips = getInitialChips;
|
|
9
9
|
exports.validateGameStateIntegrity = validateGameStateIntegrity;
|
|
10
|
-
const
|
|
10
|
+
const critical_state_error_1 = require("../errors/critical-state-error");
|
|
11
11
|
/**
|
|
12
12
|
* Audit chip conservation
|
|
13
13
|
* Formula: ∑(player.stack) + ∑(pot.amount) + ∑(currentBets) = initialChips
|
|
@@ -17,9 +17,10 @@ const CriticalStateError_1 = require("../errors/CriticalStateError");
|
|
|
17
17
|
* @throws CriticalStateError if chips don't match
|
|
18
18
|
*/
|
|
19
19
|
function auditChipConservation(state, initialChips) {
|
|
20
|
-
const
|
|
20
|
+
const handComplete = state.winners !== null && state.pots.length === 0 && state.currentBets.size === 0;
|
|
21
|
+
const currentChips = calculateTotalChips(state) + (handComplete ? state.rakeThisHand : 0);
|
|
21
22
|
if (currentChips !== initialChips) {
|
|
22
|
-
throw new
|
|
23
|
+
throw new critical_state_error_1.CriticalStateError(`Chip conservation violated: expected ${initialChips}, found ${currentChips}`, {
|
|
23
24
|
expected: initialChips,
|
|
24
25
|
actual: currentChips,
|
|
25
26
|
difference: currentChips - initialChips,
|
|
@@ -77,6 +78,10 @@ function calculateBetTotal(state) {
|
|
|
77
78
|
* - After hand complete: stack + rake (all chips have been distributed, rake removed)
|
|
78
79
|
*/
|
|
79
80
|
function getInitialChips(state) {
|
|
81
|
+
const baseline = state.initialChips;
|
|
82
|
+
if (typeof baseline === "number") {
|
|
83
|
+
return baseline;
|
|
84
|
+
}
|
|
80
85
|
let total = 0;
|
|
81
86
|
// Hand is complete if winners are declared AND pots/bets have been distributed
|
|
82
87
|
// This ensures we don't switch modes mid-hand
|
|
@@ -110,12 +115,13 @@ function validateGameStateIntegrity(state) {
|
|
|
110
115
|
return;
|
|
111
116
|
}
|
|
112
117
|
// 1. Chip conservation
|
|
113
|
-
const
|
|
118
|
+
const baseline = state.initialChips;
|
|
119
|
+
const initialChips = typeof baseline === "number" ? baseline : getInitialChips(state);
|
|
114
120
|
auditChipConservation(state, initialChips);
|
|
115
121
|
// 2. No negative stacks
|
|
116
122
|
for (const player of state.players) {
|
|
117
123
|
if (player && player.stack < 0) {
|
|
118
|
-
throw new
|
|
124
|
+
throw new critical_state_error_1.CriticalStateError(`Player ${player.id} has negative stack: ${player.stack}`, {
|
|
119
125
|
playerId: player.id,
|
|
120
126
|
stack: player.stack,
|
|
121
127
|
});
|
|
@@ -124,7 +130,7 @@ function validateGameStateIntegrity(state) {
|
|
|
124
130
|
// 3. No negative bets
|
|
125
131
|
for (const [seat, bet] of state.currentBets.entries()) {
|
|
126
132
|
if (bet < 0) {
|
|
127
|
-
throw new
|
|
133
|
+
throw new critical_state_error_1.CriticalStateError(`Seat ${seat} has negative bet: ${bet}`, {
|
|
128
134
|
seat,
|
|
129
135
|
bet,
|
|
130
136
|
});
|
|
@@ -134,7 +140,7 @@ function validateGameStateIntegrity(state) {
|
|
|
134
140
|
for (let i = 0; i < state.pots.length; i++) {
|
|
135
141
|
const pot = state.pots[i];
|
|
136
142
|
if (pot.amount < 0) {
|
|
137
|
-
throw new
|
|
143
|
+
throw new critical_state_error_1.CriticalStateError(`Pot ${i} has negative amount: ${pot.amount}`, {
|
|
138
144
|
potIndex: i,
|
|
139
145
|
amount: pot.amount,
|
|
140
146
|
});
|
|
@@ -143,14 +149,14 @@ function validateGameStateIntegrity(state) {
|
|
|
143
149
|
// 5. ActionTo must be valid seat or null
|
|
144
150
|
if (state.actionTo !== null) {
|
|
145
151
|
if (state.actionTo < 0 || state.actionTo >= state.maxPlayers) {
|
|
146
|
-
throw new
|
|
152
|
+
throw new critical_state_error_1.CriticalStateError(`Invalid actionTo: ${state.actionTo}`, {
|
|
147
153
|
actionTo: state.actionTo,
|
|
148
154
|
maxPlayers: state.maxPlayers,
|
|
149
155
|
});
|
|
150
156
|
}
|
|
151
157
|
const player = state.players[state.actionTo];
|
|
152
158
|
if (!player) {
|
|
153
|
-
throw new
|
|
159
|
+
throw new critical_state_error_1.CriticalStateError(`ActionTo points to empty seat: ${state.actionTo}`, {
|
|
154
160
|
actionTo: state.actionTo,
|
|
155
161
|
});
|
|
156
162
|
}
|
|
@@ -158,7 +164,7 @@ function validateGameStateIntegrity(state) {
|
|
|
158
164
|
// 6. Button must be valid or null
|
|
159
165
|
if (state.buttonSeat !== null) {
|
|
160
166
|
if (state.buttonSeat < 0 || state.buttonSeat >= state.maxPlayers) {
|
|
161
|
-
throw new
|
|
167
|
+
throw new critical_state_error_1.CriticalStateError(`Invalid buttonSeat: ${state.buttonSeat}`, {
|
|
162
168
|
buttonSeat: state.buttonSeat,
|
|
163
169
|
maxPlayers: state.maxPlayers,
|
|
164
170
|
});
|
package/dist/utils/rake.js
CHANGED
|
@@ -10,25 +10,23 @@ exports.calculateRake = calculateRake;
|
|
|
10
10
|
* @returns Object with rake amount and whether cap was hit
|
|
11
11
|
*/
|
|
12
12
|
function calculateRake(state, potAmount, rakeTakenSoFar = 0) {
|
|
13
|
-
// No rake for tournaments (identified by presence of
|
|
13
|
+
// No rake for tournaments (identified by presence of blindStructure)
|
|
14
14
|
if (state.config.blindStructure) {
|
|
15
15
|
return { rake: 0, capReached: false };
|
|
16
16
|
}
|
|
17
|
-
// No rake if not configured
|
|
18
17
|
const rakePercent = state.config.rakePercent ?? 0;
|
|
19
18
|
if (rakePercent === 0) {
|
|
20
19
|
return { rake: 0, capReached: false };
|
|
21
20
|
}
|
|
22
|
-
// "No Flop, No Drop" rule (standard in most cash games)
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
const noFlopNoDrop = state.config.noFlopNoDrop !== false;
|
|
21
|
+
// "No Flop, No Drop" rule (standard in most cash games).
|
|
22
|
+
// When enabled (default), no rake is taken if no flop was dealt,
|
|
23
|
+
// regardless of whether the hand ended preflop or all-in preflop.
|
|
24
|
+
const noFlopNoDrop = state.config.noFlopNoDrop !== false;
|
|
26
25
|
if (noFlopNoDrop && state.board.length === 0) {
|
|
27
26
|
return { rake: 0, capReached: false };
|
|
28
27
|
}
|
|
29
|
-
// Calculate rake as percentage
|
|
30
28
|
let rake = Math.floor((potAmount * rakePercent) / 100);
|
|
31
|
-
// Apply
|
|
29
|
+
// Apply per-hand rake cap (not per-pot).
|
|
32
30
|
let capReached = false;
|
|
33
31
|
if (state.config.rakeCap !== undefined) {
|
|
34
32
|
const rakeAllowed = state.config.rakeCap - rakeTakenSoFar;
|
|
@@ -19,8 +19,12 @@ function createSnapshot(state) {
|
|
|
19
19
|
for (const [seat, time] of state.timeBanks.entries()) {
|
|
20
20
|
timeBanks[seat] = time;
|
|
21
21
|
}
|
|
22
|
-
// Truncate previous states
|
|
23
|
-
|
|
22
|
+
// Truncate previous states and do not recursively serialize nested undo
|
|
23
|
+
// histories, avoiding exponential snapshot growth.
|
|
24
|
+
const previousStates = state.previousStates.slice(-10).map((s) => createSnapshot({
|
|
25
|
+
...s,
|
|
26
|
+
previousStates: [],
|
|
27
|
+
}));
|
|
24
28
|
return {
|
|
25
29
|
config: state.config,
|
|
26
30
|
players: [...state.players],
|
|
@@ -47,6 +51,7 @@ function createSnapshot(state) {
|
|
|
47
51
|
timeBankActiveSeat: state.timeBankActiveSeat,
|
|
48
52
|
actionHistory: Array.from(state.actionHistory),
|
|
49
53
|
previousStates,
|
|
54
|
+
initialChips: state.initialChips,
|
|
50
55
|
timestamp: state.timestamp,
|
|
51
56
|
handId: state.handId,
|
|
52
57
|
};
|
|
@@ -70,9 +75,10 @@ function restoreFromSnapshot(snapshot) {
|
|
|
70
75
|
...snapshot,
|
|
71
76
|
currentBets,
|
|
72
77
|
timeBanks,
|
|
73
|
-
timeBankActiveSeat: snapshot.timeBankActiveSeat
|
|
78
|
+
timeBankActiveSeat: snapshot.timeBankActiveSeat,
|
|
74
79
|
previousStates,
|
|
75
|
-
|
|
80
|
+
initialChips: snapshot.initialChips,
|
|
81
|
+
rakeThisHand: snapshot.rakeThisHand,
|
|
76
82
|
};
|
|
77
83
|
}
|
|
78
84
|
/**
|
|
@@ -93,7 +99,6 @@ function deserializeSnapshot(json) {
|
|
|
93
99
|
*/
|
|
94
100
|
function validateSnapshot(snapshot) {
|
|
95
101
|
try {
|
|
96
|
-
// Basic validation
|
|
97
102
|
if (!snapshot.handId)
|
|
98
103
|
return false;
|
|
99
104
|
if (snapshot.maxPlayers < 2 || snapshot.maxPlayers > 10)
|
package/dist/utils/validation.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.validateChipAmount = validateChipAmount;
|
|
7
7
|
exports.validateTimestamp = validateTimestamp;
|
|
8
|
-
const
|
|
9
|
-
const
|
|
8
|
+
const illegal_action_error_1 = require("../errors/illegal-action-error");
|
|
9
|
+
const types_1 = require("@pokertools/types");
|
|
10
10
|
const constants_1 = require("./constants");
|
|
11
11
|
/**
|
|
12
12
|
* Validate that a chip amount is a non-negative integer
|
|
@@ -18,13 +18,13 @@ const constants_1 = require("./constants");
|
|
|
18
18
|
*/
|
|
19
19
|
function validateChipAmount(amount, context) {
|
|
20
20
|
if (!Number.isFinite(amount)) {
|
|
21
|
-
throw new
|
|
21
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.INVALID_AMOUNT, `${context}: ${amount} is not a valid number`, { amount, context });
|
|
22
22
|
}
|
|
23
23
|
if (!Number.isInteger(amount)) {
|
|
24
|
-
throw new
|
|
24
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.INVALID_AMOUNT, `${context}: ${amount} must be an integer (fractional chips not allowed)`, { amount, context });
|
|
25
25
|
}
|
|
26
26
|
if (amount < 0) {
|
|
27
|
-
throw new
|
|
27
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.INVALID_AMOUNT, `${context}: ${amount} cannot be negative`, { amount, context });
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
@@ -36,17 +36,17 @@ function validateChipAmount(amount, context) {
|
|
|
36
36
|
*/
|
|
37
37
|
function validateTimestamp(timestamp, previousTimestamp) {
|
|
38
38
|
if (!Number.isFinite(timestamp) || timestamp < 0) {
|
|
39
|
-
throw new
|
|
39
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.INVALID_TIMESTAMP, `Invalid timestamp: ${timestamp}`, {
|
|
40
40
|
timestamp,
|
|
41
41
|
});
|
|
42
42
|
}
|
|
43
43
|
// Allow some clock drift tolerance for "future" timestamps
|
|
44
44
|
const now = Date.now() + constants_1.TIMESTAMP_FUTURE_TOLERANCE_MS;
|
|
45
45
|
if (timestamp > now) {
|
|
46
|
-
throw new
|
|
46
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.INVALID_TIMESTAMP, `Timestamp ${timestamp} is in the future (current: ${Date.now()})`, { timestamp, currentTime: Date.now() });
|
|
47
47
|
}
|
|
48
48
|
// Ensure timestamps are monotonically increasing (or equal for same-time actions)
|
|
49
49
|
if (previousTimestamp !== undefined && timestamp < previousTimestamp) {
|
|
50
|
-
throw new
|
|
50
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.INVALID_TIMESTAMP, `Timestamp ${timestamp} is before previous action timestamp ${previousTimestamp}`, { timestamp, previousTimestamp });
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -24,20 +24,30 @@ function createPublicView(state, playerId = null, version = 0) {
|
|
|
24
24
|
hand: visibleHand,
|
|
25
25
|
};
|
|
26
26
|
});
|
|
27
|
-
|
|
28
|
-
const currentBetsObj = {};
|
|
29
|
-
for (const [seat, amount] of state.currentBets.entries()) {
|
|
30
|
-
currentBetsObj[seat] = amount;
|
|
31
|
-
}
|
|
27
|
+
const currentBets = createSerializableReadonlyMap(state.currentBets);
|
|
32
28
|
return {
|
|
33
29
|
...state,
|
|
34
30
|
deck: [], // Always hide deck
|
|
35
31
|
players: maskedPlayers,
|
|
36
|
-
currentBets
|
|
32
|
+
currentBets,
|
|
37
33
|
viewingPlayerId: playerId,
|
|
38
34
|
version,
|
|
39
35
|
};
|
|
40
36
|
}
|
|
37
|
+
function createSerializableReadonlyMap(source) {
|
|
38
|
+
const map = new Map(source);
|
|
39
|
+
Object.defineProperty(map, "toJSON", {
|
|
40
|
+
value: () => {
|
|
41
|
+
const record = {};
|
|
42
|
+
for (const [seat, amount] of map.entries()) {
|
|
43
|
+
record[seat] = amount;
|
|
44
|
+
}
|
|
45
|
+
return record;
|
|
46
|
+
},
|
|
47
|
+
enumerable: false,
|
|
48
|
+
});
|
|
49
|
+
return map;
|
|
50
|
+
}
|
|
41
51
|
/**
|
|
42
52
|
* Get visible cards for a player based on shownCards and viewer permissions
|
|
43
53
|
* Returns null (all hidden), full hand, or partial hand with positional context preserved
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pokertools/engine",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "Enterprise-grade Texas Hold'em poker engine",
|
|
5
5
|
"author": "A.Aurelius",
|
|
6
6
|
"license": "MIT",
|
|
@@ -63,14 +63,14 @@
|
|
|
63
63
|
"access": "public"
|
|
64
64
|
},
|
|
65
65
|
"engines": {
|
|
66
|
-
"node": ">=
|
|
66
|
+
"node": ">=24.0.0"
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
|
-
"@pokertools/evaluator": "1.0.
|
|
70
|
-
"@pokertools/types": "1.0.
|
|
69
|
+
"@pokertools/evaluator": "1.0.10",
|
|
70
|
+
"@pokertools/types": "1.0.10"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
73
|
-
"@pokertools/evaluator": "1.0.
|
|
74
|
-
"@pokertools/types": "1.0.
|
|
73
|
+
"@pokertools/evaluator": "1.0.10",
|
|
74
|
+
"@pokertools/types": "1.0.10"
|
|
75
75
|
}
|
|
76
76
|
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ErrorCodes are now managed in @pokertools/types for consistency across packages.
|
|
3
|
-
* This file re-exports them for backwards compatibility.
|
|
4
|
-
*
|
|
5
|
-
* @deprecated Import from "@pokertools/types" instead
|
|
6
|
-
*/
|
|
7
|
-
export { ErrorCodes, type ErrorCode, hasErrorCode } from "@pokertools/types";
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.hasErrorCode = exports.ErrorCodes = void 0;
|
|
4
|
-
/**
|
|
5
|
-
* ErrorCodes are now managed in @pokertools/types for consistency across packages.
|
|
6
|
-
* This file re-exports them for backwards compatibility.
|
|
7
|
-
*
|
|
8
|
-
* @deprecated Import from "@pokertools/types" instead
|
|
9
|
-
*/
|
|
10
|
-
var types_1 = require("@pokertools/types");
|
|
11
|
-
Object.defineProperty(exports, "ErrorCodes", { enumerable: true, get: function () { return types_1.ErrorCodes; } });
|
|
12
|
-
Object.defineProperty(exports, "hasErrorCode", { enumerable: true, get: function () { return types_1.hasErrorCode; } });
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|