@pokertools/engine 1.0.8 → 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -5
- package/dist/.tsbuildinfo +1 -1
- package/dist/actions/betting.js +23 -73
- package/dist/actions/dealing.js +29 -56
- package/dist/actions/management.js +3 -1
- package/dist/actions/{showdownActions.js → showdown-actions.js} +13 -27
- package/dist/actions/special.js +20 -7
- package/dist/actions/{streetProgression.js → street-progression.js} +16 -31
- package/dist/actions/tournament.js +1 -4
- package/dist/actions/validation.js +48 -33
- package/dist/browser.d.ts +5 -5
- package/dist/browser.js +6 -7
- package/dist/engine/{gameReducer.js → game-reducer.js} +16 -32
- package/dist/engine/{PokerEngine.js → poker-engine.js} +16 -30
- package/dist/errors/{ConfigError.d.ts → config-error.d.ts} +1 -1
- package/dist/errors/{ConfigError.js → config-error.js} +2 -2
- package/dist/errors/{CriticalStateError.d.ts → critical-state-error.d.ts} +1 -1
- package/dist/errors/{CriticalStateError.js → critical-state-error.js} +2 -2
- package/dist/errors/{IllegalActionError.d.ts → illegal-action-error.d.ts} +2 -2
- package/dist/errors/{IllegalActionError.js → illegal-action-error.js} +2 -2
- package/dist/errors/index.d.ts +4 -4
- package/dist/errors/index.js +4 -6
- package/dist/errors/{PokerEngineError.js → poker-engine-error.js} +2 -0
- package/dist/history/exporter.js +4 -4
- package/dist/history/formats/pokerstars.js +0 -8
- package/dist/history/{handHistoryBuilder.js → hand-history-builder.js} +1 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -5
- package/dist/rules/{actionOrder.js → action-order.js} +12 -17
- package/dist/rules/blinds.js +3 -3
- package/dist/rules/{headsUp.js → heads-up.js} +0 -1
- package/dist/rules/showdown.js +45 -29
- package/dist/rules/{sidePots.js → side-pots.js} +7 -20
- package/dist/utils/deck.js +1 -4
- package/dist/utils/invariants.js +16 -10
- package/dist/utils/rake.js +6 -8
- package/dist/utils/serialization.d.ts +1 -0
- package/dist/utils/serialization.js +10 -5
- package/dist/utils/validation.js +8 -8
- package/dist/utils/{viewMasking.js → view-masking.js} +16 -6
- package/package.json +6 -6
- package/dist/errors/ErrorCodes.d.ts +0 -7
- package/dist/errors/ErrorCodes.js +0 -12
- /package/dist/actions/{showdownActions.d.ts → showdown-actions.d.ts} +0 -0
- /package/dist/actions/{streetProgression.d.ts → street-progression.d.ts} +0 -0
- /package/dist/engine/{gameReducer.d.ts → game-reducer.d.ts} +0 -0
- /package/dist/engine/{PokerEngine.d.ts → poker-engine.d.ts} +0 -0
- /package/dist/errors/{PokerEngineError.d.ts → poker-engine-error.d.ts} +0 -0
- /package/dist/history/{handHistoryBuilder.d.ts → hand-history-builder.d.ts} +0 -0
- /package/dist/rules/{actionOrder.d.ts → action-order.d.ts} +0 -0
- /package/dist/rules/{headsUp.d.ts → heads-up.d.ts} +0 -0
- /package/dist/rules/{sidePots.d.ts → side-pots.d.ts} +0 -0
- /package/dist/utils/{cardUtils.d.ts → card-utils.d.ts} +0 -0
- /package/dist/utils/{cardUtils.js → card-utils.js} +0 -0
- /package/dist/utils/{viewMasking.d.ts → view-masking.d.ts} +0 -0
|
@@ -2,48 +2,39 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.handleShow = handleShow;
|
|
4
4
|
exports.handleMuck = handleMuck;
|
|
5
|
-
const
|
|
6
|
-
const
|
|
5
|
+
const illegal_action_error_1 = require("../errors/illegal-action-error");
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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: {
|
package/dist/actions/special.js
CHANGED
|
@@ -3,7 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.handleTimeout = handleTimeout;
|
|
4
4
|
exports.handleTimeBank = handleTimeBank;
|
|
5
5
|
const positioning_1 = require("../utils/positioning");
|
|
6
|
-
const
|
|
6
|
+
const action_order_1 = require("../rules/action-order");
|
|
7
|
+
function getTotalPot(state) {
|
|
8
|
+
let total = 0;
|
|
9
|
+
for (const pot of state.pots)
|
|
10
|
+
total += pot.amount;
|
|
11
|
+
for (const bet of state.currentBets.values())
|
|
12
|
+
total += bet;
|
|
13
|
+
return total;
|
|
14
|
+
}
|
|
7
15
|
/**
|
|
8
16
|
* Handle TIMEOUT action
|
|
9
17
|
* - Folds player if they have bet to call
|
|
@@ -16,13 +24,11 @@ function handleTimeout(state, action) {
|
|
|
16
24
|
return state;
|
|
17
25
|
}
|
|
18
26
|
const { player, seat } = result;
|
|
19
|
-
// Determine if player needs to call
|
|
20
27
|
const currentBet = getCurrentBet(state);
|
|
21
28
|
const playerBet = state.currentBets.get(seat) ?? 0;
|
|
22
29
|
const needsToCall = currentBet > playerBet;
|
|
23
30
|
const newPlayers = [...state.players];
|
|
24
31
|
if (needsToCall) {
|
|
25
|
-
// Player must fold
|
|
26
32
|
newPlayers[seat] = {
|
|
27
33
|
...player,
|
|
28
34
|
status: "FOLDED" /* PlayerStatus.FOLDED */,
|
|
@@ -30,7 +36,6 @@ function handleTimeout(state, action) {
|
|
|
30
36
|
};
|
|
31
37
|
}
|
|
32
38
|
else {
|
|
33
|
-
// Player can check, but mark as sitting out
|
|
34
39
|
newPlayers[seat] = {
|
|
35
40
|
...player,
|
|
36
41
|
isSittingOut: true,
|
|
@@ -39,17 +44,25 @@ function handleTimeout(state, action) {
|
|
|
39
44
|
const newActivePlayers = needsToCall
|
|
40
45
|
? state.activePlayers.filter((s) => s !== seat)
|
|
41
46
|
: state.activePlayers;
|
|
47
|
+
const actionRecord = {
|
|
48
|
+
action,
|
|
49
|
+
seat,
|
|
50
|
+
resultingPot: getTotalPot(state),
|
|
51
|
+
resultingStack: newPlayers[seat]?.stack ?? 0,
|
|
52
|
+
street: state.street,
|
|
53
|
+
};
|
|
42
54
|
const newState = {
|
|
43
55
|
...state,
|
|
44
56
|
players: newPlayers,
|
|
45
57
|
activePlayers: newActivePlayers,
|
|
58
|
+
actionHistory: [...state.actionHistory, actionRecord],
|
|
46
59
|
timestamp: action.timestamp,
|
|
47
60
|
};
|
|
48
|
-
|
|
49
|
-
const
|
|
61
|
+
const nextToAct = (0, action_order_1.getNextToAct)(newState);
|
|
62
|
+
const actionableNext = nextToAct !== null && !newPlayers[nextToAct]?.isSittingOut ? nextToAct : null;
|
|
50
63
|
return {
|
|
51
64
|
...newState,
|
|
52
|
-
actionTo:
|
|
65
|
+
actionTo: actionableNext === seat ? null : actionableNext,
|
|
53
66
|
};
|
|
54
67
|
}
|
|
55
68
|
/**
|
|
@@ -3,8 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.progressStreet = progressStreet;
|
|
4
4
|
exports.shouldProgressStreet = shouldProgressStreet;
|
|
5
5
|
const deck_1 = require("../utils/deck");
|
|
6
|
-
const
|
|
7
|
-
const
|
|
6
|
+
const card_utils_1 = require("../utils/card-utils");
|
|
7
|
+
const action_order_1 = require("../rules/action-order");
|
|
8
8
|
/**
|
|
9
9
|
* Progress to next street
|
|
10
10
|
* - Collects bets into pots
|
|
@@ -12,33 +12,26 @@ const actionOrder_1 = require("../rules/actionOrder");
|
|
|
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
|
-
|
|
41
|
-
const firstToAct = (0, actionOrder_1.getFirstToAct)(newState);
|
|
34
|
+
const firstToAct = (0, action_order_1.getFirstToAct)(newState);
|
|
42
35
|
return {
|
|
43
36
|
...newState,
|
|
44
37
|
actionTo: firstToAct,
|
|
@@ -69,21 +62,21 @@ function dealCommunityCards(state, street) {
|
|
|
69
62
|
// Burn 1, deal 3
|
|
70
63
|
const [flopCards, flopDeck] = (0, deck_1.burnAndDeal)(deck, 3);
|
|
71
64
|
return {
|
|
72
|
-
board: [...currentBoard, ...(0,
|
|
65
|
+
board: [...currentBoard, ...(0, card_utils_1.cardCodesToStrings)(flopCards)],
|
|
73
66
|
deck: flopDeck,
|
|
74
67
|
};
|
|
75
68
|
case "TURN" /* Street.TURN */:
|
|
76
69
|
// Burn 1, deal 1
|
|
77
70
|
const [turnCards, turnDeck] = (0, deck_1.burnAndDeal)(deck, 1);
|
|
78
71
|
return {
|
|
79
|
-
board: [...currentBoard, ...(0,
|
|
72
|
+
board: [...currentBoard, ...(0, card_utils_1.cardCodesToStrings)(turnCards)],
|
|
80
73
|
deck: turnDeck,
|
|
81
74
|
};
|
|
82
75
|
case "RIVER" /* Street.RIVER */:
|
|
83
76
|
// Burn 1, deal 1
|
|
84
77
|
const [riverCards, riverDeck] = (0, deck_1.burnAndDeal)(deck, 1);
|
|
85
78
|
return {
|
|
86
|
-
board: [...currentBoard, ...(0,
|
|
79
|
+
board: [...currentBoard, ...(0, card_utils_1.cardCodesToStrings)(riverCards)],
|
|
87
80
|
deck: riverDeck,
|
|
88
81
|
};
|
|
89
82
|
default:
|
|
@@ -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
|
-
|
|
154
|
-
}
|
|
155
|
-
// Check if all active players have acted
|
|
156
|
-
return (0, actionOrder_1.isActionComplete)(state);
|
|
137
|
+
if (state.actionTo !== null)
|
|
138
|
+
return false;
|
|
139
|
+
if (state.street === "SHOWDOWN" /* Street.SHOWDOWN */)
|
|
140
|
+
return false;
|
|
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 */,
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.validateAction = validateAction;
|
|
4
|
-
const
|
|
5
|
-
const
|
|
4
|
+
const illegal_action_error_1 = require("../errors/illegal-action-error");
|
|
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,58 +43,58 @@ function validateAction(state, action) {
|
|
|
44
43
|
}
|
|
45
44
|
function validateBettingAction(state, action) {
|
|
46
45
|
if (!("playerId" in action)) {
|
|
47
|
-
throw new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 */:
|
|
89
84
|
// Note: We allow BET even when currentBet > 0 because the reducer will auto-convert it to RAISE or CALL
|
|
90
85
|
// This handles UI implementations that don't distinguish between BET and RAISE buttons
|
|
91
86
|
if ("amount" in action) {
|
|
87
|
+
if (currentBet > 0) {
|
|
88
|
+
validateBetAsRaise(state, action, seat, playerBet, currentBet, player.stack);
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
92
91
|
// Reject bets below the current bet (string bet exploit)
|
|
93
|
-
if (
|
|
94
|
-
throw new
|
|
92
|
+
if (action.amount < currentBet) {
|
|
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 });
|
|
95
94
|
}
|
|
96
95
|
// Reject bets below big blind (when no current bet)
|
|
97
96
|
if (action.amount < state.bigBlind && action.amount < player.stack) {
|
|
98
|
-
throw new
|
|
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 });
|
|
99
98
|
}
|
|
100
99
|
}
|
|
101
100
|
break;
|
|
@@ -104,26 +103,26 @@ function validateBettingAction(state, action) {
|
|
|
104
103
|
// intermediate actions (like calls or incomplete all-in raises) did NOT
|
|
105
104
|
// reopen the betting. Therefore, they cannot re-raise their own bet.
|
|
106
105
|
if (state.lastAggressorSeat === seat) {
|
|
107
|
-
throw new
|
|
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)", {
|
|
108
107
|
playerId: action.playerId,
|
|
109
108
|
seat,
|
|
110
109
|
lastAggressor: state.lastAggressorSeat,
|
|
111
110
|
});
|
|
112
111
|
}
|
|
113
112
|
if (currentBet === 0) {
|
|
114
|
-
throw new
|
|
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 });
|
|
115
114
|
}
|
|
116
115
|
if ("amount" in action) {
|
|
117
116
|
// Check if player is going all-in (incomplete raise exception)
|
|
118
117
|
const isAllIn = action.amount >= playerBet + player.stack;
|
|
119
118
|
// Reject raises that don't exceed current bet (unless all-in)
|
|
120
119
|
if (action.amount <= currentBet && !isAllIn) {
|
|
121
|
-
throw new
|
|
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 });
|
|
122
121
|
}
|
|
123
122
|
// Check minimum raise requirement (unless player is going all-in)
|
|
124
123
|
const raiseIncrement = action.amount - currentBet;
|
|
125
124
|
if (!isAllIn && action.amount < state.minRaise) {
|
|
126
|
-
throw new
|
|
125
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.RAISE_TOO_SMALL, `Raise to ${action.amount} is below minimum ${state.minRaise}`, {
|
|
127
126
|
amount: action.amount,
|
|
128
127
|
currentBet,
|
|
129
128
|
raiseIncrement,
|
|
@@ -134,63 +133,79 @@ function validateBettingAction(state, action) {
|
|
|
134
133
|
break;
|
|
135
134
|
}
|
|
136
135
|
}
|
|
136
|
+
function validateBetAsRaise(state, action, seat, playerBet, currentBet, playerStack) {
|
|
137
|
+
if (!("amount" in action))
|
|
138
|
+
return;
|
|
139
|
+
const amount = action.amount;
|
|
140
|
+
if (typeof amount !== "number")
|
|
141
|
+
return;
|
|
142
|
+
if (state.lastAggressorSeat === seat) {
|
|
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 });
|
|
144
|
+
}
|
|
145
|
+
const isAllIn = amount >= playerBet + playerStack;
|
|
146
|
+
if (amount <= currentBet && !isAllIn) {
|
|
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 });
|
|
148
|
+
}
|
|
149
|
+
if (!isAllIn && amount < state.minRaise) {
|
|
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 });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
137
153
|
function validateDealAction(state) {
|
|
138
154
|
if (state.street !== "PREFLOP" /* Street.PREFLOP */ || state.handNumber > 0) {
|
|
139
155
|
// Allow dealing if we're at showdown (hand complete) or haven't started
|
|
140
156
|
if (state.street !== "SHOWDOWN" /* Street.SHOWDOWN */ && state.handNumber > 0) {
|
|
141
|
-
throw new
|
|
157
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.CANNOT_DEAL, "Cannot deal while hand is in progress", { street: state.street });
|
|
142
158
|
}
|
|
143
159
|
}
|
|
144
|
-
// Check we have enough players
|
|
145
160
|
const activePlayers = state.players.filter((p) => p && p.stack > 0 && !p.isSittingOut);
|
|
146
161
|
if (activePlayers.length < 2) {
|
|
147
|
-
throw new
|
|
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 });
|
|
148
163
|
}
|
|
149
164
|
}
|
|
150
165
|
function validateSitAction(state, action) {
|
|
151
166
|
if (action.seat < 0 || action.seat >= state.maxPlayers) {
|
|
152
|
-
throw new
|
|
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 });
|
|
153
168
|
}
|
|
154
169
|
const existingPlayer = state.players[action.seat];
|
|
155
170
|
if (existingPlayer !== null) {
|
|
156
171
|
// Allow claiming the seat if it is RESERVED by THIS player
|
|
157
172
|
const isMyReservation = existingPlayer.status === "RESERVED" /* PlayerStatus.RESERVED */ && existingPlayer.id === action.playerId;
|
|
158
173
|
if (!isMyReservation) {
|
|
159
|
-
throw new
|
|
174
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.SEAT_OCCUPIED, `Seat ${action.seat} is already occupied`, { seat: action.seat });
|
|
160
175
|
}
|
|
161
176
|
}
|
|
162
177
|
if (action.stack <= 0) {
|
|
163
|
-
throw new
|
|
178
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.INVALID_STACK, `Stack must be positive, got ${action.stack}`, { stack: action.stack });
|
|
164
179
|
}
|
|
165
180
|
}
|
|
166
181
|
function validateStandAction(state, action) {
|
|
167
182
|
const result = (0, positioning_1.getPlayerById)(state, action.playerId);
|
|
168
183
|
if (!result) {
|
|
169
|
-
throw new
|
|
184
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.PLAYER_NOT_FOUND, `Player ${action.playerId} not found`, { playerId: action.playerId });
|
|
170
185
|
}
|
|
171
186
|
}
|
|
172
187
|
function validateTimeAction(state, action) {
|
|
173
188
|
const result = (0, positioning_1.getPlayerById)(state, action.playerId);
|
|
174
189
|
if (!result) {
|
|
175
|
-
throw new
|
|
190
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.PLAYER_NOT_FOUND, `Player ${action.playerId} not found`, { playerId: action.playerId });
|
|
176
191
|
}
|
|
177
192
|
const { seat } = result;
|
|
178
193
|
if (state.actionTo !== seat) {
|
|
179
|
-
throw new
|
|
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 });
|
|
180
195
|
}
|
|
181
196
|
}
|
|
182
197
|
function validateAddChipsAction(state, action) {
|
|
183
198
|
const result = (0, positioning_1.getPlayerById)(state, action.playerId);
|
|
184
199
|
if (!result) {
|
|
185
|
-
throw new
|
|
200
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.PLAYER_NOT_FOUND, `Player ${action.playerId} not found`, { playerId: action.playerId });
|
|
186
201
|
}
|
|
187
202
|
}
|
|
188
203
|
function validateReserveSeatAction(state, action) {
|
|
189
204
|
if (action.seat < 0 || action.seat >= state.maxPlayers) {
|
|
190
|
-
throw new
|
|
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 });
|
|
191
206
|
}
|
|
192
207
|
if (state.players[action.seat] !== null) {
|
|
193
|
-
throw new
|
|
208
|
+
throw new illegal_action_error_1.IllegalActionError(types_1.ErrorCodes.SEAT_OCCUPIED, `Seat ${action.seat} is already occupied`, { seat: action.seat });
|
|
194
209
|
}
|
|
195
210
|
}
|
|
196
211
|
/**
|
package/dist/browser.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* This file provides a browser-safe RNG using Web Crypto API
|
|
5
5
|
* and re-exports all engine functionality for use in web applications.
|
|
6
6
|
*/
|
|
7
|
-
import { PokerEngine } from "./engine/
|
|
7
|
+
import { PokerEngine } from "./engine/poker-engine";
|
|
8
8
|
import type { TableConfig } from "@pokertools/types";
|
|
9
9
|
/**
|
|
10
10
|
* Browser-compatible RNG using Web Crypto API
|
|
@@ -15,13 +15,13 @@ export declare function getBrowserRNG(): () => number;
|
|
|
15
15
|
* Create a PokerEngine instance with browser-compatible RNG
|
|
16
16
|
*/
|
|
17
17
|
export declare function createBrowserEngine(config: TableConfig): PokerEngine;
|
|
18
|
-
export * from "./engine/
|
|
18
|
+
export * from "./engine/poker-engine";
|
|
19
19
|
export * from "./actions/betting";
|
|
20
20
|
export * from "./actions/dealing";
|
|
21
21
|
export * from "./actions/management";
|
|
22
|
-
export * from "./actions/
|
|
22
|
+
export * from "./actions/showdown-actions";
|
|
23
23
|
export * from "./actions/special";
|
|
24
|
-
export * from "./utils/
|
|
24
|
+
export * from "./utils/view-masking";
|
|
25
25
|
export * from "./utils/serialization";
|
|
26
|
-
export * from "./utils/
|
|
26
|
+
export * from "./utils/card-utils";
|
|
27
27
|
export * from "./history/exporter";
|
package/dist/browser.js
CHANGED
|
@@ -22,13 +22,12 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
22
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
23
|
exports.getBrowserRNG = getBrowserRNG;
|
|
24
24
|
exports.createBrowserEngine = createBrowserEngine;
|
|
25
|
-
const
|
|
25
|
+
const poker_engine_1 = require("./engine/poker-engine");
|
|
26
26
|
/**
|
|
27
27
|
* Browser-compatible RNG using Web Crypto API
|
|
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);
|
|
@@ -55,19 +54,19 @@ function getBrowserRNG() {
|
|
|
55
54
|
* Create a PokerEngine instance with browser-compatible RNG
|
|
56
55
|
*/
|
|
57
56
|
function createBrowserEngine(config) {
|
|
58
|
-
return new
|
|
57
|
+
return new poker_engine_1.PokerEngine({
|
|
59
58
|
...config,
|
|
60
59
|
randomProvider: getBrowserRNG(),
|
|
61
60
|
});
|
|
62
61
|
}
|
|
63
62
|
// Re-export everything from main engine
|
|
64
|
-
__exportStar(require("./engine/
|
|
63
|
+
__exportStar(require("./engine/poker-engine"), exports);
|
|
65
64
|
__exportStar(require("./actions/betting"), exports);
|
|
66
65
|
__exportStar(require("./actions/dealing"), exports);
|
|
67
66
|
__exportStar(require("./actions/management"), exports);
|
|
68
|
-
__exportStar(require("./actions/
|
|
67
|
+
__exportStar(require("./actions/showdown-actions"), exports);
|
|
69
68
|
__exportStar(require("./actions/special"), exports);
|
|
70
|
-
__exportStar(require("./utils/
|
|
69
|
+
__exportStar(require("./utils/view-masking"), exports);
|
|
71
70
|
__exportStar(require("./utils/serialization"), exports);
|
|
72
|
-
__exportStar(require("./utils/
|
|
71
|
+
__exportStar(require("./utils/card-utils"), exports);
|
|
73
72
|
__exportStar(require("./history/exporter"), exports);
|