@pokertools/engine 1.0.7 → 1.0.9
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/dist/.tsbuildinfo +1 -1
- package/dist/actions/betting.js +8 -8
- package/dist/actions/dealing.js +6 -4
- package/dist/actions/management.js +3 -0
- package/dist/actions/{showdownActions.js → showdown-actions.js} +10 -10
- package/dist/actions/special.js +20 -3
- package/dist/actions/{streetProgression.js → street-progression.js} +7 -7
- package/dist/actions/validation.js +48 -27
- package/dist/browser.d.ts +5 -5
- package/dist/browser.js +6 -6
- package/dist/engine/{gameReducer.js → game-reducer.js} +11 -8
- package/dist/engine/{PokerEngine.js → poker-engine.js} +10 -10
- 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 -4
- package/dist/history/exporter.js +4 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -4
- package/dist/rules/{actionOrder.js → action-order.js} +10 -10
- package/dist/rules/blinds.js +2 -2
- package/dist/rules/showdown.js +34 -1
- package/dist/rules/{sidePots.js → side-pots.js} +2 -2
- package/dist/utils/invariants.js +16 -10
- package/dist/utils/serialization.d.ts +1 -0
- package/dist/utils/serialization.js +9 -3
- package/dist/utils/validation.js +8 -8
- package/dist/utils/{viewMasking.js → view-masking.js} +16 -6
- package/package.json +5 -5
- /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/{ErrorCodes.d.ts → error-codes.d.ts} +0 -0
- /package/dist/errors/{ErrorCodes.js → error-codes.js} +0 -0
- /package/dist/errors/{PokerEngineError.d.ts → poker-engine-error.d.ts} +0 -0
- /package/dist/errors/{PokerEngineError.js → poker-engine-error.js} +0 -0
- /package/dist/history/{handHistoryBuilder.d.ts → hand-history-builder.d.ts} +0 -0
- /package/dist/history/{handHistoryBuilder.js → hand-history-builder.js} +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/{headsUp.js → heads-up.js} +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
|
@@ -4,7 +4,7 @@ exports.getNextToAct = getNextToAct;
|
|
|
4
4
|
exports.getFirstToAct = getFirstToAct;
|
|
5
5
|
exports.isActionComplete = isActionComplete;
|
|
6
6
|
const positioning_1 = require("../utils/positioning");
|
|
7
|
-
const
|
|
7
|
+
const heads_up_1 = require("./heads-up");
|
|
8
8
|
const blinds_1 = require("./blinds");
|
|
9
9
|
/**
|
|
10
10
|
* Determine the next player to act
|
|
@@ -12,7 +12,7 @@ const blinds_1 = require("./blinds");
|
|
|
12
12
|
*/
|
|
13
13
|
function getNextToAct(state) {
|
|
14
14
|
// Special case: heads-up has different rules
|
|
15
|
-
if ((0,
|
|
15
|
+
if ((0, heads_up_1.isHeadsUp)(state)) {
|
|
16
16
|
return getNextToActHeadsUp(state);
|
|
17
17
|
}
|
|
18
18
|
return getNextToActNormal(state);
|
|
@@ -32,8 +32,8 @@ function getNextToActNormal(state) {
|
|
|
32
32
|
// Search for next player who can act
|
|
33
33
|
while (seat !== startSeat) {
|
|
34
34
|
const player = state.players[seat];
|
|
35
|
-
// Skip if: no player, folded, all-in, or
|
|
36
|
-
if (player?.status !== "ACTIVE" /* PlayerStatus.ACTIVE */ || player.stack === 0) {
|
|
35
|
+
// Skip if: no player, folded, all-in, busted, or sitting out
|
|
36
|
+
if (player?.status !== "ACTIVE" /* PlayerStatus.ACTIVE */ || player.stack === 0 || player.isSittingOut) {
|
|
37
37
|
seat = (0, positioning_1.getNextSeat)(seat, state.maxPlayers);
|
|
38
38
|
continue;
|
|
39
39
|
}
|
|
@@ -65,7 +65,7 @@ function getNextToActHeadsUp(state) {
|
|
|
65
65
|
if (state.buttonSeat === null) {
|
|
66
66
|
return null;
|
|
67
67
|
}
|
|
68
|
-
const actionOrder = (0,
|
|
68
|
+
const actionOrder = (0, heads_up_1.getHeadsUpActionOrder)(state, state.street);
|
|
69
69
|
if (actionOrder.length === 0) {
|
|
70
70
|
return null;
|
|
71
71
|
}
|
|
@@ -77,7 +77,7 @@ function getNextToActHeadsUp(state) {
|
|
|
77
77
|
// Check both players
|
|
78
78
|
for (const seat of actionOrder) {
|
|
79
79
|
const player = state.players[seat];
|
|
80
|
-
if (player?.status !== "ACTIVE" /* PlayerStatus.ACTIVE */ || player.stack === 0) {
|
|
80
|
+
if (player?.status !== "ACTIVE" /* PlayerStatus.ACTIVE */ || player.stack === 0 || player.isSittingOut) {
|
|
81
81
|
continue;
|
|
82
82
|
}
|
|
83
83
|
const playerBet = state.currentBets.get(seat) ?? 0;
|
|
@@ -94,8 +94,8 @@ function getFirstToAct(state) {
|
|
|
94
94
|
if (state.buttonSeat === null) {
|
|
95
95
|
return null;
|
|
96
96
|
}
|
|
97
|
-
if ((0,
|
|
98
|
-
const order = (0,
|
|
97
|
+
if ((0, heads_up_1.isHeadsUp)(state)) {
|
|
98
|
+
const order = (0, heads_up_1.getHeadsUpActionOrder)(state, state.street);
|
|
99
99
|
if (order.length > 0) {
|
|
100
100
|
return order[0];
|
|
101
101
|
}
|
|
@@ -128,7 +128,7 @@ function getNextActionableSeat(startSeat, state) {
|
|
|
128
128
|
// Scan full circle
|
|
129
129
|
while (seat !== endSeat) {
|
|
130
130
|
const player = state.players[seat];
|
|
131
|
-
if (player?.status === "ACTIVE" /* PlayerStatus.ACTIVE */ && player.stack > 0) {
|
|
131
|
+
if (player?.status === "ACTIVE" /* PlayerStatus.ACTIVE */ && player.stack > 0 && !player.isSittingOut) {
|
|
132
132
|
return seat;
|
|
133
133
|
}
|
|
134
134
|
seat = (0, positioning_1.getNextSeat)(seat, state.maxPlayers);
|
|
@@ -159,7 +159,7 @@ function isActionComplete(state) {
|
|
|
159
159
|
if (!player)
|
|
160
160
|
continue;
|
|
161
161
|
// Count active players who can still act
|
|
162
|
-
if (player.status === "ACTIVE" /* PlayerStatus.ACTIVE */ && player.stack > 0) {
|
|
162
|
+
if (player.status === "ACTIVE" /* PlayerStatus.ACTIVE */ && player.stack > 0 && !player.isSittingOut) {
|
|
163
163
|
activeCount++;
|
|
164
164
|
const playerBet = state.currentBets.get(seat) ?? 0;
|
|
165
165
|
// Player has matched bet and acted
|
package/dist/rules/blinds.js
CHANGED
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.getBlindPositions = getBlindPositions;
|
|
4
4
|
exports.calculateAntes = calculateAntes;
|
|
5
5
|
const positioning_1 = require("../utils/positioning");
|
|
6
|
-
const
|
|
6
|
+
const heads_up_1 = require("./heads-up");
|
|
7
7
|
/**
|
|
8
8
|
* Determine which seats should post blinds
|
|
9
9
|
*
|
|
@@ -24,7 +24,7 @@ function getBlindPositions(state) {
|
|
|
24
24
|
const buttonSeat = state.buttonSeat;
|
|
25
25
|
const isTournament = !!state.config.blindStructure;
|
|
26
26
|
// Heads-up specific logic (Button is SB)
|
|
27
|
-
if ((0,
|
|
27
|
+
if ((0, heads_up_1.isHeadsUp)(state)) {
|
|
28
28
|
const bbSeat = getNextActiveOrOccupiedSeat(buttonSeat, state.players, state.maxPlayers, isTournament);
|
|
29
29
|
if (bbSeat === null) {
|
|
30
30
|
return null;
|
package/dist/rules/showdown.js
CHANGED
|
@@ -143,7 +143,7 @@ function evaluatePot(state, pot) {
|
|
|
143
143
|
evaluations.push({
|
|
144
144
|
seat: player.seat,
|
|
145
145
|
score,
|
|
146
|
-
hand:
|
|
146
|
+
hand: getBestFiveCardHand(allCards),
|
|
147
147
|
description,
|
|
148
148
|
});
|
|
149
149
|
}
|
|
@@ -155,6 +155,39 @@ function evaluatePot(state, pot) {
|
|
|
155
155
|
const winners = evaluations.filter((e) => e.score === bestScore);
|
|
156
156
|
return winners;
|
|
157
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Return the concrete five cards that produce the best evaluator score.
|
|
160
|
+
* The evaluator score is lower-is-better, so enumerate all 5-card subsets and
|
|
161
|
+
* keep the first subset with the minimum score. Hold'em has at most 7 cards,
|
|
162
|
+
* making this deterministic brute-force path only 21 evaluations per player.
|
|
163
|
+
*/
|
|
164
|
+
function getBestFiveCardHand(cards) {
|
|
165
|
+
if (cards.length === 5) {
|
|
166
|
+
return [...cards];
|
|
167
|
+
}
|
|
168
|
+
let bestHand = null;
|
|
169
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
170
|
+
for (let a = 0; a < cards.length - 4; a++) {
|
|
171
|
+
for (let b = a + 1; b < cards.length - 3; b++) {
|
|
172
|
+
for (let c = b + 1; c < cards.length - 2; c++) {
|
|
173
|
+
for (let d = c + 1; d < cards.length - 1; d++) {
|
|
174
|
+
for (let e = d + 1; e < cards.length; e++) {
|
|
175
|
+
const candidate = [cards[a], cards[b], cards[c], cards[d], cards[e]];
|
|
176
|
+
const score = (0, evaluator_1.evaluate)((0, evaluator_1.getCardCodes)(candidate));
|
|
177
|
+
if (score < bestScore) {
|
|
178
|
+
bestScore = score;
|
|
179
|
+
bestHand = candidate;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (bestHand === null) {
|
|
187
|
+
return [...cards.slice(0, 5)];
|
|
188
|
+
}
|
|
189
|
+
return bestHand;
|
|
190
|
+
}
|
|
158
191
|
/**
|
|
159
192
|
* Check if hand should go to showdown
|
|
160
193
|
*/
|
|
@@ -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
|
*
|
|
@@ -57,7 +57,7 @@ function calculateSidePots(state) {
|
|
|
57
57
|
// Must have at least one eligible player
|
|
58
58
|
// If everyone folded at this level, something went wrong in the game logic
|
|
59
59
|
if (eligibleSeats.length === 0) {
|
|
60
|
-
throw new
|
|
60
|
+
throw new critical_state_error_1.CriticalStateError(`Side pot has no eligible players - all ${allAtThisLevel.length} players at this level have folded`, {
|
|
61
61
|
potAmount,
|
|
62
62
|
potLevel: i,
|
|
63
63
|
investmentLevel: current.amount,
|
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
|
});
|
|
@@ -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
|
};
|
|
@@ -72,7 +77,8 @@ function restoreFromSnapshot(snapshot) {
|
|
|
72
77
|
timeBanks,
|
|
73
78
|
timeBankActiveSeat: snapshot.timeBankActiveSeat ?? null, // Backward compatibility
|
|
74
79
|
previousStates,
|
|
75
|
-
|
|
80
|
+
initialChips: snapshot.initialChips,
|
|
81
|
+
rakeThisHand: snapshot.rakeThisHand ?? 0, // Add missing field with default
|
|
76
82
|
};
|
|
77
83
|
}
|
|
78
84
|
/**
|
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 error_codes_1 = require("../errors/error-codes");
|
|
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(error_codes_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(error_codes_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(error_codes_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(error_codes_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(error_codes_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(error_codes_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.9",
|
|
4
4
|
"description": "Enterprise-grade Texas Hold'em poker engine",
|
|
5
5
|
"author": "A.Aurelius",
|
|
6
6
|
"license": "MIT",
|
|
@@ -66,11 +66,11 @@
|
|
|
66
66
|
"node": ">=18.0.0"
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
|
-
"@pokertools/evaluator": "1.0.
|
|
70
|
-
"@pokertools/types": "1.0.
|
|
69
|
+
"@pokertools/evaluator": "1.0.9",
|
|
70
|
+
"@pokertools/types": "1.0.9"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
73
|
-
"@pokertools/evaluator": "1.0.
|
|
74
|
-
"@pokertools/types": "1.0.
|
|
73
|
+
"@pokertools/evaluator": "1.0.9",
|
|
74
|
+
"@pokertools/types": "1.0.9"
|
|
75
75
|
}
|
|
76
76
|
}
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|