@pokertools/engine 1.0.9 → 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 +15 -65
- package/dist/actions/dealing.js +23 -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 +28 -34
- 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 +1 -1
- package/dist/rules/heads-up.js +0 -1
- package/dist/rules/showdown.js +11 -28
- package/dist/rules/side-pots.js +5 -18
- package/dist/utils/deck.js +1 -4
- 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
|
@@ -12,32 +12,25 @@ const action_order_1 = require("../rules/action-order");
|
|
|
12
12
|
* - Resets action
|
|
13
13
|
*/
|
|
14
14
|
function progressStreet(state) {
|
|
15
|
-
// Determine next street
|
|
16
15
|
const nextStreet = getNextStreet(state.street);
|
|
17
16
|
if (nextStreet === null) {
|
|
18
|
-
// Already at showdown or beyond
|
|
19
17
|
return state;
|
|
20
18
|
}
|
|
21
|
-
|
|
22
|
-
const shouldAutoRunout = checkAutoRunout(state);
|
|
23
|
-
if (shouldAutoRunout) {
|
|
19
|
+
if (checkAutoRunout(state)) {
|
|
24
20
|
return handleAutoRunout(state);
|
|
25
21
|
}
|
|
26
|
-
// Deal community cards
|
|
27
22
|
const { board, deck } = dealCommunityCards(state, nextStreet);
|
|
28
|
-
//
|
|
29
|
-
// Note: Pots are calculated by recalculatePots() before progressStreet() is called
|
|
23
|
+
// Pots are recalculated by recalculatePots() before progressStreet() is called.
|
|
30
24
|
const newState = {
|
|
31
25
|
...state,
|
|
32
26
|
street: nextStreet,
|
|
33
27
|
board,
|
|
34
28
|
deck,
|
|
35
|
-
pots: state.pots,
|
|
29
|
+
pots: state.pots,
|
|
36
30
|
currentBets: new Map(),
|
|
37
31
|
lastAggressorSeat: null,
|
|
38
|
-
//
|
|
32
|
+
// Street progression is not a user action, keep last timestamp.
|
|
39
33
|
};
|
|
40
|
-
// Set first to act
|
|
41
34
|
const firstToAct = (0, action_order_1.getFirstToAct)(newState);
|
|
42
35
|
return {
|
|
43
36
|
...newState,
|
|
@@ -106,7 +99,6 @@ function checkAutoRunout(state) {
|
|
|
106
99
|
allInCount++;
|
|
107
100
|
}
|
|
108
101
|
}
|
|
109
|
-
// Auto-runout if ≤1 active player with chips (rest are all-in or folded)
|
|
110
102
|
return activeCount <= 1 && allInCount > 0;
|
|
111
103
|
}
|
|
112
104
|
/**
|
|
@@ -116,25 +108,21 @@ function checkAutoRunout(state) {
|
|
|
116
108
|
function handleAutoRunout(state) {
|
|
117
109
|
let currentState = state;
|
|
118
110
|
let currentStreet = state.street;
|
|
119
|
-
// Deal remaining streets manually (FLOP -> TURN -> RIVER)
|
|
120
111
|
while (currentStreet !== "RIVER" /* Street.RIVER */) {
|
|
121
112
|
const nextStreet = getNextStreet(currentStreet);
|
|
122
113
|
if (nextStreet === null || nextStreet === "SHOWDOWN" /* Street.SHOWDOWN */)
|
|
123
114
|
break;
|
|
124
|
-
// Deal community cards for this street
|
|
125
115
|
const { board, deck } = dealCommunityCards(currentState, nextStreet);
|
|
126
|
-
// Update state with new street and board
|
|
127
116
|
currentState = {
|
|
128
117
|
...currentState,
|
|
129
118
|
street: nextStreet,
|
|
130
119
|
board,
|
|
131
120
|
deck,
|
|
132
|
-
currentBets: new Map(),
|
|
121
|
+
currentBets: new Map(),
|
|
133
122
|
lastAggressorSeat: null,
|
|
134
123
|
};
|
|
135
124
|
currentStreet = nextStreet;
|
|
136
125
|
}
|
|
137
|
-
// Move to showdown
|
|
138
126
|
return {
|
|
139
127
|
...currentState,
|
|
140
128
|
street: "SHOWDOWN" /* Street.SHOWDOWN */,
|
|
@@ -146,12 +134,9 @@ function handleAutoRunout(state) {
|
|
|
146
134
|
* (All players have acted and matched bets)
|
|
147
135
|
*/
|
|
148
136
|
function shouldProgressStreet(state) {
|
|
149
|
-
if (state.actionTo !== null)
|
|
150
|
-
return false;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
return false; // Already at showdown
|
|
154
|
-
}
|
|
155
|
-
// Check if all active players have acted
|
|
137
|
+
if (state.actionTo !== null)
|
|
138
|
+
return false;
|
|
139
|
+
if (state.street === "SHOWDOWN" /* Street.SHOWDOWN */)
|
|
140
|
+
return false;
|
|
156
141
|
return (0, action_order_1.isActionComplete)(state);
|
|
157
142
|
}
|
|
@@ -5,17 +5,14 @@ exports.handleNextBlindLevel = handleNextBlindLevel;
|
|
|
5
5
|
* Handle NEXT_BLIND_LEVEL action - advance to next blind level in tournament
|
|
6
6
|
*/
|
|
7
7
|
function handleNextBlindLevel(state, action) {
|
|
8
|
-
// Only applicable for tournaments
|
|
9
8
|
if (!state.config.blindStructure) {
|
|
10
9
|
return state;
|
|
11
10
|
}
|
|
12
11
|
const nextLevel = state.blindLevel + 1;
|
|
13
|
-
// Check if we're at max level
|
|
14
12
|
if (nextLevel >= state.config.blindStructure.length) {
|
|
15
|
-
return state;
|
|
13
|
+
return state;
|
|
16
14
|
}
|
|
17
15
|
const blindLevel = state.config.blindStructure[nextLevel];
|
|
18
|
-
// Record action to history
|
|
19
16
|
const actionRecord = {
|
|
20
17
|
action: {
|
|
21
18
|
type: "NEXT_BLIND_LEVEL" /* ActionType.NEXT_BLIND_LEVEL */,
|
|
@@ -2,14 +2,13 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.validateAction = validateAction;
|
|
4
4
|
const illegal_action_error_1 = require("../errors/illegal-action-error");
|
|
5
|
-
const
|
|
5
|
+
const types_1 = require("@pokertools/types");
|
|
6
6
|
const positioning_1 = require("../utils/positioning");
|
|
7
7
|
/**
|
|
8
8
|
* Validate that an action is legal in the current game state
|
|
9
9
|
* Throws IllegalActionError if action is invalid
|
|
10
10
|
*/
|
|
11
11
|
function validateAction(state, action) {
|
|
12
|
-
// Type-specific validation
|
|
13
12
|
switch (action.type) {
|
|
14
13
|
case "FOLD" /* ActionType.FOLD */:
|
|
15
14
|
case "CHECK" /* ActionType.CHECK */:
|
|
@@ -44,45 +43,41 @@ function validateAction(state, action) {
|
|
|
44
43
|
}
|
|
45
44
|
function validateBettingAction(state, action) {
|
|
46
45
|
if (!("playerId" in action)) {
|
|
47
|
-
throw new illegal_action_error_1.IllegalActionError(
|
|
46
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.INVALID_ACTION, "Action missing playerId");
|
|
48
47
|
}
|
|
49
48
|
const result = (0, positioning_1.getPlayerById)(state, action.playerId);
|
|
50
49
|
if (!result) {
|
|
51
|
-
throw new illegal_action_error_1.IllegalActionError(
|
|
50
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.PLAYER_NOT_FOUND, `Player ${action.playerId} not found`, { playerId: action.playerId });
|
|
52
51
|
}
|
|
53
52
|
const { player, seat } = result;
|
|
54
|
-
// Check if it's player's turn
|
|
55
53
|
if (state.actionTo !== seat) {
|
|
56
|
-
throw new illegal_action_error_1.IllegalActionError(
|
|
54
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.NOT_YOUR_TURN, `Player ${action.playerId} attempted to act, but action is on seat ${state.actionTo}`, {
|
|
57
55
|
playerId: action.playerId,
|
|
58
56
|
playerSeat: seat,
|
|
59
57
|
actionTo: state.actionTo,
|
|
60
58
|
street: state.street,
|
|
61
59
|
});
|
|
62
60
|
}
|
|
63
|
-
// Check player status
|
|
64
61
|
if (player.status !== "ACTIVE" /* PlayerStatus.ACTIVE */) {
|
|
65
|
-
throw new illegal_action_error_1.IllegalActionError(
|
|
62
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.PLAYER_NOT_ACTIVE, `Player ${action.playerId} cannot act with status ${player.status}`, { playerId: action.playerId, status: player.status });
|
|
66
63
|
}
|
|
67
|
-
// Check player has chips
|
|
68
64
|
if (player.stack === 0 && action.type !== "FOLD" /* ActionType.FOLD */) {
|
|
69
|
-
throw new illegal_action_error_1.IllegalActionError(
|
|
65
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.NO_CHIPS, `Player ${action.playerId} has no chips`, {
|
|
70
66
|
playerId: action.playerId,
|
|
71
67
|
});
|
|
72
68
|
}
|
|
73
|
-
// Action-specific validation
|
|
74
69
|
const currentBet = getCurrentBet(state);
|
|
75
70
|
const playerBet = state.currentBets.get(seat) ?? 0;
|
|
76
71
|
const toCall = currentBet - playerBet;
|
|
77
72
|
switch (action.type) {
|
|
78
73
|
case "CHECK" /* ActionType.CHECK */:
|
|
79
74
|
if (toCall > 0) {
|
|
80
|
-
throw new illegal_action_error_1.IllegalActionError(
|
|
75
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.CANNOT_CHECK, `Player ${action.playerId} cannot check with ${toCall} to call`, { playerId: action.playerId, toCall });
|
|
81
76
|
}
|
|
82
77
|
break;
|
|
83
78
|
case "CALL" /* ActionType.CALL */:
|
|
84
79
|
if (toCall === 0) {
|
|
85
|
-
throw new illegal_action_error_1.IllegalActionError(
|
|
80
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.NOTHING_TO_CALL, `Player ${action.playerId} has nothing to call`, { playerId: action.playerId });
|
|
86
81
|
}
|
|
87
82
|
break;
|
|
88
83
|
case "BET" /* ActionType.BET */:
|
|
@@ -95,11 +90,11 @@ function validateBettingAction(state, action) {
|
|
|
95
90
|
}
|
|
96
91
|
// Reject bets below the current bet (string bet exploit)
|
|
97
92
|
if (action.amount < currentBet) {
|
|
98
|
-
throw new illegal_action_error_1.IllegalActionError(
|
|
93
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.BET_TOO_SMALL, `Bet of ${action.amount} is below current bet ${currentBet}`, { amount: action.amount, currentBet });
|
|
99
94
|
}
|
|
100
95
|
// Reject bets below big blind (when no current bet)
|
|
101
96
|
if (action.amount < state.bigBlind && action.amount < player.stack) {
|
|
102
|
-
throw new illegal_action_error_1.IllegalActionError(
|
|
97
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.BET_TOO_SMALL, `Bet of ${action.amount} is below minimum ${state.bigBlind}`, { amount: action.amount, minimum: state.bigBlind });
|
|
103
98
|
}
|
|
104
99
|
}
|
|
105
100
|
break;
|
|
@@ -108,26 +103,26 @@ function validateBettingAction(state, action) {
|
|
|
108
103
|
// intermediate actions (like calls or incomplete all-in raises) did NOT
|
|
109
104
|
// reopen the betting. Therefore, they cannot re-raise their own bet.
|
|
110
105
|
if (state.lastAggressorSeat === seat) {
|
|
111
|
-
throw new illegal_action_error_1.IllegalActionError(
|
|
106
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.CANNOT_RERAISE, "Betting has not been re-opened to you (incomplete raise or no action)", {
|
|
112
107
|
playerId: action.playerId,
|
|
113
108
|
seat,
|
|
114
109
|
lastAggressor: state.lastAggressorSeat,
|
|
115
110
|
});
|
|
116
111
|
}
|
|
117
112
|
if (currentBet === 0) {
|
|
118
|
-
throw new illegal_action_error_1.IllegalActionError(
|
|
113
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.CANNOT_RAISE, `Player ${action.playerId} cannot raise when there's no bet`, { playerId: action.playerId });
|
|
119
114
|
}
|
|
120
115
|
if ("amount" in action) {
|
|
121
116
|
// Check if player is going all-in (incomplete raise exception)
|
|
122
117
|
const isAllIn = action.amount >= playerBet + player.stack;
|
|
123
118
|
// Reject raises that don't exceed current bet (unless all-in)
|
|
124
119
|
if (action.amount <= currentBet && !isAllIn) {
|
|
125
|
-
throw new illegal_action_error_1.IllegalActionError(
|
|
120
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.RAISE_TOO_SMALL, `Raise to ${action.amount} must be greater than current bet ${currentBet}`, { amount: action.amount, currentBet });
|
|
126
121
|
}
|
|
127
122
|
// Check minimum raise requirement (unless player is going all-in)
|
|
128
123
|
const raiseIncrement = action.amount - currentBet;
|
|
129
124
|
if (!isAllIn && action.amount < state.minRaise) {
|
|
130
|
-
throw new illegal_action_error_1.IllegalActionError(
|
|
125
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.RAISE_TOO_SMALL, `Raise to ${action.amount} is below minimum ${state.minRaise}`, {
|
|
131
126
|
amount: action.amount,
|
|
132
127
|
currentBet,
|
|
133
128
|
raiseIncrement,
|
|
@@ -145,73 +140,72 @@ function validateBetAsRaise(state, action, seat, playerBet, currentBet, playerSt
|
|
|
145
140
|
if (typeof amount !== "number")
|
|
146
141
|
return;
|
|
147
142
|
if (state.lastAggressorSeat === seat) {
|
|
148
|
-
throw new illegal_action_error_1.IllegalActionError(
|
|
143
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.CANNOT_RERAISE, "Betting has not been re-opened to you (incomplete raise or no action)", { playerId: action.playerId, seat, lastAggressor: state.lastAggressorSeat });
|
|
149
144
|
}
|
|
150
145
|
const isAllIn = amount >= playerBet + playerStack;
|
|
151
146
|
if (amount <= currentBet && !isAllIn) {
|
|
152
|
-
throw new illegal_action_error_1.IllegalActionError(
|
|
147
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.RAISE_TOO_SMALL, `Raise to ${amount} must be greater than current bet ${currentBet}`, { amount, currentBet });
|
|
153
148
|
}
|
|
154
149
|
if (!isAllIn && amount < state.minRaise) {
|
|
155
|
-
throw new illegal_action_error_1.IllegalActionError(
|
|
150
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.RAISE_TOO_SMALL, `Raise to ${amount} is below minimum ${state.minRaise}`, { amount, currentBet, minRaise: state.minRaise });
|
|
156
151
|
}
|
|
157
152
|
}
|
|
158
153
|
function validateDealAction(state) {
|
|
159
154
|
if (state.street !== "PREFLOP" /* Street.PREFLOP */ || state.handNumber > 0) {
|
|
160
155
|
// Allow dealing if we're at showdown (hand complete) or haven't started
|
|
161
156
|
if (state.street !== "SHOWDOWN" /* Street.SHOWDOWN */ && state.handNumber > 0) {
|
|
162
|
-
throw new illegal_action_error_1.IllegalActionError(
|
|
157
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.CANNOT_DEAL, "Cannot deal while hand is in progress", { street: state.street });
|
|
163
158
|
}
|
|
164
159
|
}
|
|
165
|
-
// Check we have enough players
|
|
166
160
|
const activePlayers = state.players.filter((p) => p && p.stack > 0 && !p.isSittingOut);
|
|
167
161
|
if (activePlayers.length < 2) {
|
|
168
|
-
throw new illegal_action_error_1.IllegalActionError(
|
|
162
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.NOT_ENOUGH_PLAYERS, `Need at least 2 players to deal, found ${activePlayers.length}`, { playerCount: activePlayers.length });
|
|
169
163
|
}
|
|
170
164
|
}
|
|
171
165
|
function validateSitAction(state, action) {
|
|
172
166
|
if (action.seat < 0 || action.seat >= state.maxPlayers) {
|
|
173
|
-
throw new illegal_action_error_1.IllegalActionError(
|
|
167
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.INVALID_SEAT, `Seat ${action.seat} is invalid (max: ${state.maxPlayers - 1})`, { seat: action.seat, maxPlayers: state.maxPlayers });
|
|
174
168
|
}
|
|
175
169
|
const existingPlayer = state.players[action.seat];
|
|
176
170
|
if (existingPlayer !== null) {
|
|
177
171
|
// Allow claiming the seat if it is RESERVED by THIS player
|
|
178
172
|
const isMyReservation = existingPlayer.status === "RESERVED" /* PlayerStatus.RESERVED */ && existingPlayer.id === action.playerId;
|
|
179
173
|
if (!isMyReservation) {
|
|
180
|
-
throw new illegal_action_error_1.IllegalActionError(
|
|
174
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.SEAT_OCCUPIED, `Seat ${action.seat} is already occupied`, { seat: action.seat });
|
|
181
175
|
}
|
|
182
176
|
}
|
|
183
177
|
if (action.stack <= 0) {
|
|
184
|
-
throw new illegal_action_error_1.IllegalActionError(
|
|
178
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.INVALID_STACK, `Stack must be positive, got ${action.stack}`, { stack: action.stack });
|
|
185
179
|
}
|
|
186
180
|
}
|
|
187
181
|
function validateStandAction(state, action) {
|
|
188
182
|
const result = (0, positioning_1.getPlayerById)(state, action.playerId);
|
|
189
183
|
if (!result) {
|
|
190
|
-
throw new illegal_action_error_1.IllegalActionError(
|
|
184
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.PLAYER_NOT_FOUND, `Player ${action.playerId} not found`, { playerId: action.playerId });
|
|
191
185
|
}
|
|
192
186
|
}
|
|
193
187
|
function validateTimeAction(state, action) {
|
|
194
188
|
const result = (0, positioning_1.getPlayerById)(state, action.playerId);
|
|
195
189
|
if (!result) {
|
|
196
|
-
throw new illegal_action_error_1.IllegalActionError(
|
|
190
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.PLAYER_NOT_FOUND, `Player ${action.playerId} not found`, { playerId: action.playerId });
|
|
197
191
|
}
|
|
198
192
|
const { seat } = result;
|
|
199
193
|
if (state.actionTo !== seat) {
|
|
200
|
-
throw new illegal_action_error_1.IllegalActionError(
|
|
194
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.NOT_YOUR_TURN, `Player ${action.playerId} cannot use time action when it's not their turn`, { playerId: action.playerId, actionTo: state.actionTo });
|
|
201
195
|
}
|
|
202
196
|
}
|
|
203
197
|
function validateAddChipsAction(state, action) {
|
|
204
198
|
const result = (0, positioning_1.getPlayerById)(state, action.playerId);
|
|
205
199
|
if (!result) {
|
|
206
|
-
throw new illegal_action_error_1.IllegalActionError(
|
|
200
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.PLAYER_NOT_FOUND, `Player ${action.playerId} not found`, { playerId: action.playerId });
|
|
207
201
|
}
|
|
208
202
|
}
|
|
209
203
|
function validateReserveSeatAction(state, action) {
|
|
210
204
|
if (action.seat < 0 || action.seat >= state.maxPlayers) {
|
|
211
|
-
throw new illegal_action_error_1.IllegalActionError(
|
|
205
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.INVALID_SEAT, `Seat ${action.seat} is invalid (max: ${state.maxPlayers - 1})`, { seat: action.seat, maxPlayers: state.maxPlayers });
|
|
212
206
|
}
|
|
213
207
|
if (state.players[action.seat] !== null) {
|
|
214
|
-
throw new illegal_action_error_1.IllegalActionError(
|
|
208
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.SEAT_OCCUPIED, `Seat ${action.seat} is already occupied`, { seat: action.seat });
|
|
215
209
|
}
|
|
216
210
|
}
|
|
217
211
|
/**
|
package/dist/browser.js
CHANGED
|
@@ -28,7 +28,6 @@ const poker_engine_1 = require("./engine/poker-engine");
|
|
|
28
28
|
* Falls back to Math.random() only in environments without crypto
|
|
29
29
|
*/
|
|
30
30
|
function getBrowserRNG() {
|
|
31
|
-
// Check for Web Crypto API
|
|
32
31
|
if (typeof window !== "undefined" && window.crypto?.getRandomValues) {
|
|
33
32
|
return () => {
|
|
34
33
|
const buffer = new Uint32Array(1);
|
|
@@ -22,12 +22,9 @@ const constants_1 = require("../utils/constants");
|
|
|
22
22
|
* @returns New game state
|
|
23
23
|
*/
|
|
24
24
|
function gameReducer(state, action) {
|
|
25
|
-
// Validate action
|
|
26
25
|
(0, validation_1.validateAction)(state, action);
|
|
27
|
-
// Apply action based on type
|
|
28
26
|
let newState;
|
|
29
27
|
switch (action.type) {
|
|
30
|
-
// Management actions
|
|
31
28
|
case "SIT" /* ActionType.SIT */:
|
|
32
29
|
newState = (0, management_1.handleSit)(state, action);
|
|
33
30
|
break;
|
|
@@ -40,11 +37,9 @@ function gameReducer(state, action) {
|
|
|
40
37
|
case "RESERVE_SEAT" /* ActionType.RESERVE_SEAT */:
|
|
41
38
|
newState = (0, management_1.handleReserveSeat)(state, action);
|
|
42
39
|
break;
|
|
43
|
-
// Dealing
|
|
44
40
|
case "DEAL" /* ActionType.DEAL */:
|
|
45
41
|
newState = (0, dealing_1.handleDeal)(state, action);
|
|
46
42
|
break;
|
|
47
|
-
// Betting actions
|
|
48
43
|
case "FOLD" /* ActionType.FOLD */:
|
|
49
44
|
newState = (0, betting_1.handleFold)(state, action);
|
|
50
45
|
break;
|
|
@@ -54,14 +49,12 @@ function gameReducer(state, action) {
|
|
|
54
49
|
case "CALL" /* ActionType.CALL */:
|
|
55
50
|
newState = (0, betting_1.handleCall)(state, action);
|
|
56
51
|
break;
|
|
52
|
+
// Auto-convert BET to RAISE/CALL when there's already a wager.
|
|
53
|
+
// Note: action.amount is the TOTAL bet, not the increment.
|
|
57
54
|
case "BET" /* ActionType.BET */:
|
|
58
|
-
// Auto-convert BET to appropriate action if there's already a bet
|
|
59
|
-
// This handles UI implementations that don't distinguish between BET/CALL/RAISE buttons
|
|
60
|
-
// Note: action.amount is the TOTAL bet size, not the amount to add
|
|
61
55
|
const currentBet = Math.max(...Array.from(state.currentBets.values()), 0);
|
|
62
56
|
if (currentBet > 0 && "amount" in action) {
|
|
63
57
|
if (action.amount === currentBet) {
|
|
64
|
-
// Amount equals current bet -> Convert to CALL
|
|
65
58
|
const callAction = {
|
|
66
59
|
type: "CALL" /* ActionType.CALL */,
|
|
67
60
|
playerId: action.playerId,
|
|
@@ -70,7 +63,6 @@ function gameReducer(state, action) {
|
|
|
70
63
|
newState = (0, betting_1.handleCall)(state, callAction);
|
|
71
64
|
}
|
|
72
65
|
else if (action.amount > currentBet) {
|
|
73
|
-
// Amount exceeds current bet -> Convert to RAISE
|
|
74
66
|
const raiseAction = {
|
|
75
67
|
type: "RAISE" /* ActionType.RAISE */,
|
|
76
68
|
playerId: action.playerId,
|
|
@@ -80,7 +72,7 @@ function gameReducer(state, action) {
|
|
|
80
72
|
newState = (0, betting_1.handleRaise)(state, raiseAction);
|
|
81
73
|
}
|
|
82
74
|
else {
|
|
83
|
-
//
|
|
75
|
+
// amount < currentBet: pass through (will fail validation)
|
|
84
76
|
newState = (0, betting_1.handleBet)(state, action);
|
|
85
77
|
}
|
|
86
78
|
}
|
|
@@ -91,14 +83,12 @@ function gameReducer(state, action) {
|
|
|
91
83
|
case "RAISE" /* ActionType.RAISE */:
|
|
92
84
|
newState = (0, betting_1.handleRaise)(state, action);
|
|
93
85
|
break;
|
|
94
|
-
// Showdown actions
|
|
95
86
|
case "SHOW" /* ActionType.SHOW */:
|
|
96
87
|
newState = (0, showdown_actions_1.handleShow)(state, action);
|
|
97
88
|
break;
|
|
98
89
|
case "MUCK" /* ActionType.MUCK */:
|
|
99
90
|
newState = (0, showdown_actions_1.handleMuck)(state, action);
|
|
100
91
|
break;
|
|
101
|
-
// Special actions
|
|
102
92
|
case "TIMEOUT" /* ActionType.TIMEOUT */:
|
|
103
93
|
newState = (0, special_1.handleTimeout)(state, action);
|
|
104
94
|
break;
|
|
@@ -109,33 +99,24 @@ function gameReducer(state, action) {
|
|
|
109
99
|
newState = (0, tournament_1.handleNextBlindLevel)(state, action);
|
|
110
100
|
break;
|
|
111
101
|
default:
|
|
112
|
-
// Unknown action type
|
|
113
102
|
newState = state;
|
|
114
103
|
break;
|
|
115
104
|
}
|
|
116
|
-
//
|
|
105
|
+
// Recalculate pots, then progress street.
|
|
117
106
|
if ((0, street_progression_1.shouldProgressStreet)(newState)) {
|
|
118
|
-
// Recalculate pots before progressing
|
|
119
107
|
newState = (0, side_pots_1.recalculatePots)(newState);
|
|
120
108
|
newState = (0, street_progression_1.progressStreet)(newState);
|
|
121
109
|
}
|
|
122
|
-
// Check if we should go to showdown
|
|
123
110
|
if ((0, showdown_1.shouldShowdown)(newState)) {
|
|
124
|
-
newState = {
|
|
125
|
-
...newState,
|
|
126
|
-
street: newState.street, // Keep current street for showdown
|
|
127
|
-
};
|
|
128
111
|
newState = (0, showdown_1.determineWinners)(newState);
|
|
129
112
|
}
|
|
130
113
|
if (newState.actionTo !== null && newState.players[newState.actionTo]?.isSittingOut) {
|
|
131
114
|
newState = { ...newState, actionTo: null };
|
|
132
115
|
}
|
|
133
|
-
//
|
|
134
|
-
// Enabled by default, can be disabled via config (not recommended for production)
|
|
116
|
+
// Integrity check; can be disabled via config (not recommended for production).
|
|
135
117
|
if (newState.config.validateIntegrity !== false) {
|
|
136
118
|
(0, invariants_1.validateGameStateIntegrity)(newState);
|
|
137
119
|
}
|
|
138
|
-
// Add to previous states for undo
|
|
139
120
|
const previousStates = [...newState.previousStates, state].slice(-constants_1.MAX_UNDO_HISTORY);
|
|
140
121
|
return {
|
|
141
122
|
...newState,
|
|
@@ -12,20 +12,18 @@ const validation_1 = require("../utils/validation");
|
|
|
12
12
|
* Wraps the pure reducer with a stateful API
|
|
13
13
|
*/
|
|
14
14
|
class PokerEngine {
|
|
15
|
+
currentState;
|
|
16
|
+
listeners = [];
|
|
17
|
+
timeProvider;
|
|
15
18
|
constructor(config, timeProvider = () => Date.now()) {
|
|
16
|
-
this.listeners = [];
|
|
17
|
-
// Validate config
|
|
18
19
|
this.validateConfig(config);
|
|
19
|
-
// Initialize time provider
|
|
20
20
|
this.timeProvider = timeProvider;
|
|
21
|
-
// Initialize state
|
|
22
21
|
this.currentState = this.createInitialState(config);
|
|
23
22
|
}
|
|
24
23
|
/**
|
|
25
24
|
* Add a player to the table
|
|
26
25
|
*/
|
|
27
26
|
sit(seat, id, name, stack) {
|
|
28
|
-
// Validate chip amount is a non-negative integer
|
|
29
27
|
(0, validation_1.validateChipAmount)(stack, "Sit stack");
|
|
30
28
|
const action = {
|
|
31
29
|
type: "SIT" /* ActionType.SIT */,
|
|
@@ -63,13 +61,10 @@ class PokerEngine {
|
|
|
63
61
|
* If action.timestamp is not provided, the engine will automatically set it
|
|
64
62
|
*/
|
|
65
63
|
act(action) {
|
|
66
|
-
// Ensure timestamp is set
|
|
67
64
|
const timestamp = action.timestamp ?? this.timeProvider();
|
|
68
|
-
// Validate timestamp if provided by caller
|
|
69
65
|
if (action.timestamp !== undefined) {
|
|
70
66
|
(0, validation_1.validateTimestamp)(timestamp, this.currentState.timestamp);
|
|
71
67
|
}
|
|
72
|
-
// Validate chip amounts for betting actions
|
|
73
68
|
if ("amount" in action && typeof action.amount === "number") {
|
|
74
69
|
(0, validation_1.validateChipAmount)(action.amount, `${action.type} amount`);
|
|
75
70
|
}
|
|
@@ -86,9 +81,7 @@ class PokerEngine {
|
|
|
86
81
|
*/
|
|
87
82
|
validate(action) {
|
|
88
83
|
try {
|
|
89
|
-
// Dry-run the reducer
|
|
90
|
-
// We don't need to deep clone state because reducer is immutable
|
|
91
|
-
// and pure, and we discard the result.
|
|
84
|
+
// Dry-run the reducer; we discard the result since it's immutable and pure.
|
|
92
85
|
(0, game_reducer_1.gameReducer)(this.currentState, action);
|
|
93
86
|
return { valid: true };
|
|
94
87
|
}
|
|
@@ -110,11 +103,8 @@ class PokerEngine {
|
|
|
110
103
|
// Hydrate PublicState into GameState if needed
|
|
111
104
|
const newState = {
|
|
112
105
|
...serverState,
|
|
113
|
-
// Ensure deck exists (empty for client/public state)
|
|
114
106
|
deck: "deck" in serverState ? serverState.deck : [],
|
|
115
|
-
|
|
116
|
-
players: serverState.players, // Type assertion needed due to deep readonly/mutable mismatch potential
|
|
117
|
-
// Ensure config carries isClient flag if set locally
|
|
107
|
+
players: serverState.players,
|
|
118
108
|
config: {
|
|
119
109
|
...serverState.config,
|
|
120
110
|
isClient: this.currentState.config.isClient,
|
|
@@ -174,7 +164,6 @@ class PokerEngine {
|
|
|
174
164
|
*/
|
|
175
165
|
on(callback) {
|
|
176
166
|
this.listeners.push(callback);
|
|
177
|
-
// Return unsubscribe function
|
|
178
167
|
return () => {
|
|
179
168
|
const index = this.listeners.indexOf(callback);
|
|
180
169
|
if (index > -1) {
|
|
@@ -191,9 +180,8 @@ class PokerEngine {
|
|
|
191
180
|
}
|
|
192
181
|
const nextLevel = this.currentState.blindLevel + 1;
|
|
193
182
|
if (nextLevel >= this.currentState.config.blindStructure.length) {
|
|
194
|
-
return;
|
|
183
|
+
return;
|
|
195
184
|
}
|
|
196
|
-
// Dispatch NEXT_BLIND_LEVEL action to notify listeners
|
|
197
185
|
this.dispatch({
|
|
198
186
|
type: "NEXT_BLIND_LEVEL" /* ActionType.NEXT_BLIND_LEVEL */,
|
|
199
187
|
timestamp: this.timeProvider(),
|
|
@@ -223,13 +211,11 @@ class PokerEngine {
|
|
|
223
211
|
const oldState = this.currentState;
|
|
224
212
|
try {
|
|
225
213
|
this.currentState = (0, game_reducer_1.gameReducer)(this.currentState, action);
|
|
226
|
-
// Notify listeners
|
|
227
214
|
for (const listener of this.listeners) {
|
|
228
215
|
listener(action, oldState, this.currentState);
|
|
229
216
|
}
|
|
230
217
|
}
|
|
231
218
|
catch (error) {
|
|
232
|
-
// Re-throw error but keep old state
|
|
233
219
|
throw error;
|
|
234
220
|
}
|
|
235
221
|
}
|
package/dist/errors/index.js
CHANGED
|
@@ -14,9 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
// Export all errors
|
|
18
17
|
__exportStar(require("./poker-engine-error"), exports);
|
|
19
18
|
__exportStar(require("./critical-state-error"), exports);
|
|
20
19
|
__exportStar(require("./illegal-action-error"), exports);
|
|
21
20
|
__exportStar(require("./config-error"), exports);
|
|
22
|
-
// ErrorCodes now exported from @pokertools/types
|
|
@@ -11,25 +11,19 @@ exports.exportToPokerStars = exportToPokerStars;
|
|
|
11
11
|
*/
|
|
12
12
|
function exportToPokerStars(history, options = { format: "pokerstars" }) {
|
|
13
13
|
const lines = [];
|
|
14
|
-
// Header line
|
|
15
14
|
lines.push(buildHeader(history));
|
|
16
15
|
lines.push(buildTableInfo(history));
|
|
17
|
-
// Button and seats
|
|
18
16
|
lines.push(`Button: seat #${history.buttonSeat + 1}`);
|
|
19
17
|
lines.push("");
|
|
20
|
-
// Player stacks
|
|
21
18
|
for (const player of history.players) {
|
|
22
19
|
lines.push(`Seat ${player.seat + 1}: ${player.name} ($${player.startingStack.toFixed(2)} in chips)`);
|
|
23
20
|
}
|
|
24
21
|
lines.push("");
|
|
25
|
-
// Blinds and antes
|
|
26
22
|
lines.push(buildBlindsLine(history));
|
|
27
23
|
if (history.stakes.ante > 0) {
|
|
28
24
|
lines.push(buildAntesLine(history));
|
|
29
25
|
}
|
|
30
|
-
// Hole cards (dealt to)
|
|
31
26
|
lines.push(buildHoleCardsLine(history, options));
|
|
32
|
-
// Each street
|
|
33
27
|
for (const street of history.streets) {
|
|
34
28
|
lines.push("");
|
|
35
29
|
lines.push(buildStreetHeader(street));
|
|
@@ -37,7 +31,6 @@ function exportToPokerStars(history, options = { format: "pokerstars" }) {
|
|
|
37
31
|
lines.push(buildActionLine(action, history));
|
|
38
32
|
}
|
|
39
33
|
}
|
|
40
|
-
// Summary
|
|
41
34
|
lines.push("");
|
|
42
35
|
lines.push(buildSummary(history));
|
|
43
36
|
return lines.join("\n");
|
|
@@ -61,7 +54,6 @@ function buildTableInfo(history) {
|
|
|
61
54
|
*/
|
|
62
55
|
function buildBlindsLine(history) {
|
|
63
56
|
const lines = [];
|
|
64
|
-
// Find SB and BB seats (button+1 and button+2)
|
|
65
57
|
const sbSeat = (history.buttonSeat + 1) % history.maxPlayers;
|
|
66
58
|
const bbSeat = (history.buttonSeat + 2) % history.maxPlayers;
|
|
67
59
|
const sbPlayer = history.players.find((p) => p.seat === sbSeat);
|
|
@@ -84,10 +84,9 @@ function groupActionsByStreet(state) {
|
|
|
84
84
|
grouped.set("TURN" /* Street.TURN */, []);
|
|
85
85
|
grouped.set("RIVER" /* Street.RIVER */, []);
|
|
86
86
|
grouped.set("SHOWDOWN" /* Street.SHOWDOWN */, []);
|
|
87
|
-
// Group action history by street
|
|
88
87
|
for (const record of state.actionHistory) {
|
|
89
88
|
if (record.seat === null)
|
|
90
|
-
continue;
|
|
89
|
+
continue;
|
|
91
90
|
const street = record.street ?? state.street;
|
|
92
91
|
const existing = grouped.get(street) ?? [];
|
|
93
92
|
const actionRecord = {
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,6 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
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
18
|
var poker_engine_1 = require("./engine/poker-engine");
|
|
20
19
|
Object.defineProperty(exports, "PokerEngine", { enumerable: true, get: function () { return poker_engine_1.PokerEngine; } });
|
|
21
20
|
// Types (re-exported from @pokertools/types for convenience)
|