@pokertools/engine 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +607 -0
- package/dist/actions/betting.d.ts +21 -0
- package/dist/actions/betting.js +410 -0
- package/dist/actions/dealing.d.ts +9 -0
- package/dist/actions/dealing.js +206 -0
- package/dist/actions/management.d.ts +9 -0
- package/dist/actions/management.js +58 -0
- package/dist/actions/showdownActions.d.ts +9 -0
- package/dist/actions/showdownActions.js +119 -0
- package/dist/actions/special.d.ts +14 -0
- package/dist/actions/special.js +98 -0
- package/dist/actions/streetProgression.d.ts +13 -0
- package/dist/actions/streetProgression.js +157 -0
- package/dist/actions/tournament.d.ts +5 -0
- package/dist/actions/tournament.js +38 -0
- package/dist/actions/validation.d.ts +6 -0
- package/dist/actions/validation.js +182 -0
- package/dist/engine/PokerEngine.d.ts +92 -0
- package/dist/engine/PokerEngine.js +246 -0
- package/dist/engine/gameReducer.d.ts +10 -0
- package/dist/engine/gameReducer.js +135 -0
- package/dist/errors/ConfigError.d.ts +8 -0
- package/dist/errors/ConfigError.js +15 -0
- package/dist/errors/CriticalStateError.d.ts +8 -0
- package/dist/errors/CriticalStateError.js +15 -0
- package/dist/errors/ErrorCodes.d.ts +38 -0
- package/dist/errors/ErrorCodes.js +46 -0
- package/dist/errors/IllegalActionError.d.ts +9 -0
- package/dist/errors/IllegalActionError.js +15 -0
- package/dist/errors/PokerEngineError.d.ts +8 -0
- package/dist/errors/PokerEngineError.js +19 -0
- package/dist/errors/index.d.ts +5 -0
- package/dist/errors/index.js +22 -0
- package/dist/history/exporter.d.ts +28 -0
- package/dist/history/exporter.js +60 -0
- package/dist/history/formats/json.d.ts +14 -0
- package/dist/history/formats/json.js +46 -0
- package/dist/history/formats/pokerstars.d.ts +10 -0
- package/dist/history/formats/pokerstars.js +188 -0
- package/dist/history/handHistoryBuilder.d.ts +10 -0
- package/dist/history/handHistoryBuilder.js +179 -0
- package/dist/history/types.d.ts +73 -0
- package/dist/history/types.js +5 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +38 -0
- package/dist/rules/actionOrder.d.ts +14 -0
- package/dist/rules/actionOrder.js +211 -0
- package/dist/rules/blinds.d.ts +24 -0
- package/dist/rules/blinds.js +64 -0
- package/dist/rules/headsUp.d.ts +15 -0
- package/dist/rules/headsUp.js +44 -0
- package/dist/rules/showdown.d.ts +9 -0
- package/dist/rules/showdown.js +164 -0
- package/dist/rules/sidePots.d.ts +32 -0
- package/dist/rules/sidePots.js +173 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/utils/cardUtils.d.ts +12 -0
- package/dist/utils/cardUtils.js +30 -0
- package/dist/utils/constants.d.ts +38 -0
- package/dist/utils/constants.js +41 -0
- package/dist/utils/deck.d.ts +46 -0
- package/dist/utils/deck.js +126 -0
- package/dist/utils/invariants.d.ts +39 -0
- package/dist/utils/invariants.js +163 -0
- package/dist/utils/positioning.d.ts +36 -0
- package/dist/utils/positioning.js +97 -0
- package/dist/utils/rake.d.ts +13 -0
- package/dist/utils/rake.js +45 -0
- package/dist/utils/serialization.d.ts +53 -0
- package/dist/utils/serialization.js +106 -0
- package/dist/utils/validation.d.ts +20 -0
- package/dist/utils/validation.js +52 -0
- package/dist/utils/viewMasking.d.ts +20 -0
- package/dist/utils/viewMasking.js +90 -0
- package/package.json +58 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.exportMultipleHands = exports.getHandHistory = exports.exportHandHistory = exports.auditChipConservation = exports.calculateTotalChips = exports.createPublicView = exports.restoreFromSnapshot = exports.createSnapshot = exports.PokerEngine = void 0;
|
|
18
|
+
// Main Engine Class
|
|
19
|
+
var PokerEngine_1 = require("./engine/PokerEngine");
|
|
20
|
+
Object.defineProperty(exports, "PokerEngine", { enumerable: true, get: function () { return PokerEngine_1.PokerEngine; } });
|
|
21
|
+
// Types (re-exported from @pokertools/types for convenience)
|
|
22
|
+
__exportStar(require("@pokertools/types"), exports);
|
|
23
|
+
// Errors
|
|
24
|
+
__exportStar(require("./errors"), exports);
|
|
25
|
+
// Utilities (for advanced usage)
|
|
26
|
+
var serialization_1 = require("./utils/serialization");
|
|
27
|
+
Object.defineProperty(exports, "createSnapshot", { enumerable: true, get: function () { return serialization_1.createSnapshot; } });
|
|
28
|
+
Object.defineProperty(exports, "restoreFromSnapshot", { enumerable: true, get: function () { return serialization_1.restoreFromSnapshot; } });
|
|
29
|
+
var viewMasking_1 = require("./utils/viewMasking");
|
|
30
|
+
Object.defineProperty(exports, "createPublicView", { enumerable: true, get: function () { return viewMasking_1.createPublicView; } });
|
|
31
|
+
var invariants_1 = require("./utils/invariants");
|
|
32
|
+
Object.defineProperty(exports, "calculateTotalChips", { enumerable: true, get: function () { return invariants_1.calculateTotalChips; } });
|
|
33
|
+
Object.defineProperty(exports, "auditChipConservation", { enumerable: true, get: function () { return invariants_1.auditChipConservation; } });
|
|
34
|
+
// Hand History
|
|
35
|
+
var exporter_1 = require("./history/exporter");
|
|
36
|
+
Object.defineProperty(exports, "exportHandHistory", { enumerable: true, get: function () { return exporter_1.exportHandHistory; } });
|
|
37
|
+
Object.defineProperty(exports, "getHandHistory", { enumerable: true, get: function () { return exporter_1.getHandHistory; } });
|
|
38
|
+
Object.defineProperty(exports, "exportMultipleHands", { enumerable: true, get: function () { return exporter_1.exportMultipleHands; } });
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { GameState } from "@pokertools/types";
|
|
2
|
+
/**
|
|
3
|
+
* Determine the next player to act
|
|
4
|
+
* Returns seat number or null if action is complete
|
|
5
|
+
*/
|
|
6
|
+
export declare function getNextToAct(state: GameState): number | null;
|
|
7
|
+
/**
|
|
8
|
+
* Get first player to act for the current street
|
|
9
|
+
*/
|
|
10
|
+
export declare function getFirstToAct(state: GameState): number | null;
|
|
11
|
+
/**
|
|
12
|
+
* Check if action is complete (everyone has acted and matched bets or folded/all-in)
|
|
13
|
+
*/
|
|
14
|
+
export declare function isActionComplete(state: GameState): boolean;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getNextToAct = getNextToAct;
|
|
4
|
+
exports.getFirstToAct = getFirstToAct;
|
|
5
|
+
exports.isActionComplete = isActionComplete;
|
|
6
|
+
const positioning_1 = require("../utils/positioning");
|
|
7
|
+
const headsUp_1 = require("./headsUp");
|
|
8
|
+
const blinds_1 = require("./blinds");
|
|
9
|
+
/**
|
|
10
|
+
* Determine the next player to act
|
|
11
|
+
* Returns seat number or null if action is complete
|
|
12
|
+
*/
|
|
13
|
+
function getNextToAct(state) {
|
|
14
|
+
// Special case: heads-up has different rules
|
|
15
|
+
if ((0, headsUp_1.isHeadsUp)(state)) {
|
|
16
|
+
return getNextToActHeadsUp(state);
|
|
17
|
+
}
|
|
18
|
+
return getNextToActNormal(state);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Get next to act in normal (3+ player) game
|
|
22
|
+
*/
|
|
23
|
+
function getNextToActNormal(state) {
|
|
24
|
+
if (state.actionTo === null) {
|
|
25
|
+
// Action not started, find first to act
|
|
26
|
+
return getFirstToAct(state);
|
|
27
|
+
}
|
|
28
|
+
const currentBet = getCurrentBet(state);
|
|
29
|
+
let seat = (0, positioning_1.getNextSeat)(state.actionTo, state.maxPlayers);
|
|
30
|
+
const startSeat = state.actionTo;
|
|
31
|
+
let foundActionable = false;
|
|
32
|
+
// Search for next player who can act
|
|
33
|
+
while (seat !== startSeat) {
|
|
34
|
+
const player = state.players[seat];
|
|
35
|
+
// Skip if: no player, folded, all-in, or busted
|
|
36
|
+
if (!player || player.status !== "ACTIVE" /* PlayerStatus.ACTIVE */ || player.stack === 0) {
|
|
37
|
+
seat = (0, positioning_1.getNextSeat)(seat, state.maxPlayers);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
// Player can act if:
|
|
41
|
+
// 1. They haven't acted this street yet, OR
|
|
42
|
+
// 2. Current bet is higher than their bet
|
|
43
|
+
const playerBet = state.currentBets.get(seat) ?? 0;
|
|
44
|
+
if (playerBet < currentBet) {
|
|
45
|
+
return seat; // Player needs to respond to bet
|
|
46
|
+
}
|
|
47
|
+
// Player has matched current bet
|
|
48
|
+
// Check if they've already acted
|
|
49
|
+
if (!hasActedThisStreet(state, seat)) {
|
|
50
|
+
return seat; // Player hasn't acted yet
|
|
51
|
+
}
|
|
52
|
+
foundActionable = true;
|
|
53
|
+
seat = (0, positioning_1.getNextSeat)(seat, state.maxPlayers);
|
|
54
|
+
}
|
|
55
|
+
// Full circle - check if everyone has acted and matched bets
|
|
56
|
+
if (foundActionable && isActionComplete(state)) {
|
|
57
|
+
return null; // Action complete
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Get next to act in heads-up game
|
|
63
|
+
*/
|
|
64
|
+
function getNextToActHeadsUp(state) {
|
|
65
|
+
if (state.buttonSeat === null) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const actionOrder = (0, headsUp_1.getHeadsUpActionOrder)(state, state.street);
|
|
69
|
+
if (actionOrder.length === 0) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
// If action hasn't started, return first player
|
|
73
|
+
if (state.actionTo === null) {
|
|
74
|
+
return actionOrder[0];
|
|
75
|
+
}
|
|
76
|
+
const currentBet = getCurrentBet(state);
|
|
77
|
+
// Check both players
|
|
78
|
+
for (const seat of actionOrder) {
|
|
79
|
+
const player = state.players[seat];
|
|
80
|
+
if (!player || player.status !== "ACTIVE" /* PlayerStatus.ACTIVE */ || player.stack === 0) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const playerBet = state.currentBets.get(seat) ?? 0;
|
|
84
|
+
if (playerBet < currentBet || !hasActedThisStreet(state, seat)) {
|
|
85
|
+
return seat;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return null; // Action complete
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Get first player to act for the current street
|
|
92
|
+
*/
|
|
93
|
+
function getFirstToAct(state) {
|
|
94
|
+
if (state.buttonSeat === null) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
if ((0, headsUp_1.isHeadsUp)(state)) {
|
|
98
|
+
const order = (0, headsUp_1.getHeadsUpActionOrder)(state, state.street);
|
|
99
|
+
if (order.length > 0) {
|
|
100
|
+
return order[0];
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
if (state.street === "PREFLOP" /* Street.PREFLOP */) {
|
|
105
|
+
// Preflop: First to act is UTG (Left of BB)
|
|
106
|
+
// We use blind positions to find the BB seat
|
|
107
|
+
const blinds = (0, blinds_1.getBlindPositions)(state);
|
|
108
|
+
if (!blinds) {
|
|
109
|
+
// Fallback if no blinds found (shouldn't happen)
|
|
110
|
+
return getNextActionableSeat(state.buttonSeat, state);
|
|
111
|
+
}
|
|
112
|
+
// UTG is the next actionable seat after Big Blind
|
|
113
|
+
return getNextActionableSeat(blinds.bigBlindSeat, state);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
// Postflop: First to act is left of Button
|
|
117
|
+
// We start scanning immediately after button
|
|
118
|
+
// (Button might be dead/empty, but the position exists)
|
|
119
|
+
return getNextActionableSeat(state.buttonSeat, state);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Find next actionable player starting from the seat AFTER the given startSeat
|
|
124
|
+
*/
|
|
125
|
+
function getNextActionableSeat(startSeat, state) {
|
|
126
|
+
let seat = (0, positioning_1.getNextSeat)(startSeat, state.maxPlayers);
|
|
127
|
+
const endSeat = startSeat;
|
|
128
|
+
// Scan full circle
|
|
129
|
+
while (seat !== endSeat) {
|
|
130
|
+
const player = state.players[seat];
|
|
131
|
+
if (player && player.status === "ACTIVE" /* PlayerStatus.ACTIVE */ && player.stack > 0) {
|
|
132
|
+
return seat;
|
|
133
|
+
}
|
|
134
|
+
seat = (0, positioning_1.getNextSeat)(seat, state.maxPlayers);
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Get current highest bet this street
|
|
140
|
+
*/
|
|
141
|
+
function getCurrentBet(state) {
|
|
142
|
+
let maxBet = 0;
|
|
143
|
+
for (const bet of state.currentBets.values()) {
|
|
144
|
+
if (bet > maxBet) {
|
|
145
|
+
maxBet = bet;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return maxBet;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Check if action is complete (everyone has acted and matched bets or folded/all-in)
|
|
152
|
+
*/
|
|
153
|
+
function isActionComplete(state) {
|
|
154
|
+
const currentBet = getCurrentBet(state);
|
|
155
|
+
let activeCount = 0;
|
|
156
|
+
let actedCount = 0;
|
|
157
|
+
for (let seat = 0; seat < state.players.length; seat++) {
|
|
158
|
+
const player = state.players[seat];
|
|
159
|
+
if (!player)
|
|
160
|
+
continue;
|
|
161
|
+
// Count active players who can still act
|
|
162
|
+
if (player.status === "ACTIVE" /* PlayerStatus.ACTIVE */ && player.stack > 0) {
|
|
163
|
+
activeCount++;
|
|
164
|
+
const playerBet = state.currentBets.get(seat) ?? 0;
|
|
165
|
+
// Player has matched bet and acted
|
|
166
|
+
if (playerBet === currentBet && hasActedThisStreet(state, seat)) {
|
|
167
|
+
actedCount++;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Action complete if all active players have acted and matched bets
|
|
172
|
+
// OR if there are no active players (all all-in or folded) during an active hand
|
|
173
|
+
if (activeCount === 0) {
|
|
174
|
+
// Only return true if we're in a hand (not pre-deal)
|
|
175
|
+
// Check: Are there all-in players with bets?
|
|
176
|
+
const allInPlayers = state.players.filter((p) => p && p.status === "ALL_IN" /* PlayerStatus.ALL_IN */);
|
|
177
|
+
return allInPlayers.length > 0 && state.currentBets.size > 0;
|
|
178
|
+
}
|
|
179
|
+
return activeCount > 0 && activeCount === actedCount;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Check if a player has acted this street
|
|
183
|
+
* This is tracked by checking if they appear in the action history for this street
|
|
184
|
+
*/
|
|
185
|
+
function hasActedThisStreet(state, seat) {
|
|
186
|
+
// Find actions from current street
|
|
187
|
+
const streetStartIndex = findStreetStartIndex(state);
|
|
188
|
+
for (let i = streetStartIndex; i < state.actionHistory.length; i++) {
|
|
189
|
+
if (state.actionHistory[i].seat === seat) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Find the index in action history where current street started
|
|
197
|
+
* Counts backwards from the end until we find a different street
|
|
198
|
+
*/
|
|
199
|
+
function findStreetStartIndex(state) {
|
|
200
|
+
const currentStreet = state.street;
|
|
201
|
+
// Search backwards through action history
|
|
202
|
+
for (let i = state.actionHistory.length - 1; i >= 0; i--) {
|
|
203
|
+
const record = state.actionHistory[i];
|
|
204
|
+
// If we find an action from a different street, the current street starts after it
|
|
205
|
+
if (record.street && record.street !== currentStreet) {
|
|
206
|
+
return i + 1;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// If all actions are from current street (or no street recorded), start from beginning
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { GameState } from "@pokertools/types";
|
|
2
|
+
/**
|
|
3
|
+
* Result of blind posting calculation
|
|
4
|
+
*/
|
|
5
|
+
export interface BlindPositions {
|
|
6
|
+
readonly smallBlindSeat: number;
|
|
7
|
+
readonly bigBlindSeat: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Determine which seats should post blinds
|
|
11
|
+
*
|
|
12
|
+
* Heads-Up (2 players):
|
|
13
|
+
* - SB = button (button IS small blind)
|
|
14
|
+
* - BB = other player
|
|
15
|
+
*
|
|
16
|
+
* Normal (Dead Button Rule):
|
|
17
|
+
* - SB = Button + 1 (Can be empty -> Dead Small Blind)
|
|
18
|
+
* - BB = Next Occupied Seat after SB
|
|
19
|
+
*/
|
|
20
|
+
export declare function getBlindPositions(state: GameState): BlindPositions | null;
|
|
21
|
+
/**
|
|
22
|
+
* Calculate blind amounts for antes
|
|
23
|
+
*/
|
|
24
|
+
export declare function calculateAntes(state: GameState): Map<number, number>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getBlindPositions = getBlindPositions;
|
|
4
|
+
exports.calculateAntes = calculateAntes;
|
|
5
|
+
const positioning_1 = require("../utils/positioning");
|
|
6
|
+
const headsUp_1 = require("./headsUp");
|
|
7
|
+
/**
|
|
8
|
+
* Determine which seats should post blinds
|
|
9
|
+
*
|
|
10
|
+
* Heads-Up (2 players):
|
|
11
|
+
* - SB = button (button IS small blind)
|
|
12
|
+
* - BB = other player
|
|
13
|
+
*
|
|
14
|
+
* Normal (Dead Button Rule):
|
|
15
|
+
* - SB = Button + 1 (Can be empty -> Dead Small Blind)
|
|
16
|
+
* - BB = Next Occupied Seat after SB
|
|
17
|
+
*/
|
|
18
|
+
function getBlindPositions(state) {
|
|
19
|
+
if (state.buttonSeat === null) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const buttonSeat = state.buttonSeat;
|
|
23
|
+
// Heads-up specific logic (Button is SB)
|
|
24
|
+
if ((0, headsUp_1.isHeadsUp)(state)) {
|
|
25
|
+
const bbSeat = (0, positioning_1.getNextOccupiedSeat)(buttonSeat, state.players, state.maxPlayers);
|
|
26
|
+
if (bbSeat === null) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
smallBlindSeat: buttonSeat,
|
|
31
|
+
bigBlindSeat: bbSeat,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
// Normal Play (Dead Button / Dead Small Blind Logic)
|
|
35
|
+
// 1. SB is ALWAYS the immediate next seat, even if empty
|
|
36
|
+
const sbSeat = (0, positioning_1.getNextSeat)(buttonSeat, state.maxPlayers);
|
|
37
|
+
// 2. BB is the next ACTIVE/OCCUPIED player after the SB position
|
|
38
|
+
const bbSeat = (0, positioning_1.getNextOccupiedSeat)(sbSeat, state.players, state.maxPlayers);
|
|
39
|
+
if (bbSeat === null) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
smallBlindSeat: sbSeat,
|
|
44
|
+
bigBlindSeat: bbSeat,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Calculate blind amounts for antes
|
|
49
|
+
*/
|
|
50
|
+
function calculateAntes(state) {
|
|
51
|
+
const antes = new Map();
|
|
52
|
+
if (state.ante === 0) {
|
|
53
|
+
return antes;
|
|
54
|
+
}
|
|
55
|
+
// All active players post antes
|
|
56
|
+
for (let seat = 0; seat < state.players.length; seat++) {
|
|
57
|
+
const player = state.players[seat];
|
|
58
|
+
if (player && player.stack > 0) {
|
|
59
|
+
const anteAmount = Math.min(player.stack, state.ante);
|
|
60
|
+
antes.set(seat, anteAmount);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return antes;
|
|
64
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { GameState, Street } from "@pokertools/types";
|
|
2
|
+
/**
|
|
3
|
+
* Determine if the game is heads-up (exactly 2 seated players)
|
|
4
|
+
* This checks seated players, not just active in current hand,
|
|
5
|
+
* because heads-up rules apply to the table structure, not hand state
|
|
6
|
+
*/
|
|
7
|
+
export declare function isHeadsUp(state: GameState): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Get heads-up action order for a given street
|
|
10
|
+
* In heads-up:
|
|
11
|
+
* - Button IS small blind
|
|
12
|
+
* - Button acts FIRST preflop
|
|
13
|
+
* - Button acts LAST postflop
|
|
14
|
+
*/
|
|
15
|
+
export declare function getHeadsUpActionOrder(state: GameState, street: Street): number[];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isHeadsUp = isHeadsUp;
|
|
4
|
+
exports.getHeadsUpActionOrder = getHeadsUpActionOrder;
|
|
5
|
+
/**
|
|
6
|
+
* Determine if the game is heads-up (exactly 2 seated players)
|
|
7
|
+
* This checks seated players, not just active in current hand,
|
|
8
|
+
* because heads-up rules apply to the table structure, not hand state
|
|
9
|
+
*/
|
|
10
|
+
function isHeadsUp(state) {
|
|
11
|
+
const seatedPlayers = state.players.filter((p) => p !== null);
|
|
12
|
+
return seatedPlayers.length === 2;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Get heads-up action order for a given street
|
|
16
|
+
* In heads-up:
|
|
17
|
+
* - Button IS small blind
|
|
18
|
+
* - Button acts FIRST preflop
|
|
19
|
+
* - Button acts LAST postflop
|
|
20
|
+
*/
|
|
21
|
+
function getHeadsUpActionOrder(state, street) {
|
|
22
|
+
if (state.buttonSeat === null) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
const buttonSeat = state.buttonSeat;
|
|
26
|
+
const activePlayers = state.players
|
|
27
|
+
.map((p, seat) => ({ player: p, seat }))
|
|
28
|
+
.filter(({ player }) => player && (player.status === "ACTIVE" /* PlayerStatus.ACTIVE */ || player.status === "ALL_IN" /* PlayerStatus.ALL_IN */))
|
|
29
|
+
.map(({ seat }) => seat);
|
|
30
|
+
if (activePlayers.length !== 2) {
|
|
31
|
+
return activePlayers;
|
|
32
|
+
}
|
|
33
|
+
// Find the two seats
|
|
34
|
+
const [seat1, seat2] = activePlayers.sort((a, b) => a - b);
|
|
35
|
+
const otherSeat = seat1 === buttonSeat ? seat2 : seat1;
|
|
36
|
+
if (street === "PREFLOP" /* Street.PREFLOP */) {
|
|
37
|
+
// Button acts first preflop
|
|
38
|
+
return [buttonSeat, otherSeat];
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
// Button acts last postflop (other player first)
|
|
42
|
+
return [otherSeat, buttonSeat];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { GameState } from "@pokertools/types";
|
|
2
|
+
/**
|
|
3
|
+
* Determine winners and distribute pots
|
|
4
|
+
*/
|
|
5
|
+
export declare function determineWinners(state: GameState): GameState;
|
|
6
|
+
/**
|
|
7
|
+
* Check if hand should go to showdown
|
|
8
|
+
*/
|
|
9
|
+
export declare function shouldShowdown(state: GameState): boolean;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.determineWinners = determineWinners;
|
|
4
|
+
exports.shouldShowdown = shouldShowdown;
|
|
5
|
+
const evaluator_1 = require("@pokertools/evaluator");
|
|
6
|
+
const positioning_1 = require("../utils/positioning");
|
|
7
|
+
const rake_1 = require("../utils/rake");
|
|
8
|
+
/**
|
|
9
|
+
* Determine winners and distribute pots
|
|
10
|
+
*/
|
|
11
|
+
function determineWinners(state) {
|
|
12
|
+
const winners = [];
|
|
13
|
+
const newPlayers = [...state.players];
|
|
14
|
+
let totalRake = 0;
|
|
15
|
+
const winnerSeats = new Set();
|
|
16
|
+
// Process each pot (side pots first, then main)
|
|
17
|
+
const sortedPots = [...state.pots].sort((a, b) => {
|
|
18
|
+
if (a.type === "SIDE" && b.type === "MAIN")
|
|
19
|
+
return -1;
|
|
20
|
+
if (a.type === "MAIN" && b.type === "SIDE")
|
|
21
|
+
return 1;
|
|
22
|
+
return 0;
|
|
23
|
+
});
|
|
24
|
+
for (const pot of sortedPots) {
|
|
25
|
+
const potWinners = evaluatePot(state, pot);
|
|
26
|
+
// Calculate and deduct rake (cash games only)
|
|
27
|
+
// Apply GLOBAL rake cap across all pots (per-hand, not per-pot)
|
|
28
|
+
const { rake } = (0, rake_1.calculateRake)(state, pot.amount, totalRake);
|
|
29
|
+
totalRake += rake;
|
|
30
|
+
const potAfterRake = pot.amount - rake;
|
|
31
|
+
// Distribute pot among winners
|
|
32
|
+
const share = Math.floor(potAfterRake / potWinners.length);
|
|
33
|
+
const remainder = potAfterRake % potWinners.length;
|
|
34
|
+
// Sort winners by position (worst to best) for odd chip distribution
|
|
35
|
+
// TDA Rule: Odd chips go to first player(s) clockwise from button
|
|
36
|
+
const sortedWinners = [...potWinners].sort((a, b) => {
|
|
37
|
+
if (state.buttonSeat === null)
|
|
38
|
+
return 0;
|
|
39
|
+
const distA = (0, positioning_1.getDistanceFromButton)(a.seat, state.buttonSeat, state.maxPlayers);
|
|
40
|
+
const distB = (0, positioning_1.getDistanceFromButton)(b.seat, state.buttonSeat, state.maxPlayers);
|
|
41
|
+
return distA - distB;
|
|
42
|
+
});
|
|
43
|
+
// Distribute chips to all winners
|
|
44
|
+
for (let i = 0; i < sortedWinners.length; i++) {
|
|
45
|
+
const evaluation = sortedWinners[i];
|
|
46
|
+
let award = share;
|
|
47
|
+
// Distribute odd chips one at a time to worst positions
|
|
48
|
+
// (first N winners in sorted order get the extra chips)
|
|
49
|
+
if (i < remainder) {
|
|
50
|
+
award += 1;
|
|
51
|
+
}
|
|
52
|
+
// Track winner seats
|
|
53
|
+
winnerSeats.add(evaluation.seat);
|
|
54
|
+
// Award chips to player
|
|
55
|
+
const player = newPlayers[evaluation.seat];
|
|
56
|
+
newPlayers[evaluation.seat] = {
|
|
57
|
+
...player,
|
|
58
|
+
stack: player.stack + award,
|
|
59
|
+
};
|
|
60
|
+
// Record winner
|
|
61
|
+
winners.push({
|
|
62
|
+
seat: evaluation.seat,
|
|
63
|
+
amount: award,
|
|
64
|
+
hand: evaluation.hand,
|
|
65
|
+
handRank: evaluation.description,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Set shown cards for winners and losers
|
|
70
|
+
for (let seat = 0; seat < newPlayers.length; seat++) {
|
|
71
|
+
const player = newPlayers[seat];
|
|
72
|
+
if (player && player.hand !== null) {
|
|
73
|
+
if (winnerSeats.has(seat)) {
|
|
74
|
+
// Winners must show all cards
|
|
75
|
+
newPlayers[seat] = {
|
|
76
|
+
...player,
|
|
77
|
+
shownCards: [0, 1], // Show both cards
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
// Losers are mucked by default (can be changed via SHOW action)
|
|
82
|
+
newPlayers[seat] = {
|
|
83
|
+
...player,
|
|
84
|
+
shownCards: null, // Mucked - hand preserved but not shown
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// NOTE: We do NOT reset totalInvestedThisHand here because it's used by getInitialChips()
|
|
90
|
+
// to calculate total chips in the game. It will be reset when a new hand is dealt.
|
|
91
|
+
return {
|
|
92
|
+
...state,
|
|
93
|
+
players: newPlayers,
|
|
94
|
+
winners,
|
|
95
|
+
rakeThisHand: totalRake,
|
|
96
|
+
pots: [], // Pots distributed
|
|
97
|
+
actionTo: null,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Evaluate a single pot and return winner(s)
|
|
102
|
+
*/
|
|
103
|
+
function evaluatePot(state, pot) {
|
|
104
|
+
// Get eligible players (not folded)
|
|
105
|
+
const eligible = pot.eligibleSeats
|
|
106
|
+
.map((seat) => state.players[seat])
|
|
107
|
+
.filter((player) => player && (player.status === "ACTIVE" /* PlayerStatus.ACTIVE */ || player.status === "ALL_IN" /* PlayerStatus.ALL_IN */));
|
|
108
|
+
// If only one player, they win without showing
|
|
109
|
+
if (eligible.length === 1) {
|
|
110
|
+
const player = eligible[0];
|
|
111
|
+
return [
|
|
112
|
+
{
|
|
113
|
+
seat: player.seat,
|
|
114
|
+
score: 0,
|
|
115
|
+
hand: [],
|
|
116
|
+
description: "Uncontested",
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
// Evaluate all hands
|
|
121
|
+
const evaluations = [];
|
|
122
|
+
for (const player of eligible) {
|
|
123
|
+
if (!player?.hand)
|
|
124
|
+
continue;
|
|
125
|
+
// Combine hole cards + board (7 cards total for river)
|
|
126
|
+
const allCards = [...player.hand, ...state.board];
|
|
127
|
+
if (allCards.length < 5) {
|
|
128
|
+
// Not enough cards (shouldn't happen)
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
// Evaluate using @pokertools/evaluator
|
|
132
|
+
const cardCodes = (0, evaluator_1.getCardCodes)(allCards);
|
|
133
|
+
const score = (0, evaluator_1.evaluate)(cardCodes);
|
|
134
|
+
const handRank = (0, evaluator_1.rank)(cardCodes);
|
|
135
|
+
const description = (0, evaluator_1.rankDescription)(handRank);
|
|
136
|
+
evaluations.push({
|
|
137
|
+
seat: player.seat,
|
|
138
|
+
score,
|
|
139
|
+
hand: [...player.hand], // Store hole cards (copy to mutable array)
|
|
140
|
+
description,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
// Find best hand(s)
|
|
144
|
+
const bestScore = Math.min(...evaluations.map((e) => e.score));
|
|
145
|
+
const winners = evaluations.filter((e) => e.score === bestScore);
|
|
146
|
+
return winners;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Check if hand should go to showdown
|
|
150
|
+
*/
|
|
151
|
+
function shouldShowdown(state) {
|
|
152
|
+
// Showdown if:
|
|
153
|
+
// 1. We're at SHOWDOWN street
|
|
154
|
+
// 2. Multiple players remain (not folded)
|
|
155
|
+
// 3. Winners haven't been determined yet
|
|
156
|
+
if (state.street !== "SHOWDOWN" /* Street.SHOWDOWN */) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
if (state.winners !== null) {
|
|
160
|
+
return false; // Already determined winners
|
|
161
|
+
}
|
|
162
|
+
const activePlayers = state.players.filter((p) => p && (p.status === "ACTIVE" /* PlayerStatus.ACTIVE */ || p.status === "ALL_IN" /* PlayerStatus.ALL_IN */));
|
|
163
|
+
return activePlayers.length >= 2;
|
|
164
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { GameState, Pot } from "@pokertools/types";
|
|
2
|
+
/**
|
|
3
|
+
* Calculate side pots using iterative subtraction method
|
|
4
|
+
*
|
|
5
|
+
* Algorithm:
|
|
6
|
+
* 1. Combine all player investments (bets + total invested)
|
|
7
|
+
* 2. Sort by investment amount (ascending)
|
|
8
|
+
* 3. For each player from smallest to largest:
|
|
9
|
+
* - Create pot = (player investment - previous) × remaining players
|
|
10
|
+
* - Add player + all higher investors to eligible list
|
|
11
|
+
* 4. Return pots array (main + sides)
|
|
12
|
+
*
|
|
13
|
+
* @param state Current game state
|
|
14
|
+
* @returns Array of pots (main pot first, then side pots)
|
|
15
|
+
*/
|
|
16
|
+
export declare function calculateSidePots(state: GameState): Pot[];
|
|
17
|
+
/**
|
|
18
|
+
* Calculate uncalled bet (when highest better has no callers)
|
|
19
|
+
*
|
|
20
|
+
* @param state Current game state
|
|
21
|
+
* @returns Tuple of [uncalled amount, seat to return to]
|
|
22
|
+
*/
|
|
23
|
+
export declare function calculateUncalledBet(state: GameState): [number, number] | null;
|
|
24
|
+
/**
|
|
25
|
+
* Return uncalled bet to player
|
|
26
|
+
*/
|
|
27
|
+
export declare function returnUncalledBet(state: GameState): GameState;
|
|
28
|
+
/**
|
|
29
|
+
* Recalculate pots after street action completes
|
|
30
|
+
* This is called before progressing to next street
|
|
31
|
+
*/
|
|
32
|
+
export declare function recalculatePots(state: GameState): GameState;
|