@pokertools/engine 1.0.9 → 1.0.11
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 +16 -68
- package/dist/actions/dealing.js +24 -52
- package/dist/actions/management.js +0 -1
- package/dist/actions/showdown-actions.js +12 -26
- package/dist/actions/special.js +0 -4
- package/dist/actions/street-progression.js +9 -24
- package/dist/actions/tournament.js +1 -4
- package/dist/actions/validation.js +30 -35
- package/dist/browser.js +0 -1
- package/dist/engine/game-reducer.js +5 -24
- package/dist/engine/poker-engine.js +6 -20
- package/dist/errors/illegal-action-error.d.ts +1 -1
- package/dist/errors/index.js +0 -2
- package/dist/errors/poker-engine-error.js +2 -0
- package/dist/history/formats/pokerstars.js +0 -8
- package/dist/history/hand-history-builder.js +1 -2
- package/dist/index.js +0 -1
- package/dist/rules/action-order.js +2 -7
- package/dist/rules/blinds.js +4 -3
- package/dist/rules/heads-up.js +1 -2
- package/dist/rules/showdown.js +12 -29
- package/dist/rules/side-pots.js +5 -18
- package/dist/utils/deck.js +1 -4
- package/dist/utils/invariants.js +8 -0
- package/dist/utils/rake.js +6 -8
- package/dist/utils/serialization.js +2 -3
- package/dist/utils/validation.js +7 -7
- package/package.json +6 -6
- package/dist/errors/error-codes.d.ts +0 -7
- package/dist/errors/error-codes.js +0 -12
package/dist/actions/betting.js
CHANGED
|
@@ -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,20 +36,13 @@ 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
|
-
// If we have 1 Active player but multiple Live players (others are All-In),
|
|
54
|
-
// the game should naturally progress to Showdown via progressStreet/checkAutoRunout
|
|
55
|
-
// Move to next player
|
|
56
46
|
const nextToAct = (0, action_order_1.getNextToAct)(currentState);
|
|
57
47
|
return {
|
|
58
48
|
...currentState,
|
|
@@ -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,10 +68,8 @@ 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
|
-
// Move to next player
|
|
86
73
|
const nextToAct = (0, action_order_1.getNextToAct)(newState);
|
|
87
74
|
return {
|
|
88
75
|
...newState,
|
|
@@ -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,10 +112,8 @@ 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
|
-
// Move to next player
|
|
137
117
|
const nextToAct = (0, action_order_1.getNextToAct)(newState);
|
|
138
118
|
return {
|
|
139
119
|
...newState,
|
|
@@ -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,14 +152,12 @@ 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
|
-
// Move to next player
|
|
186
161
|
const nextToAct = (0, action_order_1.getNextToAct)(newState);
|
|
187
162
|
return {
|
|
188
163
|
...newState,
|
|
@@ -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,15 +196,10 @@ 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
|
-
|
|
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
|
|
234
|
-
const newMinRaise = reopensBetting
|
|
235
|
-
? raiseAmount + raiseIncrement
|
|
236
|
-
: raiseAmount + state.lastRaiseAmount;
|
|
201
|
+
// Incomplete all-in raises do not change the next minimum full raise.
|
|
202
|
+
const newMinRaise = reopensBetting ? raiseAmount + raiseIncrement : state.minRaise;
|
|
237
203
|
const newState = {
|
|
238
204
|
...state,
|
|
239
205
|
players: newPlayers,
|
|
@@ -242,10 +208,8 @@ function handleRaise(state, action) {
|
|
|
242
208
|
lastRaiseAmount: reopensBetting ? raiseIncrement : state.lastRaiseAmount,
|
|
243
209
|
lastAggressorSeat: reopensBetting ? seat : state.lastAggressorSeat,
|
|
244
210
|
actionHistory: [...state.actionHistory, actionRecord],
|
|
245
|
-
timeBankActiveSeat: null, // Clear time bank flag on any action
|
|
246
211
|
timestamp: action.timestamp,
|
|
247
212
|
};
|
|
248
|
-
// Move to next player
|
|
249
213
|
const nextToAct = (0, action_order_1.getNextToAct)(newState);
|
|
250
214
|
return {
|
|
251
215
|
...newState,
|
|
@@ -278,31 +242,26 @@ function getTotalPot(state) {
|
|
|
278
242
|
return total;
|
|
279
243
|
}
|
|
280
244
|
/**
|
|
281
|
-
* Award pots to remaining eligible players when hand ends by folds
|
|
282
|
-
* Properly handles side pot eligibility and uncalled bets
|
|
245
|
+
* Award pots to remaining eligible players when hand ends by folds.
|
|
283
246
|
*
|
|
284
247
|
* Key principle: Uncalled bets are NOT raked and are returned to the bettor immediately.
|
|
285
|
-
* Only the contested portion
|
|
248
|
+
* Only the contested portion is subject to rake.
|
|
286
249
|
*/
|
|
287
250
|
function awardPotToLastPlayer(state, winningSeat) {
|
|
288
251
|
const newPlayers = [...state.players];
|
|
289
252
|
const newActionHistory = [...state.actionHistory];
|
|
290
253
|
const winners = [];
|
|
291
|
-
// Process each pot separately, checking eligibility
|
|
292
254
|
let totalRakeFromPots = 0;
|
|
293
255
|
for (const pot of state.pots) {
|
|
294
|
-
// Find all non-folded players eligible for this pot
|
|
295
256
|
const eligibleNonFolded = pot.eligibleSeats.filter((seat) => {
|
|
296
257
|
const player = state.players[seat];
|
|
297
258
|
return player && player.status !== "FOLDED" /* PlayerStatus.FOLDED */;
|
|
298
259
|
});
|
|
299
|
-
// Calculate rake for this pot - GLOBAL cap applied across all pots
|
|
300
260
|
const { rake: potRake } = (0, rake_1.calculateRake)(state, pot.amount, totalRakeFromPots);
|
|
301
261
|
totalRakeFromPots += potRake;
|
|
302
262
|
const potAfterRake = pot.amount - potRake;
|
|
303
263
|
if (eligibleNonFolded.length === 0) {
|
|
304
|
-
//
|
|
305
|
-
// Award to last player to fold from eligible seats (fallback)
|
|
264
|
+
// Defensive fallback: no eligible players remain.
|
|
306
265
|
const lastEligible = pot.eligibleSeats[pot.eligibleSeats.length - 1];
|
|
307
266
|
const player = newPlayers[lastEligible];
|
|
308
267
|
if (player) {
|
|
@@ -319,7 +278,6 @@ function awardPotToLastPlayer(state, winningSeat) {
|
|
|
319
278
|
}
|
|
320
279
|
}
|
|
321
280
|
else if (eligibleNonFolded.length === 1) {
|
|
322
|
-
// Exactly one eligible player - they win this pot
|
|
323
281
|
const winnerSeat = eligibleNonFolded[0];
|
|
324
282
|
const player = newPlayers[winnerSeat];
|
|
325
283
|
newPlayers[winnerSeat] = {
|
|
@@ -334,8 +292,6 @@ function awardPotToLastPlayer(state, winningSeat) {
|
|
|
334
292
|
});
|
|
335
293
|
}
|
|
336
294
|
else {
|
|
337
|
-
// Multiple eligible players remain - this means awardPotToLastPlayer was called incorrectly
|
|
338
|
-
// The hand should have gone to showdown instead
|
|
339
295
|
throw new critical_state_error_1.CriticalStateError("awardPotToLastPlayer called with multiple eligible players remaining", {
|
|
340
296
|
potAmount: pot.amount,
|
|
341
297
|
eligibleSeats: pot.eligibleSeats,
|
|
@@ -344,28 +300,25 @@ function awardPotToLastPlayer(state, winningSeat) {
|
|
|
344
300
|
});
|
|
345
301
|
}
|
|
346
302
|
}
|
|
347
|
-
//
|
|
303
|
+
// Uncalled bet logic: three-step process.
|
|
348
304
|
if (state.currentBets.size > 0) {
|
|
349
305
|
const winnersBet = state.currentBets.get(winningSeat) ?? 0;
|
|
350
|
-
//
|
|
351
|
-
// This determines how much of the winner's bet was actually "called"
|
|
306
|
+
// Second-highest bet determines how much of winner's bet was actually "called".
|
|
352
307
|
let maxOpponentBet = 0;
|
|
353
308
|
for (const [seat, amount] of state.currentBets.entries()) {
|
|
354
309
|
if (seat !== winningSeat && amount > maxOpponentBet) {
|
|
355
310
|
maxOpponentBet = amount;
|
|
356
311
|
}
|
|
357
312
|
}
|
|
358
|
-
// Calculate uncalled and called portions
|
|
359
313
|
const uncalledAmount = winnersBet > maxOpponentBet ? winnersBet - maxOpponentBet : 0;
|
|
360
314
|
const calledPortion = winnersBet > maxOpponentBet ? maxOpponentBet : winnersBet;
|
|
361
|
-
// Step 1: Return uncalled bet immediately (NO RAKE on uncalled bets)
|
|
315
|
+
// Step 1: Return uncalled bet immediately (NO RAKE on uncalled bets).
|
|
362
316
|
if (uncalledAmount > 0) {
|
|
363
317
|
const player = newPlayers[winningSeat];
|
|
364
318
|
newPlayers[winningSeat] = {
|
|
365
319
|
...player,
|
|
366
320
|
stack: player.stack + uncalledAmount,
|
|
367
321
|
};
|
|
368
|
-
// Record the uncalled bet return
|
|
369
322
|
newActionHistory.push({
|
|
370
323
|
action: {
|
|
371
324
|
type: "UNCALLED_BET_RETURNED" /* ActionType.UNCALLED_BET_RETURNED */,
|
|
@@ -379,14 +332,14 @@ function awardPotToLastPlayer(state, winningSeat) {
|
|
|
379
332
|
street: state.street,
|
|
380
333
|
});
|
|
381
334
|
}
|
|
382
|
-
// Step 2:
|
|
335
|
+
// Step 2: Contested pot = winner's called portion + all opponent bets.
|
|
383
336
|
let contestedPot = calledPortion;
|
|
384
337
|
for (const [seat, amount] of state.currentBets.entries()) {
|
|
385
338
|
if (seat !== winningSeat) {
|
|
386
339
|
contestedPot += amount;
|
|
387
340
|
}
|
|
388
341
|
}
|
|
389
|
-
// Step 3: Rake and award the contested portion only
|
|
342
|
+
// Step 3: Rake and award the contested portion only.
|
|
390
343
|
if (contestedPot > 0) {
|
|
391
344
|
const { rake } = (0, rake_1.calculateRake)(state, contestedPot, totalRakeFromPots);
|
|
392
345
|
const totalRake = totalRakeFromPots + rake;
|
|
@@ -396,8 +349,6 @@ function awardPotToLastPlayer(state, winningSeat) {
|
|
|
396
349
|
...player,
|
|
397
350
|
stack: player.stack + winnings,
|
|
398
351
|
};
|
|
399
|
-
// Update winners array
|
|
400
|
-
// Only count actual winnings (contested pot after rake, minus winner's own contribution)
|
|
401
352
|
const actualWinnings = winnings - calledPortion;
|
|
402
353
|
if (actualWinnings > 0) {
|
|
403
354
|
const existingIndex = winners.findIndex((w) => w.seat === winningSeat);
|
|
@@ -416,16 +367,13 @@ function awardPotToLastPlayer(state, winningSeat) {
|
|
|
416
367
|
});
|
|
417
368
|
}
|
|
418
369
|
}
|
|
419
|
-
// Update total rake for the hand
|
|
420
370
|
totalRakeFromPots = totalRake;
|
|
421
371
|
}
|
|
422
372
|
}
|
|
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
373
|
return {
|
|
426
374
|
...state,
|
|
427
375
|
players: newPlayers,
|
|
428
|
-
street: "SHOWDOWN" /* Street.SHOWDOWN */,
|
|
376
|
+
street: "SHOWDOWN" /* Street.SHOWDOWN */,
|
|
429
377
|
pots: [],
|
|
430
378
|
currentBets: new Map(),
|
|
431
379
|
winners,
|
package/dist/actions/dealing.js
CHANGED
|
@@ -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
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 */;
|
|
@@ -258,13 +232,13 @@ function handleDeal(state, action) {
|
|
|
258
232
|
Array.from(currentBets.values()).reduce((sum, amount) => sum + amount, 0),
|
|
259
233
|
minRaise: state.bigBlind,
|
|
260
234
|
lastRaiseAmount: state.bigBlind,
|
|
235
|
+
lastAggressorSeat: null,
|
|
261
236
|
activePlayers,
|
|
262
237
|
winners: null,
|
|
263
238
|
rakeThisHand: 0,
|
|
264
239
|
actionHistory: [],
|
|
265
240
|
timestamp: action.timestamp,
|
|
266
241
|
};
|
|
267
|
-
// Set first to act
|
|
268
242
|
const firstToAct = (0, action_order_1.getFirstToAct)(newState);
|
|
269
243
|
return {
|
|
270
244
|
...newState,
|
|
@@ -277,7 +251,6 @@ function handleDeal(state, action) {
|
|
|
277
251
|
*/
|
|
278
252
|
function moveButton(state) {
|
|
279
253
|
if (state.buttonSeat === null) {
|
|
280
|
-
// First hand, find first seated player
|
|
281
254
|
for (let seat = 0; seat < state.maxPlayers; seat++) {
|
|
282
255
|
if (state.players[seat] !== null) {
|
|
283
256
|
return seat;
|
|
@@ -285,8 +258,7 @@ function moveButton(state) {
|
|
|
285
258
|
}
|
|
286
259
|
return 0;
|
|
287
260
|
}
|
|
288
|
-
//
|
|
289
|
-
// We do not skip empty seats here.
|
|
261
|
+
// Dead Button: advance to next seat index regardless of occupancy.
|
|
290
262
|
return (0, positioning_1.getNextSeat)(state.buttonSeat, state.maxPlayers);
|
|
291
263
|
}
|
|
292
264
|
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 {
|
|
@@ -3,47 +3,38 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.handleShow = handleShow;
|
|
4
4
|
exports.handleMuck = handleMuck;
|
|
5
5
|
const illegal_action_error_1 = require("../errors/illegal-action-error");
|
|
6
|
-
const
|
|
6
|
+
const types_1 = require("@pokertools/types");
|
|
7
7
|
/**
|
|
8
8
|
* Handle SHOW action - player reveals their cards at showdown
|
|
9
9
|
*/
|
|
10
10
|
function handleShow(state, action) {
|
|
11
|
-
// Find player
|
|
12
11
|
const player = state.players.find((p) => p?.id === action.playerId);
|
|
13
12
|
if (!player) {
|
|
14
|
-
throw new illegal_action_error_1.IllegalActionError(
|
|
13
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.PLAYER_NOT_FOUND, `Player ${action.playerId} not found`, { playerId: action.playerId });
|
|
15
14
|
}
|
|
16
|
-
// Can only show at showdown
|
|
17
15
|
if (state.street !== "SHOWDOWN" /* Street.SHOWDOWN */) {
|
|
18
|
-
throw new illegal_action_error_1.IllegalActionError(
|
|
16
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.INVALID_ACTION, `Can only show cards at showdown (current street: ${state.street})`, { playerId: action.playerId, street: state.street });
|
|
19
17
|
}
|
|
20
|
-
// Player must not have folded
|
|
21
18
|
if (player.status === "FOLDED" /* PlayerStatus.FOLDED */) {
|
|
22
|
-
throw new illegal_action_error_1.IllegalActionError(
|
|
19
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.INVALID_ACTION, `Cannot show cards after folding`, {
|
|
23
20
|
playerId: action.playerId,
|
|
24
21
|
status: player.status,
|
|
25
22
|
});
|
|
26
23
|
}
|
|
27
|
-
// Player must have cards
|
|
28
24
|
if (!player.hand) {
|
|
29
|
-
throw new illegal_action_error_1.IllegalActionError(
|
|
25
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.INVALID_ACTION, `Player has no cards to show`, {
|
|
30
26
|
playerId: action.playerId,
|
|
31
27
|
});
|
|
32
28
|
}
|
|
33
|
-
// Determine which cards to show
|
|
34
29
|
let cardIndices;
|
|
35
30
|
if (action.cardIndices && action.cardIndices.length > 0) {
|
|
36
|
-
// Validate indices are within bounds
|
|
37
31
|
cardIndices = action.cardIndices.filter((i) => i >= 0 && i < player.hand.length);
|
|
38
|
-
if (cardIndices.length === 0)
|
|
39
|
-
return state;
|
|
40
|
-
}
|
|
32
|
+
if (cardIndices.length === 0)
|
|
33
|
+
return state;
|
|
41
34
|
}
|
|
42
35
|
else {
|
|
43
|
-
// Default: show all cards
|
|
44
36
|
cardIndices = Array.from({ length: player.hand.length }, (_, i) => i);
|
|
45
37
|
}
|
|
46
|
-
// Update player's shown cards
|
|
47
38
|
const newPlayers = [...state.players];
|
|
48
39
|
newPlayers[player.seat] = {
|
|
49
40
|
...player,
|
|
@@ -72,32 +63,27 @@ function handleShow(state, action) {
|
|
|
72
63
|
* Handle MUCK action - player hides their cards at showdown
|
|
73
64
|
*/
|
|
74
65
|
function handleMuck(state, action) {
|
|
75
|
-
// Find player
|
|
76
66
|
const player = state.players.find((p) => p?.id === action.playerId);
|
|
77
67
|
if (!player) {
|
|
78
|
-
throw new illegal_action_error_1.IllegalActionError(
|
|
68
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.PLAYER_NOT_FOUND, `Player ${action.playerId} not found`, { playerId: action.playerId });
|
|
79
69
|
}
|
|
80
|
-
// Can only muck at showdown
|
|
81
70
|
if (state.street !== "SHOWDOWN" /* Street.SHOWDOWN */) {
|
|
82
|
-
throw new illegal_action_error_1.IllegalActionError(
|
|
71
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.INVALID_ACTION, `Can only muck cards at showdown (current street: ${state.street})`, { playerId: action.playerId, street: state.street });
|
|
83
72
|
}
|
|
84
|
-
// Player must not have folded
|
|
85
73
|
if (player.status === "FOLDED" /* PlayerStatus.FOLDED */) {
|
|
86
|
-
throw new illegal_action_error_1.IllegalActionError(
|
|
74
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.INVALID_ACTION, `Cannot muck cards after folding`, {
|
|
87
75
|
playerId: action.playerId,
|
|
88
76
|
status: player.status,
|
|
89
77
|
});
|
|
90
78
|
}
|
|
91
|
-
// Cannot muck if you're a winner (winners must show)
|
|
92
79
|
const isWinner = state.winners?.some((w) => w.seat === player.seat);
|
|
93
80
|
if (isWinner) {
|
|
94
|
-
throw new illegal_action_error_1.IllegalActionError(
|
|
81
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.INVALID_ACTION, `Winners cannot muck - cards must be shown`, { playerId: action.playerId });
|
|
95
82
|
}
|
|
96
|
-
// Set shown cards to null (mucked) - hand is preserved in player.hand
|
|
97
83
|
const newPlayers = [...state.players];
|
|
98
84
|
newPlayers[player.seat] = {
|
|
99
85
|
...player,
|
|
100
|
-
shownCards: null,
|
|
86
|
+
shownCards: null,
|
|
101
87
|
};
|
|
102
88
|
const actionRecord = {
|
|
103
89
|
action: {
|