@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/actions/betting.js
CHANGED
|
@@ -6,8 +6,8 @@ exports.handleCall = handleCall;
|
|
|
6
6
|
exports.handleBet = handleBet;
|
|
7
7
|
exports.handleRaise = handleRaise;
|
|
8
8
|
const positioning_1 = require("../utils/positioning");
|
|
9
|
-
const
|
|
10
|
-
const
|
|
9
|
+
const action_order_1 = require("../rules/action-order");
|
|
10
|
+
const critical_state_error_1 = require("../errors/critical-state-error");
|
|
11
11
|
const rake_1 = require("../utils/rake");
|
|
12
12
|
/**
|
|
13
13
|
* Handle FOLD action
|
|
@@ -15,18 +15,15 @@ const rake_1 = require("../utils/rake");
|
|
|
15
15
|
function handleFold(state, action) {
|
|
16
16
|
const result = (0, positioning_1.getPlayerById)(state, action.playerId);
|
|
17
17
|
if (!result) {
|
|
18
|
-
return state;
|
|
18
|
+
return state;
|
|
19
19
|
}
|
|
20
20
|
const { seat } = result;
|
|
21
21
|
const newPlayers = [...state.players];
|
|
22
|
-
// Set player status to FOLDED
|
|
23
22
|
newPlayers[seat] = {
|
|
24
23
|
...newPlayers[seat],
|
|
25
24
|
status: "FOLDED" /* PlayerStatus.FOLDED */,
|
|
26
25
|
};
|
|
27
|
-
// Remove from active players
|
|
28
26
|
const newActivePlayers = state.activePlayers.filter((s) => s !== seat);
|
|
29
|
-
// Add to action history
|
|
30
27
|
const actionRecord = {
|
|
31
28
|
action,
|
|
32
29
|
seat,
|
|
@@ -39,21 +36,14 @@ function handleFold(state, action) {
|
|
|
39
36
|
players: newPlayers,
|
|
40
37
|
activePlayers: newActivePlayers,
|
|
41
38
|
actionHistory: [...state.actionHistory, actionRecord],
|
|
42
|
-
timeBankActiveSeat: null,
|
|
39
|
+
timeBankActiveSeat: null,
|
|
43
40
|
timestamp: action.timestamp,
|
|
44
41
|
};
|
|
45
|
-
// Check if only one player with a live hand remains
|
|
46
|
-
// Count players who have not folded (Active + All-In)
|
|
47
42
|
const playersWithLiveHands = currentState.players.filter((p) => p && p.status !== "FOLDED" /* PlayerStatus.FOLDED */);
|
|
48
|
-
// Only end the hand if exactly one player has cards
|
|
49
43
|
if (playersWithLiveHands.length === 1 && playersWithLiveHands[0]) {
|
|
50
|
-
// Award remaining pots to last player with live hand
|
|
51
44
|
return awardPotToLastPlayer(currentState, playersWithLiveHands[0].seat);
|
|
52
45
|
}
|
|
53
|
-
|
|
54
|
-
// the game should naturally progress to Showdown via progressStreet/checkAutoRunout
|
|
55
|
-
// Move to next player
|
|
56
|
-
const nextToAct = (0, actionOrder_1.getNextToAct)(currentState);
|
|
46
|
+
const nextToAct = (0, action_order_1.getNextToAct)(currentState);
|
|
57
47
|
return {
|
|
58
48
|
...currentState,
|
|
59
49
|
actionTo: nextToAct,
|
|
@@ -68,7 +58,6 @@ function handleCheck(state, action) {
|
|
|
68
58
|
return state;
|
|
69
59
|
}
|
|
70
60
|
const { seat } = result;
|
|
71
|
-
// Add to action history
|
|
72
61
|
const actionRecord = {
|
|
73
62
|
action,
|
|
74
63
|
seat,
|
|
@@ -79,11 +68,9 @@ function handleCheck(state, action) {
|
|
|
79
68
|
const newState = {
|
|
80
69
|
...state,
|
|
81
70
|
actionHistory: [...state.actionHistory, actionRecord],
|
|
82
|
-
timeBankActiveSeat: null, // Clear time bank flag on any action
|
|
83
71
|
timestamp: action.timestamp,
|
|
84
72
|
};
|
|
85
|
-
|
|
86
|
-
const nextToAct = (0, actionOrder_1.getNextToAct)(newState);
|
|
73
|
+
const nextToAct = (0, action_order_1.getNextToAct)(newState);
|
|
87
74
|
return {
|
|
88
75
|
...newState,
|
|
89
76
|
actionTo: nextToAct,
|
|
@@ -98,14 +85,11 @@ function handleCall(state, action) {
|
|
|
98
85
|
return state;
|
|
99
86
|
}
|
|
100
87
|
const { player, seat } = result;
|
|
101
|
-
// Calculate amount to call
|
|
102
88
|
const currentBet = getCurrentBet(state);
|
|
103
89
|
const playerBet = state.currentBets.get(seat) ?? 0;
|
|
104
90
|
const toCall = currentBet - playerBet;
|
|
105
|
-
// Determine actual call amount (may be all-in)
|
|
106
91
|
const callAmount = Math.min(toCall, player.stack);
|
|
107
92
|
const isAllIn = callAmount === player.stack;
|
|
108
|
-
// Update player
|
|
109
93
|
const newPlayers = [...state.players];
|
|
110
94
|
newPlayers[seat] = {
|
|
111
95
|
...player,
|
|
@@ -114,12 +98,10 @@ function handleCall(state, action) {
|
|
|
114
98
|
totalInvestedThisHand: player.totalInvestedThisHand + callAmount,
|
|
115
99
|
status: isAllIn ? "ALL_IN" /* PlayerStatus.ALL_IN */ : "ACTIVE" /* PlayerStatus.ACTIVE */,
|
|
116
100
|
};
|
|
117
|
-
// Update current bets
|
|
118
101
|
const newCurrentBets = new Map(state.currentBets);
|
|
119
102
|
newCurrentBets.set(seat, playerBet + callAmount);
|
|
120
|
-
// Add to action history (include actual call amount in the action)
|
|
121
103
|
const actionRecord = {
|
|
122
|
-
action: { ...action, amount: callAmount },
|
|
104
|
+
action: { ...action, amount: callAmount },
|
|
123
105
|
seat,
|
|
124
106
|
resultingPot: getTotalPot(state) + callAmount,
|
|
125
107
|
resultingStack: newPlayers[seat].stack,
|
|
@@ -130,11 +112,9 @@ function handleCall(state, action) {
|
|
|
130
112
|
players: newPlayers,
|
|
131
113
|
currentBets: newCurrentBets,
|
|
132
114
|
actionHistory: [...state.actionHistory, actionRecord],
|
|
133
|
-
timeBankActiveSeat: null, // Clear time bank flag on any action
|
|
134
115
|
timestamp: action.timestamp,
|
|
135
116
|
};
|
|
136
|
-
|
|
137
|
-
const nextToAct = (0, actionOrder_1.getNextToAct)(newState);
|
|
117
|
+
const nextToAct = (0, action_order_1.getNextToAct)(newState);
|
|
138
118
|
return {
|
|
139
119
|
...newState,
|
|
140
120
|
actionTo: nextToAct,
|
|
@@ -151,7 +131,6 @@ function handleBet(state, action) {
|
|
|
151
131
|
const { player, seat } = result;
|
|
152
132
|
const betAmount = Math.min(action.amount, player.stack);
|
|
153
133
|
const isAllIn = betAmount === player.stack;
|
|
154
|
-
// Update player
|
|
155
134
|
const newPlayers = [...state.players];
|
|
156
135
|
newPlayers[seat] = {
|
|
157
136
|
...player,
|
|
@@ -160,10 +139,8 @@ function handleBet(state, action) {
|
|
|
160
139
|
totalInvestedThisHand: player.totalInvestedThisHand + betAmount,
|
|
161
140
|
status: isAllIn ? "ALL_IN" /* PlayerStatus.ALL_IN */ : "ACTIVE" /* PlayerStatus.ACTIVE */,
|
|
162
141
|
};
|
|
163
|
-
// Update current bets
|
|
164
142
|
const newCurrentBets = new Map(state.currentBets);
|
|
165
143
|
newCurrentBets.set(seat, betAmount);
|
|
166
|
-
// Add to action history
|
|
167
144
|
const actionRecord = {
|
|
168
145
|
action,
|
|
169
146
|
seat,
|
|
@@ -175,15 +152,13 @@ function handleBet(state, action) {
|
|
|
175
152
|
...state,
|
|
176
153
|
players: newPlayers,
|
|
177
154
|
currentBets: newCurrentBets,
|
|
178
|
-
minRaise: betAmount + betAmount,
|
|
155
|
+
minRaise: betAmount + betAmount,
|
|
179
156
|
lastRaiseAmount: betAmount,
|
|
180
157
|
lastAggressorSeat: seat,
|
|
181
158
|
actionHistory: [...state.actionHistory, actionRecord],
|
|
182
|
-
timeBankActiveSeat: null, // Clear time bank flag on any action
|
|
183
159
|
timestamp: action.timestamp,
|
|
184
160
|
};
|
|
185
|
-
|
|
186
|
-
const nextToAct = (0, actionOrder_1.getNextToAct)(newState);
|
|
161
|
+
const nextToAct = (0, action_order_1.getNextToAct)(newState);
|
|
187
162
|
return {
|
|
188
163
|
...newState,
|
|
189
164
|
actionTo: nextToAct,
|
|
@@ -203,9 +178,7 @@ function handleRaise(state, action) {
|
|
|
203
178
|
const raiseAmount = Math.min(action.amount, playerBet + player.stack);
|
|
204
179
|
const addedChips = raiseAmount - playerBet;
|
|
205
180
|
const isAllIn = addedChips === player.stack;
|
|
206
|
-
// Calculate raise increment
|
|
207
181
|
const raiseIncrement = raiseAmount - currentBet;
|
|
208
|
-
// Update player
|
|
209
182
|
const newPlayers = [...state.players];
|
|
210
183
|
newPlayers[seat] = {
|
|
211
184
|
...player,
|
|
@@ -214,10 +187,8 @@ function handleRaise(state, action) {
|
|
|
214
187
|
totalInvestedThisHand: player.totalInvestedThisHand + addedChips,
|
|
215
188
|
status: isAllIn ? "ALL_IN" /* PlayerStatus.ALL_IN */ : "ACTIVE" /* PlayerStatus.ACTIVE */,
|
|
216
189
|
};
|
|
217
|
-
// Update current bets
|
|
218
190
|
const newCurrentBets = new Map(state.currentBets);
|
|
219
191
|
newCurrentBets.set(seat, raiseAmount);
|
|
220
|
-
// Add to action history
|
|
221
192
|
const actionRecord = {
|
|
222
193
|
action,
|
|
223
194
|
seat,
|
|
@@ -225,12 +196,9 @@ function handleRaise(state, action) {
|
|
|
225
196
|
resultingStack: newPlayers[seat].stack,
|
|
226
197
|
street: state.street,
|
|
227
198
|
};
|
|
228
|
-
//
|
|
199
|
+
// Incomplete-raise rule: does this raise reopen the betting?
|
|
229
200
|
const reopensBetting = raiseIncrement >= state.lastRaiseAmount;
|
|
230
|
-
//
|
|
231
|
-
// - If reopens betting: new currentBet + new increment
|
|
232
|
-
// - If incomplete raise: new currentBet + old increment (TDA/WSOP rule)
|
|
233
|
-
// Example: P1 bets 100, P2 all-in 120, P3 must raise to 120+100=220 minimum
|
|
201
|
+
// When reopened, min raise = new bet + new increment; otherwise, new bet + old increment.
|
|
234
202
|
const newMinRaise = reopensBetting
|
|
235
203
|
? raiseAmount + raiseIncrement
|
|
236
204
|
: raiseAmount + state.lastRaiseAmount;
|
|
@@ -242,11 +210,9 @@ function handleRaise(state, action) {
|
|
|
242
210
|
lastRaiseAmount: reopensBetting ? raiseIncrement : state.lastRaiseAmount,
|
|
243
211
|
lastAggressorSeat: reopensBetting ? seat : state.lastAggressorSeat,
|
|
244
212
|
actionHistory: [...state.actionHistory, actionRecord],
|
|
245
|
-
timeBankActiveSeat: null, // Clear time bank flag on any action
|
|
246
213
|
timestamp: action.timestamp,
|
|
247
214
|
};
|
|
248
|
-
|
|
249
|
-
const nextToAct = (0, actionOrder_1.getNextToAct)(newState);
|
|
215
|
+
const nextToAct = (0, action_order_1.getNextToAct)(newState);
|
|
250
216
|
return {
|
|
251
217
|
...newState,
|
|
252
218
|
actionTo: nextToAct,
|
|
@@ -278,31 +244,26 @@ function getTotalPot(state) {
|
|
|
278
244
|
return total;
|
|
279
245
|
}
|
|
280
246
|
/**
|
|
281
|
-
* Award pots to remaining eligible players when hand ends by folds
|
|
282
|
-
* Properly handles side pot eligibility and uncalled bets
|
|
247
|
+
* Award pots to remaining eligible players when hand ends by folds.
|
|
283
248
|
*
|
|
284
249
|
* Key principle: Uncalled bets are NOT raked and are returned to the bettor immediately.
|
|
285
|
-
* Only the contested portion
|
|
250
|
+
* Only the contested portion is subject to rake.
|
|
286
251
|
*/
|
|
287
252
|
function awardPotToLastPlayer(state, winningSeat) {
|
|
288
253
|
const newPlayers = [...state.players];
|
|
289
254
|
const newActionHistory = [...state.actionHistory];
|
|
290
255
|
const winners = [];
|
|
291
|
-
// Process each pot separately, checking eligibility
|
|
292
256
|
let totalRakeFromPots = 0;
|
|
293
257
|
for (const pot of state.pots) {
|
|
294
|
-
// Find all non-folded players eligible for this pot
|
|
295
258
|
const eligibleNonFolded = pot.eligibleSeats.filter((seat) => {
|
|
296
259
|
const player = state.players[seat];
|
|
297
260
|
return player && player.status !== "FOLDED" /* PlayerStatus.FOLDED */;
|
|
298
261
|
});
|
|
299
|
-
// Calculate rake for this pot - GLOBAL cap applied across all pots
|
|
300
262
|
const { rake: potRake } = (0, rake_1.calculateRake)(state, pot.amount, totalRakeFromPots);
|
|
301
263
|
totalRakeFromPots += potRake;
|
|
302
264
|
const potAfterRake = pot.amount - potRake;
|
|
303
265
|
if (eligibleNonFolded.length === 0) {
|
|
304
|
-
//
|
|
305
|
-
// Award to last player to fold from eligible seats (fallback)
|
|
266
|
+
// Defensive fallback: no eligible players remain.
|
|
306
267
|
const lastEligible = pot.eligibleSeats[pot.eligibleSeats.length - 1];
|
|
307
268
|
const player = newPlayers[lastEligible];
|
|
308
269
|
if (player) {
|
|
@@ -319,7 +280,6 @@ function awardPotToLastPlayer(state, winningSeat) {
|
|
|
319
280
|
}
|
|
320
281
|
}
|
|
321
282
|
else if (eligibleNonFolded.length === 1) {
|
|
322
|
-
// Exactly one eligible player - they win this pot
|
|
323
283
|
const winnerSeat = eligibleNonFolded[0];
|
|
324
284
|
const player = newPlayers[winnerSeat];
|
|
325
285
|
newPlayers[winnerSeat] = {
|
|
@@ -334,9 +294,7 @@ function awardPotToLastPlayer(state, winningSeat) {
|
|
|
334
294
|
});
|
|
335
295
|
}
|
|
336
296
|
else {
|
|
337
|
-
|
|
338
|
-
// The hand should have gone to showdown instead
|
|
339
|
-
throw new CriticalStateError_1.CriticalStateError("awardPotToLastPlayer called with multiple eligible players remaining", {
|
|
297
|
+
throw new critical_state_error_1.CriticalStateError("awardPotToLastPlayer called with multiple eligible players remaining", {
|
|
340
298
|
potAmount: pot.amount,
|
|
341
299
|
eligibleSeats: pot.eligibleSeats,
|
|
342
300
|
eligibleNonFolded,
|
|
@@ -344,28 +302,25 @@ function awardPotToLastPlayer(state, winningSeat) {
|
|
|
344
302
|
});
|
|
345
303
|
}
|
|
346
304
|
}
|
|
347
|
-
//
|
|
305
|
+
// Uncalled bet logic: three-step process.
|
|
348
306
|
if (state.currentBets.size > 0) {
|
|
349
307
|
const winnersBet = state.currentBets.get(winningSeat) ?? 0;
|
|
350
|
-
//
|
|
351
|
-
// This determines how much of the winner's bet was actually "called"
|
|
308
|
+
// Second-highest bet determines how much of winner's bet was actually "called".
|
|
352
309
|
let maxOpponentBet = 0;
|
|
353
310
|
for (const [seat, amount] of state.currentBets.entries()) {
|
|
354
311
|
if (seat !== winningSeat && amount > maxOpponentBet) {
|
|
355
312
|
maxOpponentBet = amount;
|
|
356
313
|
}
|
|
357
314
|
}
|
|
358
|
-
// Calculate uncalled and called portions
|
|
359
315
|
const uncalledAmount = winnersBet > maxOpponentBet ? winnersBet - maxOpponentBet : 0;
|
|
360
316
|
const calledPortion = winnersBet > maxOpponentBet ? maxOpponentBet : winnersBet;
|
|
361
|
-
// Step 1: Return uncalled bet immediately (NO RAKE on uncalled bets)
|
|
317
|
+
// Step 1: Return uncalled bet immediately (NO RAKE on uncalled bets).
|
|
362
318
|
if (uncalledAmount > 0) {
|
|
363
319
|
const player = newPlayers[winningSeat];
|
|
364
320
|
newPlayers[winningSeat] = {
|
|
365
321
|
...player,
|
|
366
322
|
stack: player.stack + uncalledAmount,
|
|
367
323
|
};
|
|
368
|
-
// Record the uncalled bet return
|
|
369
324
|
newActionHistory.push({
|
|
370
325
|
action: {
|
|
371
326
|
type: "UNCALLED_BET_RETURNED" /* ActionType.UNCALLED_BET_RETURNED */,
|
|
@@ -379,14 +334,14 @@ function awardPotToLastPlayer(state, winningSeat) {
|
|
|
379
334
|
street: state.street,
|
|
380
335
|
});
|
|
381
336
|
}
|
|
382
|
-
// Step 2:
|
|
337
|
+
// Step 2: Contested pot = winner's called portion + all opponent bets.
|
|
383
338
|
let contestedPot = calledPortion;
|
|
384
339
|
for (const [seat, amount] of state.currentBets.entries()) {
|
|
385
340
|
if (seat !== winningSeat) {
|
|
386
341
|
contestedPot += amount;
|
|
387
342
|
}
|
|
388
343
|
}
|
|
389
|
-
// Step 3: Rake and award the contested portion only
|
|
344
|
+
// Step 3: Rake and award the contested portion only.
|
|
390
345
|
if (contestedPot > 0) {
|
|
391
346
|
const { rake } = (0, rake_1.calculateRake)(state, contestedPot, totalRakeFromPots);
|
|
392
347
|
const totalRake = totalRakeFromPots + rake;
|
|
@@ -396,8 +351,6 @@ function awardPotToLastPlayer(state, winningSeat) {
|
|
|
396
351
|
...player,
|
|
397
352
|
stack: player.stack + winnings,
|
|
398
353
|
};
|
|
399
|
-
// Update winners array
|
|
400
|
-
// Only count actual winnings (contested pot after rake, minus winner's own contribution)
|
|
401
354
|
const actualWinnings = winnings - calledPortion;
|
|
402
355
|
if (actualWinnings > 0) {
|
|
403
356
|
const existingIndex = winners.findIndex((w) => w.seat === winningSeat);
|
|
@@ -416,16 +369,13 @@ function awardPotToLastPlayer(state, winningSeat) {
|
|
|
416
369
|
});
|
|
417
370
|
}
|
|
418
371
|
}
|
|
419
|
-
// Update total rake for the hand
|
|
420
372
|
totalRakeFromPots = totalRake;
|
|
421
373
|
}
|
|
422
374
|
}
|
|
423
|
-
// NOTE: We do NOT reset totalInvestedThisHand here because it's used by getInitialChips()
|
|
424
|
-
// to calculate total chips in the game. It will be reset when a new hand is dealt.
|
|
425
375
|
return {
|
|
426
376
|
...state,
|
|
427
377
|
players: newPlayers,
|
|
428
|
-
street: "SHOWDOWN" /* Street.SHOWDOWN */,
|
|
378
|
+
street: "SHOWDOWN" /* Street.SHOWDOWN */,
|
|
429
379
|
pots: [],
|
|
430
380
|
currentBets: new Map(),
|
|
431
381
|
winners,
|
package/dist/actions/dealing.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.handleDeal = handleDeal;
|
|
4
4
|
const deck_1 = require("../utils/deck");
|
|
5
|
-
const
|
|
5
|
+
const card_utils_1 = require("../utils/card-utils");
|
|
6
6
|
const blinds_1 = require("../rules/blinds");
|
|
7
|
-
const
|
|
7
|
+
const action_order_1 = require("../rules/action-order");
|
|
8
8
|
const positioning_1 = require("../utils/positioning");
|
|
9
9
|
/**
|
|
10
10
|
* Deal a new hand
|
|
@@ -14,66 +14,56 @@ const positioning_1 = require("../utils/positioning");
|
|
|
14
14
|
* - Sets action to first to act
|
|
15
15
|
*/
|
|
16
16
|
function handleDeal(state, action) {
|
|
17
|
-
//
|
|
17
|
+
// Dead Button rule: advances to next seat index regardless of occupancy.
|
|
18
18
|
let newButtonSeat = moveButton(state);
|
|
19
|
-
// Determine if this is a tournament
|
|
20
19
|
const isTournament = !!state.config.blindStructure;
|
|
21
20
|
const isClient = !!state.config.isClient;
|
|
22
|
-
//
|
|
23
|
-
// In client mode, we use an empty deck and deal masked cards
|
|
21
|
+
// Client mode: empty deck, cards dealt as masked (null).
|
|
24
22
|
const rng = state.config.randomProvider ?? Math.random;
|
|
25
23
|
const deck = isClient ? [] : (0, deck_1.shuffle)((0, deck_1.createDeck)(), rng);
|
|
26
|
-
// Create a copy of timeBanks to modify
|
|
27
24
|
const newTimeBanks = new Map(state.timeBanks);
|
|
28
|
-
//
|
|
25
|
+
// Merge pending add-ons; expire stale reservations.
|
|
29
26
|
const newPlayers = state.players.map((player) => {
|
|
30
27
|
if (!player)
|
|
31
28
|
return null;
|
|
32
|
-
// Skip reserved players (they haven't confirmed yet)
|
|
33
29
|
if (player.status === "RESERVED" /* PlayerStatus.RESERVED */) {
|
|
34
|
-
// Check if reservation has expired
|
|
35
30
|
if (player.reservationExpiry && action.timestamp >= player.reservationExpiry) {
|
|
36
|
-
// Reservation expired, remove player
|
|
37
31
|
newTimeBanks.delete(player.seat);
|
|
38
32
|
return null;
|
|
39
33
|
}
|
|
40
|
-
// Keep reserved player as-is
|
|
41
34
|
return player;
|
|
42
35
|
}
|
|
43
|
-
// Merge pendingAddOn into stack
|
|
44
36
|
const newStack = player.stack + player.pendingAddOn;
|
|
45
37
|
return {
|
|
46
38
|
...player,
|
|
47
39
|
stack: newStack,
|
|
48
|
-
pendingAddOn: 0,
|
|
40
|
+
pendingAddOn: 0,
|
|
49
41
|
};
|
|
50
42
|
});
|
|
51
43
|
newButtonSeat = moveHeadsUpButtonToOccupiedSeat(newButtonSeat, {
|
|
52
44
|
...state,
|
|
53
45
|
players: newPlayers,
|
|
54
46
|
});
|
|
55
|
-
//
|
|
47
|
+
// Recompute blind positions with updated button/players.
|
|
56
48
|
const blindPositions = (0, blinds_1.getBlindPositions)({
|
|
57
49
|
...state,
|
|
58
50
|
buttonSeat: newButtonSeat,
|
|
59
51
|
players: newPlayers,
|
|
60
52
|
});
|
|
61
|
-
//
|
|
53
|
+
// Determine which players receive cards.
|
|
62
54
|
const playersToReceive = [];
|
|
63
55
|
for (let seat = 0; seat < newPlayers.length; seat++) {
|
|
64
56
|
const player = newPlayers[seat];
|
|
65
|
-
// Basic eligibility checks
|
|
66
57
|
if (!player || player.stack <= 0 || player.status === "RESERVED" /* PlayerStatus.RESERVED */) {
|
|
67
58
|
continue;
|
|
68
59
|
}
|
|
69
|
-
//
|
|
70
|
-
//
|
|
60
|
+
// WAIT_FOR_BB check: sit player IN when they reach the Big Blind,
|
|
61
|
+
// sit player OUT when they haven't reached it yet.
|
|
71
62
|
let shouldPlay = true;
|
|
72
63
|
if (!isTournament && player.sitInOption === "WAIT_FOR_BB" /* SitInOption.WAIT_FOR_BB */) {
|
|
73
64
|
const isInBigBlind = blindPositions?.bigBlindSeat === seat;
|
|
74
65
|
if (isInBigBlind) {
|
|
75
|
-
// PLAYER RE-ENTRY:
|
|
76
|
-
// We must update the player object in newPlayers to reflect they are back.
|
|
66
|
+
// PLAYER RE-ENTRY: Force them active when they are in the Big Blind.
|
|
77
67
|
newPlayers[seat] = {
|
|
78
68
|
...player,
|
|
79
69
|
isSittingOut: false,
|
|
@@ -81,8 +71,7 @@ function handleDeal(state, action) {
|
|
|
81
71
|
shouldPlay = true;
|
|
82
72
|
}
|
|
83
73
|
else {
|
|
84
|
-
// Not in BB
|
|
85
|
-
// Only update if not already sitting out to avoid object churn
|
|
74
|
+
// Not yet in BB; force sit-out (avoids object churn if already sitting out).
|
|
86
75
|
if (!player.isSittingOut) {
|
|
87
76
|
newPlayers[seat] = {
|
|
88
77
|
...player,
|
|
@@ -93,67 +82,60 @@ function handleDeal(state, action) {
|
|
|
93
82
|
}
|
|
94
83
|
}
|
|
95
84
|
else if (player.isSittingOut) {
|
|
96
|
-
// Standard sitting out check
|
|
97
85
|
shouldPlay = false;
|
|
98
86
|
}
|
|
99
87
|
if (shouldPlay) {
|
|
100
88
|
playersToReceive.push(seat);
|
|
101
89
|
}
|
|
102
90
|
}
|
|
103
|
-
// Deal 2 cards to each player
|
|
104
91
|
let remainingDeck = deck;
|
|
105
|
-
// Initialize hands for receiving players (active, not sitting out)
|
|
106
92
|
for (const seat of playersToReceive) {
|
|
107
93
|
newPlayers[seat] = {
|
|
108
94
|
...newPlayers[seat],
|
|
109
|
-
hand: [],
|
|
110
|
-
shownCards: null,
|
|
95
|
+
hand: [],
|
|
96
|
+
shownCards: null,
|
|
111
97
|
status: "ACTIVE" /* PlayerStatus.ACTIVE */,
|
|
112
98
|
betThisStreet: 0,
|
|
113
99
|
totalInvestedThisHand: 0,
|
|
114
100
|
};
|
|
115
101
|
}
|
|
116
|
-
//
|
|
102
|
+
// Tournament: sitting-out players initialized as FOLDED (must post blinds/antes).
|
|
117
103
|
if (isTournament) {
|
|
118
104
|
for (let seat = 0; seat < newPlayers.length; seat++) {
|
|
119
105
|
const player = newPlayers[seat];
|
|
120
106
|
if (player && player.stack > 0 && player.isSittingOut && !playersToReceive.includes(seat)) {
|
|
121
107
|
newPlayers[seat] = {
|
|
122
108
|
...player,
|
|
123
|
-
hand: null,
|
|
109
|
+
hand: null,
|
|
124
110
|
shownCards: null,
|
|
125
|
-
status: "FOLDED" /* PlayerStatus.FOLDED */,
|
|
111
|
+
status: "FOLDED" /* PlayerStatus.FOLDED */,
|
|
126
112
|
betThisStreet: 0,
|
|
127
113
|
totalInvestedThisHand: 0,
|
|
128
114
|
};
|
|
129
115
|
}
|
|
130
116
|
}
|
|
131
117
|
}
|
|
132
|
-
// Deal 2 cards, one
|
|
118
|
+
// Deal 2 hole cards, one at a time, clockwise.
|
|
133
119
|
for (let round = 0; round < 2; round++) {
|
|
134
120
|
for (const seat of playersToReceive) {
|
|
135
121
|
let cardStrings;
|
|
136
122
|
if (isClient) {
|
|
137
|
-
// Client mode: Deal masked cards
|
|
138
123
|
cardStrings = [null];
|
|
139
124
|
}
|
|
140
125
|
else {
|
|
141
|
-
// Server mode: Deal from deck
|
|
142
126
|
const [cards, nextDeck] = (0, deck_1.dealCards)(remainingDeck, 1);
|
|
143
127
|
remainingDeck = nextDeck;
|
|
144
|
-
cardStrings = (0,
|
|
128
|
+
cardStrings = (0, card_utils_1.cardCodesToStrings)(cards);
|
|
145
129
|
}
|
|
146
|
-
// Append to existing hand
|
|
147
130
|
const currentPlayer = newPlayers[seat];
|
|
148
|
-
const currentHand = currentPlayer.hand ?? [];
|
|
131
|
+
const currentHand = currentPlayer.hand ?? [];
|
|
149
132
|
newPlayers[seat] = {
|
|
150
133
|
...currentPlayer,
|
|
151
134
|
hand: [...currentHand, ...cardStrings],
|
|
152
135
|
};
|
|
153
136
|
}
|
|
154
137
|
}
|
|
155
|
-
// Post blinds and antes
|
|
156
|
-
// Recalculate blind positions with the new button seat
|
|
138
|
+
// Post blinds and antes.
|
|
157
139
|
const finalBlindPositions = (0, blinds_1.getBlindPositions)({
|
|
158
140
|
...state,
|
|
159
141
|
buttonSeat: newButtonSeat,
|
|
@@ -162,9 +144,6 @@ function handleDeal(state, action) {
|
|
|
162
144
|
const currentBets = new Map();
|
|
163
145
|
if (finalBlindPositions) {
|
|
164
146
|
const { smallBlindSeat, bigBlindSeat } = finalBlindPositions;
|
|
165
|
-
// Post small blind
|
|
166
|
-
// In tournaments: sitting-out players MUST post to prevent "blinding off" exploit
|
|
167
|
-
// In cash games: sitting-out SB is treated as "Dead Small Blind" (no post)
|
|
168
147
|
const sbPlayer = newPlayers[smallBlindSeat];
|
|
169
148
|
if (sbPlayer && sbPlayer.stack > 0) {
|
|
170
149
|
const shouldPostSB = isTournament || !sbPlayer.isSittingOut;
|
|
@@ -184,12 +163,11 @@ function handleDeal(state, action) {
|
|
|
184
163
|
};
|
|
185
164
|
}
|
|
186
165
|
}
|
|
187
|
-
//
|
|
188
|
-
// Post big blind (Must exist for hand to start)
|
|
166
|
+
// Dead Small Blind: sbPlayer is null or (cash game && sitting out).
|
|
189
167
|
const bbPlayer = newPlayers[bigBlindSeat];
|
|
190
168
|
if (bbPlayer) {
|
|
191
|
-
// In
|
|
192
|
-
// In
|
|
169
|
+
// In cash games sitting-out players are skipped by getBlindPositions.
|
|
170
|
+
// In tournaments sitting-out players MUST post blinds (anti-blinding-off).
|
|
193
171
|
const shouldPostBB = isTournament || !bbPlayer.isSittingOut;
|
|
194
172
|
if (shouldPostBB) {
|
|
195
173
|
const bbAmount = Math.min(bbPlayer.stack, state.bigBlind);
|
|
@@ -208,9 +186,7 @@ function handleDeal(state, action) {
|
|
|
208
186
|
}
|
|
209
187
|
}
|
|
210
188
|
}
|
|
211
|
-
//
|
|
212
|
-
// In tournaments: ALL players with chips must post (including sitting-out)
|
|
213
|
-
// In cash games: Only active players post
|
|
189
|
+
// Antes: tournament — all players with chips (incl. sitting-out); cash — active only.
|
|
214
190
|
if (state.ante > 0) {
|
|
215
191
|
const playersToAnteFrom = isTournament
|
|
216
192
|
? state.players.map((p, idx) => (p && p.stack > 0 ? idx : -1)).filter((idx) => idx >= 0)
|
|
@@ -236,9 +212,7 @@ function handleDeal(state, action) {
|
|
|
236
212
|
}
|
|
237
213
|
}
|
|
238
214
|
}
|
|
239
|
-
// Start with empty pots (bets will be collected when street progresses)
|
|
240
215
|
const pots = [];
|
|
241
|
-
// Get active players
|
|
242
216
|
const activePlayers = playersToReceive.filter((seat) => {
|
|
243
217
|
const player = newPlayers[seat];
|
|
244
218
|
return player.status === "ACTIVE" /* PlayerStatus.ACTIVE */;
|
|
@@ -254,6 +228,8 @@ function handleDeal(state, action) {
|
|
|
254
228
|
players: newPlayers,
|
|
255
229
|
pots,
|
|
256
230
|
currentBets,
|
|
231
|
+
initialChips: newPlayers.reduce((sum, player) => sum + (player ? player.stack : 0), 0) +
|
|
232
|
+
Array.from(currentBets.values()).reduce((sum, amount) => sum + amount, 0),
|
|
257
233
|
minRaise: state.bigBlind,
|
|
258
234
|
lastRaiseAmount: state.bigBlind,
|
|
259
235
|
activePlayers,
|
|
@@ -262,8 +238,7 @@ function handleDeal(state, action) {
|
|
|
262
238
|
actionHistory: [],
|
|
263
239
|
timestamp: action.timestamp,
|
|
264
240
|
};
|
|
265
|
-
|
|
266
|
-
const firstToAct = (0, actionOrder_1.getFirstToAct)(newState);
|
|
241
|
+
const firstToAct = (0, action_order_1.getFirstToAct)(newState);
|
|
267
242
|
return {
|
|
268
243
|
...newState,
|
|
269
244
|
actionTo: firstToAct,
|
|
@@ -275,7 +250,6 @@ function handleDeal(state, action) {
|
|
|
275
250
|
*/
|
|
276
251
|
function moveButton(state) {
|
|
277
252
|
if (state.buttonSeat === null) {
|
|
278
|
-
// First hand, find first seated player
|
|
279
253
|
for (let seat = 0; seat < state.maxPlayers; seat++) {
|
|
280
254
|
if (state.players[seat] !== null) {
|
|
281
255
|
return seat;
|
|
@@ -283,8 +257,7 @@ function moveButton(state) {
|
|
|
283
257
|
}
|
|
284
258
|
return 0;
|
|
285
259
|
}
|
|
286
|
-
//
|
|
287
|
-
// We do not skip empty seats here.
|
|
260
|
+
// Dead Button: advance to next seat index regardless of occupancy.
|
|
288
261
|
return (0, positioning_1.getNextSeat)(state.buttonSeat, state.maxPlayers);
|
|
289
262
|
}
|
|
290
263
|
function moveHeadsUpButtonToOccupiedSeat(buttonSeat, state) {
|
|
@@ -28,7 +28,6 @@ function handleSit(state, action) {
|
|
|
28
28
|
};
|
|
29
29
|
const newPlayers = [...state.players];
|
|
30
30
|
newPlayers[action.seat] = newPlayer;
|
|
31
|
-
// Add to time banks
|
|
32
31
|
const newTimeBanks = new Map(state.timeBanks);
|
|
33
32
|
newTimeBanks.set(action.seat, newPlayer.timeBank);
|
|
34
33
|
return {
|
|
@@ -65,6 +64,8 @@ function handleStand(state, action) {
|
|
|
65
64
|
// advanced to the next player. The invariant check will now pass.
|
|
66
65
|
}
|
|
67
66
|
// 2. Remove player from table (Standard Stand Logic)
|
|
67
|
+
const chipsLeavingTable = currentState.players[seat]?.stack ?? 0;
|
|
68
|
+
const currentBaseline = currentState.initialChips;
|
|
68
69
|
const newPlayers = [...currentState.players];
|
|
69
70
|
newPlayers[seat] = null;
|
|
70
71
|
// Remove from time banks
|
|
@@ -79,6 +80,7 @@ function handleStand(state, action) {
|
|
|
79
80
|
players: newPlayers,
|
|
80
81
|
activePlayers: newActivePlayers,
|
|
81
82
|
timeBanks: newTimeBanks,
|
|
83
|
+
initialChips: typeof currentBaseline === "number" ? currentBaseline - chipsLeavingTable : undefined,
|
|
82
84
|
timestamp: action.timestamp,
|
|
83
85
|
};
|
|
84
86
|
}
|