@pokertools/engine 1.0.0 → 1.0.4
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 +590 -444
- package/dist/.tsbuildinfo +1 -0
- package/dist/actions/betting.js +83 -50
- package/dist/actions/dealing.js +118 -27
- package/dist/actions/management.d.ts +12 -1
- package/dist/actions/management.js +86 -5
- package/dist/actions/special.d.ts +18 -0
- package/dist/actions/special.js +20 -0
- package/dist/actions/validation.js +27 -2
- package/dist/browser.d.ts +27 -0
- package/dist/browser.js +73 -0
- package/dist/engine/PokerEngine.d.ts +23 -2
- package/dist/engine/PokerEngine.js +54 -2
- package/dist/engine/gameReducer.js +6 -0
- package/dist/errors/ErrorCodes.d.ts +4 -35
- package/dist/errors/ErrorCodes.js +7 -41
- package/dist/errors/index.d.ts +0 -1
- package/dist/errors/index.js +1 -1
- package/dist/history/exporter.d.ts +1 -2
- package/dist/history/formats/json.d.ts +1 -1
- package/dist/history/formats/pokerstars.d.ts +1 -1
- package/dist/history/handHistoryBuilder.d.ts +1 -2
- package/dist/history/handHistoryBuilder.js +4 -1
- package/dist/index.d.ts +1 -1
- package/dist/rules/actionOrder.js +4 -4
- package/dist/rules/blinds.d.ts +2 -0
- package/dist/rules/blinds.js +27 -3
- package/dist/rules/headsUp.js +18 -0
- package/dist/rules/showdown.js +10 -0
- package/dist/utils/cardUtils.d.ts +2 -1
- package/dist/utils/cardUtils.js +2 -1
- package/dist/utils/invariants.js +4 -0
- package/dist/utils/positioning.js +2 -2
- package/dist/utils/serialization.d.ts +1 -0
- package/dist/utils/serialization.js +2 -0
- package/dist/utils/viewMasking.d.ts +2 -1
- package/dist/utils/viewMasking.js +9 -1
- package/package.json +30 -12
- package/dist/history/types.d.ts +0 -73
- package/dist/history/types.js +0 -5
- package/dist/tsconfig.tsbuildinfo +0 -1
package/dist/actions/dealing.js
CHANGED
|
@@ -15,23 +15,93 @@ const positioning_1 = require("../utils/positioning");
|
|
|
15
15
|
*/
|
|
16
16
|
function handleDeal(state, action) {
|
|
17
17
|
// Move button (Dead Button logic: moves to next seat index regardless of occupancy)
|
|
18
|
-
|
|
18
|
+
let newButtonSeat = moveButton(state);
|
|
19
19
|
// Determine if this is a tournament
|
|
20
20
|
const isTournament = !!state.config.blindStructure;
|
|
21
|
-
|
|
21
|
+
const isClient = !!state.config.isClient;
|
|
22
|
+
// Create and shuffle deck (server only)
|
|
23
|
+
// In client mode, we use an empty deck and deal masked cards
|
|
22
24
|
const rng = state.config.randomProvider ?? Math.random;
|
|
23
|
-
const deck = (0, deck_1.shuffle)((0, deck_1.createDeck)(), rng);
|
|
25
|
+
const deck = isClient ? [] : (0, deck_1.shuffle)((0, deck_1.createDeck)(), rng);
|
|
26
|
+
// Create a copy of timeBanks to modify
|
|
27
|
+
const newTimeBanks = new Map(state.timeBanks);
|
|
28
|
+
// First, merge pendingAddOn into stack for all players
|
|
29
|
+
const newPlayers = state.players.map((player) => {
|
|
30
|
+
if (!player)
|
|
31
|
+
return null;
|
|
32
|
+
// Skip reserved players (they haven't confirmed yet)
|
|
33
|
+
if (player.status === "RESERVED" /* PlayerStatus.RESERVED */) {
|
|
34
|
+
// Check if reservation has expired
|
|
35
|
+
if (player.reservationExpiry && action.timestamp >= player.reservationExpiry) {
|
|
36
|
+
// Reservation expired, remove player
|
|
37
|
+
newTimeBanks.delete(player.seat);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
// Keep reserved player as-is
|
|
41
|
+
return player;
|
|
42
|
+
}
|
|
43
|
+
// Merge pendingAddOn into stack
|
|
44
|
+
const newStack = player.stack + player.pendingAddOn;
|
|
45
|
+
return {
|
|
46
|
+
...player,
|
|
47
|
+
stack: newStack,
|
|
48
|
+
pendingAddOn: 0, // Clear pending add-on
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
newButtonSeat = moveHeadsUpButtonToOccupiedSeat(newButtonSeat, {
|
|
52
|
+
...state,
|
|
53
|
+
players: newPlayers,
|
|
54
|
+
});
|
|
55
|
+
// Get blind positions for this hand
|
|
56
|
+
const blindPositions = (0, blinds_1.getBlindPositions)({
|
|
57
|
+
...state,
|
|
58
|
+
buttonSeat: newButtonSeat,
|
|
59
|
+
players: newPlayers,
|
|
60
|
+
});
|
|
24
61
|
// Get players who will be dealt in
|
|
25
62
|
const playersToReceive = [];
|
|
26
|
-
for (let seat = 0; seat <
|
|
27
|
-
const player =
|
|
28
|
-
|
|
63
|
+
for (let seat = 0; seat < newPlayers.length; seat++) {
|
|
64
|
+
const player = newPlayers[seat];
|
|
65
|
+
// Basic eligibility checks
|
|
66
|
+
if (!player || player.stack <= 0 || player.status === "RESERVED" /* PlayerStatus.RESERVED */) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
// We check WAIT_FOR_BB *before* checking isSittingOut.
|
|
70
|
+
// This allows us to "unsit" a player if they hit the Big Blind.
|
|
71
|
+
let shouldPlay = true;
|
|
72
|
+
if (!isTournament && player.sitInOption === "WAIT_FOR_BB" /* SitInOption.WAIT_FOR_BB */) {
|
|
73
|
+
const isInBigBlind = blindPositions?.bigBlindSeat === seat;
|
|
74
|
+
if (isInBigBlind) {
|
|
75
|
+
// PLAYER RE-ENTRY: They are in the Big Blind. Force them active.
|
|
76
|
+
// We must update the player object in newPlayers to reflect they are back.
|
|
77
|
+
newPlayers[seat] = {
|
|
78
|
+
...player,
|
|
79
|
+
isSittingOut: false,
|
|
80
|
+
};
|
|
81
|
+
shouldPlay = true;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
// Not in BB yet. Force them to sit out.
|
|
85
|
+
// Only update if not already sitting out to avoid object churn
|
|
86
|
+
if (!player.isSittingOut) {
|
|
87
|
+
newPlayers[seat] = {
|
|
88
|
+
...player,
|
|
89
|
+
isSittingOut: true,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
shouldPlay = false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else if (player.isSittingOut) {
|
|
96
|
+
// Standard sitting out check
|
|
97
|
+
shouldPlay = false;
|
|
98
|
+
}
|
|
99
|
+
if (shouldPlay) {
|
|
29
100
|
playersToReceive.push(seat);
|
|
30
101
|
}
|
|
31
102
|
}
|
|
32
103
|
// Deal 2 cards to each player
|
|
33
104
|
let remainingDeck = deck;
|
|
34
|
-
const newPlayers = [...state.players];
|
|
35
105
|
// Initialize hands for receiving players (active, not sitting out)
|
|
36
106
|
for (const seat of playersToReceive) {
|
|
37
107
|
newPlayers[seat] = {
|
|
@@ -62,10 +132,17 @@ function handleDeal(state, action) {
|
|
|
62
132
|
// Deal 2 cards, one by one, in circle (standard poker procedure)
|
|
63
133
|
for (let round = 0; round < 2; round++) {
|
|
64
134
|
for (const seat of playersToReceive) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
135
|
+
let cardStrings;
|
|
136
|
+
if (isClient) {
|
|
137
|
+
// Client mode: Deal masked cards
|
|
138
|
+
cardStrings = [null];
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
// Server mode: Deal from deck
|
|
142
|
+
const [cards, nextDeck] = (0, deck_1.dealCards)(remainingDeck, 1);
|
|
143
|
+
remainingDeck = nextDeck;
|
|
144
|
+
cardStrings = (0, cardUtils_1.cardCodesToStrings)(cards);
|
|
145
|
+
}
|
|
69
146
|
// Append to existing hand
|
|
70
147
|
const currentPlayer = newPlayers[seat];
|
|
71
148
|
const currentHand = currentPlayer.hand ?? []; // Should be [] from initialization
|
|
@@ -76,14 +153,15 @@ function handleDeal(state, action) {
|
|
|
76
153
|
}
|
|
77
154
|
}
|
|
78
155
|
// Post blinds and antes
|
|
79
|
-
|
|
156
|
+
// Recalculate blind positions with the new button seat
|
|
157
|
+
const finalBlindPositions = (0, blinds_1.getBlindPositions)({
|
|
80
158
|
...state,
|
|
81
159
|
buttonSeat: newButtonSeat,
|
|
82
160
|
players: newPlayers,
|
|
83
161
|
});
|
|
84
162
|
const currentBets = new Map();
|
|
85
|
-
if (
|
|
86
|
-
const { smallBlindSeat, bigBlindSeat } =
|
|
163
|
+
if (finalBlindPositions) {
|
|
164
|
+
const { smallBlindSeat, bigBlindSeat } = finalBlindPositions;
|
|
87
165
|
// Post small blind
|
|
88
166
|
// In tournaments: sitting-out players MUST post to prevent "blinding off" exploit
|
|
89
167
|
// In cash games: sitting-out SB is treated as "Dead Small Blind" (no post)
|
|
@@ -110,19 +188,24 @@ function handleDeal(state, action) {
|
|
|
110
188
|
// Post big blind (Must exist for hand to start)
|
|
111
189
|
const bbPlayer = newPlayers[bigBlindSeat];
|
|
112
190
|
if (bbPlayer) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
191
|
+
// In Cash Games: sitting-out players should NEVER post blinds (they are skipped by getBlindPositions)
|
|
192
|
+
// In Tournaments: sitting-out players MUST post blinds to prevent "blinding off" exploit
|
|
193
|
+
const shouldPostBB = isTournament || !bbPlayer.isSittingOut;
|
|
194
|
+
if (shouldPostBB) {
|
|
195
|
+
const bbAmount = Math.min(bbPlayer.stack, state.bigBlind);
|
|
196
|
+
currentBets.set(bigBlindSeat, bbAmount);
|
|
197
|
+
newPlayers[bigBlindSeat] = {
|
|
198
|
+
...bbPlayer,
|
|
199
|
+
stack: bbPlayer.stack - bbAmount,
|
|
200
|
+
betThisStreet: bbAmount,
|
|
201
|
+
totalInvestedThisHand: bbAmount,
|
|
202
|
+
status: bbAmount === bbPlayer.stack
|
|
203
|
+
? "ALL_IN" /* PlayerStatus.ALL_IN */
|
|
204
|
+
: bbPlayer.isSittingOut
|
|
205
|
+
? "FOLDED" /* PlayerStatus.FOLDED */
|
|
206
|
+
: "ACTIVE" /* PlayerStatus.ACTIVE */,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
126
209
|
}
|
|
127
210
|
}
|
|
128
211
|
// Post antes if configured
|
|
@@ -204,3 +287,11 @@ function moveButton(state) {
|
|
|
204
287
|
// We do not skip empty seats here.
|
|
205
288
|
return (0, positioning_1.getNextSeat)(state.buttonSeat, state.maxPlayers);
|
|
206
289
|
}
|
|
290
|
+
function moveHeadsUpButtonToOccupiedSeat(buttonSeat, state) {
|
|
291
|
+
const occupiedSeats = state.players.filter((player) => player !== null && player.stack > 0);
|
|
292
|
+
const buttonPlayer = state.players[buttonSeat];
|
|
293
|
+
if (occupiedSeats.length !== 2 || (buttonPlayer !== null && buttonPlayer.stack > 0)) {
|
|
294
|
+
return buttonSeat;
|
|
295
|
+
}
|
|
296
|
+
return (0, positioning_1.getNextOccupiedSeat)(buttonSeat, state.players, state.maxPlayers) ?? buttonSeat;
|
|
297
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GameState, SitAction, StandAction } from "@pokertools/types";
|
|
1
|
+
import { GameState, SitAction, StandAction, AddChipsAction, ReserveSeatAction } from "@pokertools/types";
|
|
2
2
|
/**
|
|
3
3
|
* Handle SIT action - add player to table
|
|
4
4
|
*/
|
|
@@ -7,3 +7,14 @@ export declare function handleSit(state: GameState, action: SitAction): GameStat
|
|
|
7
7
|
* Handle STAND action - remove player from table
|
|
8
8
|
*/
|
|
9
9
|
export declare function handleStand(state: GameState, action: StandAction): GameState;
|
|
10
|
+
/**
|
|
11
|
+
* Handle ADD_CHIPS action - add chips to player's pending stack
|
|
12
|
+
* Chips are held in pendingAddOn and will be merged into stack at start of next hand
|
|
13
|
+
*/
|
|
14
|
+
export declare function handleAddChips(state: GameState, action: AddChipsAction): GameState;
|
|
15
|
+
/**
|
|
16
|
+
* Handle RESERVE_SEAT action - reserve a seat for a player
|
|
17
|
+
* Marks the seat as RESERVED with an expiration timestamp
|
|
18
|
+
* API can use this to lock a seat while processing payment
|
|
19
|
+
*/
|
|
20
|
+
export declare function handleReserveSeat(state: GameState, action: ReserveSeatAction): GameState;
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.handleSit = handleSit;
|
|
4
4
|
exports.handleStand = handleStand;
|
|
5
|
+
exports.handleAddChips = handleAddChips;
|
|
6
|
+
exports.handleReserveSeat = handleReserveSeat;
|
|
5
7
|
const positioning_1 = require("../utils/positioning");
|
|
8
|
+
const betting_1 = require("./betting");
|
|
6
9
|
/**
|
|
7
10
|
* Handle SIT action - add player to table
|
|
8
11
|
*/
|
|
@@ -19,6 +22,9 @@ function handleSit(state, action) {
|
|
|
19
22
|
totalInvestedThisHand: 0,
|
|
20
23
|
isSittingOut: false,
|
|
21
24
|
timeBank: state.config.timeBankSeconds ?? 30,
|
|
25
|
+
pendingAddOn: 0,
|
|
26
|
+
sitInOption: action.sitInOption ?? "IMMEDIATE" /* SitInOption.IMMEDIATE */,
|
|
27
|
+
reservationExpiry: null,
|
|
22
28
|
};
|
|
23
29
|
const newPlayers = [...state.players];
|
|
24
30
|
newPlayers[action.seat] = newPlayer;
|
|
@@ -40,19 +46,94 @@ function handleStand(state, action) {
|
|
|
40
46
|
if (!result) {
|
|
41
47
|
return state;
|
|
42
48
|
}
|
|
43
|
-
|
|
44
|
-
const
|
|
49
|
+
let currentState = state;
|
|
50
|
+
const { player, seat } = result;
|
|
51
|
+
// 1. BEST PRACTICE: If the player is in a live hand, they must FOLD first.
|
|
52
|
+
// This resolves the "ActionTo" pointer, potential winners, and pot eligibility
|
|
53
|
+
// using the standard game rules defined in handleFold.
|
|
54
|
+
const isLiveHand = currentState.handNumber > 0 &&
|
|
55
|
+
currentState.street !== "SHOWDOWN" && // Not needed if hand is over
|
|
56
|
+
(player.status === "ACTIVE" || player.status === "ALL_IN");
|
|
57
|
+
if (isLiveHand) {
|
|
58
|
+
// Execute a "Virtual Fold" to gracefully exit the hand
|
|
59
|
+
currentState = (0, betting_1.handleFold)(currentState, {
|
|
60
|
+
type: "FOLD" /* ActionType.FOLD */,
|
|
61
|
+
playerId: player.id,
|
|
62
|
+
timestamp: action.timestamp,
|
|
63
|
+
});
|
|
64
|
+
// NOTE: handleFold returns a new state where actionTo has already been
|
|
65
|
+
// advanced to the next player. The invariant check will now pass.
|
|
66
|
+
}
|
|
67
|
+
// 2. Remove player from table (Standard Stand Logic)
|
|
68
|
+
const newPlayers = [...currentState.players];
|
|
45
69
|
newPlayers[seat] = null;
|
|
46
70
|
// Remove from time banks
|
|
47
|
-
const newTimeBanks = new Map(
|
|
71
|
+
const newTimeBanks = new Map(currentState.timeBanks);
|
|
48
72
|
newTimeBanks.delete(seat);
|
|
49
73
|
// Remove from active players if present
|
|
50
|
-
|
|
74
|
+
// (Note: handleFold might have already moved them to FOLDED status,
|
|
75
|
+
// but we ensure they are fully removed from tracking here)
|
|
76
|
+
const newActivePlayers = currentState.activePlayers.filter((s) => s !== seat);
|
|
51
77
|
return {
|
|
52
|
-
...
|
|
78
|
+
...currentState,
|
|
53
79
|
players: newPlayers,
|
|
54
80
|
activePlayers: newActivePlayers,
|
|
55
81
|
timeBanks: newTimeBanks,
|
|
56
82
|
timestamp: action.timestamp,
|
|
57
83
|
};
|
|
58
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Handle ADD_CHIPS action - add chips to player's pending stack
|
|
87
|
+
* Chips are held in pendingAddOn and will be merged into stack at start of next hand
|
|
88
|
+
*/
|
|
89
|
+
function handleAddChips(state, action) {
|
|
90
|
+
const result = (0, positioning_1.getPlayerById)(state, action.playerId);
|
|
91
|
+
if (!result) {
|
|
92
|
+
return state;
|
|
93
|
+
}
|
|
94
|
+
const { player, seat } = result;
|
|
95
|
+
const newPlayers = [...state.players];
|
|
96
|
+
newPlayers[seat] = {
|
|
97
|
+
...player,
|
|
98
|
+
pendingAddOn: player.pendingAddOn + action.amount,
|
|
99
|
+
};
|
|
100
|
+
return {
|
|
101
|
+
...state,
|
|
102
|
+
players: newPlayers,
|
|
103
|
+
timestamp: action.timestamp,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Handle RESERVE_SEAT action - reserve a seat for a player
|
|
108
|
+
* Marks the seat as RESERVED with an expiration timestamp
|
|
109
|
+
* API can use this to lock a seat while processing payment
|
|
110
|
+
*/
|
|
111
|
+
function handleReserveSeat(state, action) {
|
|
112
|
+
// Check if seat is already occupied
|
|
113
|
+
if (state.players[action.seat] !== null) {
|
|
114
|
+
return state;
|
|
115
|
+
}
|
|
116
|
+
const reservedPlayer = {
|
|
117
|
+
id: action.playerId,
|
|
118
|
+
name: action.playerName,
|
|
119
|
+
seat: action.seat,
|
|
120
|
+
stack: 0,
|
|
121
|
+
hand: null,
|
|
122
|
+
shownCards: null,
|
|
123
|
+
status: "RESERVED" /* PlayerStatus.RESERVED */,
|
|
124
|
+
betThisStreet: 0,
|
|
125
|
+
totalInvestedThisHand: 0,
|
|
126
|
+
isSittingOut: false,
|
|
127
|
+
timeBank: state.config.timeBankSeconds ?? 30,
|
|
128
|
+
pendingAddOn: 0,
|
|
129
|
+
sitInOption: "IMMEDIATE" /* SitInOption.IMMEDIATE */,
|
|
130
|
+
reservationExpiry: action.expiryTimestamp,
|
|
131
|
+
};
|
|
132
|
+
const newPlayers = [...state.players];
|
|
133
|
+
newPlayers[action.seat] = reservedPlayer;
|
|
134
|
+
return {
|
|
135
|
+
...state,
|
|
136
|
+
players: newPlayers,
|
|
137
|
+
timestamp: action.timestamp,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
@@ -10,5 +10,23 @@ export declare function handleTimeout(state: GameState, action: TimeoutAction):
|
|
|
10
10
|
* Handle TIME_BANK action
|
|
11
11
|
* - Deducts time from player's time bank
|
|
12
12
|
* - Keeps action on same player
|
|
13
|
+
*
|
|
14
|
+
* TIME BANK DEDUCTION POLICY:
|
|
15
|
+
* This implementation uses a "pay-per-activation" model where:
|
|
16
|
+
* - Each time bank activation deducts a fixed amount (default: 10 seconds)
|
|
17
|
+
* - Player receives the full deduction amount as additional time
|
|
18
|
+
* - If remaining time bank is less than the deduction, it is fully consumed
|
|
19
|
+
* - This prevents players from getting "free" time when they have < 10s remaining
|
|
20
|
+
*
|
|
21
|
+
* Example scenarios:
|
|
22
|
+
* - Player has 30s, activates time bank → 20s remaining, gets 10s additional time
|
|
23
|
+
* - Player has 5s, activates time bank → 0s remaining, gets 10s additional time
|
|
24
|
+
* - Player has 0s, cannot activate time bank → forced timeout/fold
|
|
25
|
+
*
|
|
26
|
+
* Alternative design consideration:
|
|
27
|
+
* If you want "time-as-resource" (only deduct what you use), you would need:
|
|
28
|
+
* - Track time used per activation in the UI layer
|
|
29
|
+
* - Deduct actual time consumed rather than fixed amount
|
|
30
|
+
* - Return unused time if action is made before deduction expires
|
|
13
31
|
*/
|
|
14
32
|
export declare function handleTimeBank(state: GameState, action: TimeBankAction): GameState;
|
package/dist/actions/special.js
CHANGED
|
@@ -56,6 +56,24 @@ function handleTimeout(state, action) {
|
|
|
56
56
|
* Handle TIME_BANK action
|
|
57
57
|
* - Deducts time from player's time bank
|
|
58
58
|
* - Keeps action on same player
|
|
59
|
+
*
|
|
60
|
+
* TIME BANK DEDUCTION POLICY:
|
|
61
|
+
* This implementation uses a "pay-per-activation" model where:
|
|
62
|
+
* - Each time bank activation deducts a fixed amount (default: 10 seconds)
|
|
63
|
+
* - Player receives the full deduction amount as additional time
|
|
64
|
+
* - If remaining time bank is less than the deduction, it is fully consumed
|
|
65
|
+
* - This prevents players from getting "free" time when they have < 10s remaining
|
|
66
|
+
*
|
|
67
|
+
* Example scenarios:
|
|
68
|
+
* - Player has 30s, activates time bank → 20s remaining, gets 10s additional time
|
|
69
|
+
* - Player has 5s, activates time bank → 0s remaining, gets 10s additional time
|
|
70
|
+
* - Player has 0s, cannot activate time bank → forced timeout/fold
|
|
71
|
+
*
|
|
72
|
+
* Alternative design consideration:
|
|
73
|
+
* If you want "time-as-resource" (only deduct what you use), you would need:
|
|
74
|
+
* - Track time used per activation in the UI layer
|
|
75
|
+
* - Deduct actual time consumed rather than fixed amount
|
|
76
|
+
* - Return unused time if action is made before deduction expires
|
|
59
77
|
*/
|
|
60
78
|
function handleTimeBank(state, action) {
|
|
61
79
|
const result = (0, positioning_1.getPlayerById)(state, action.playerId);
|
|
@@ -73,6 +91,7 @@ function handleTimeBank(state, action) {
|
|
|
73
91
|
});
|
|
74
92
|
}
|
|
75
93
|
// Deduct time from player's time bank (configurable, default 10 seconds)
|
|
94
|
+
// Uses pay-per-activation model: deduct full amount even if less is available
|
|
76
95
|
const deduction = state.config.timeBankDeductionSeconds ?? 10;
|
|
77
96
|
const newTimeBank = Math.max(0, currentTimeBank - deduction);
|
|
78
97
|
const newTimeBanks = new Map(state.timeBanks);
|
|
@@ -80,6 +99,7 @@ function handleTimeBank(state, action) {
|
|
|
80
99
|
return {
|
|
81
100
|
...state,
|
|
82
101
|
timeBanks: newTimeBanks,
|
|
102
|
+
timeBankActiveSeat: seat, // Mark time bank as active for this player
|
|
83
103
|
timestamp: action.timestamp,
|
|
84
104
|
// Keep actionTo the same (extends player's turn)
|
|
85
105
|
};
|
|
@@ -31,6 +31,12 @@ function validateAction(state, action) {
|
|
|
31
31
|
case "TIME_BANK" /* ActionType.TIME_BANK */:
|
|
32
32
|
validateTimeAction(state, action);
|
|
33
33
|
break;
|
|
34
|
+
case "ADD_CHIPS" /* ActionType.ADD_CHIPS */:
|
|
35
|
+
validateAddChipsAction(state, action);
|
|
36
|
+
break;
|
|
37
|
+
case "RESERVE_SEAT" /* ActionType.RESERVE_SEAT */:
|
|
38
|
+
validateReserveSeatAction(state, action);
|
|
39
|
+
break;
|
|
34
40
|
default:
|
|
35
41
|
// Other actions don't need validation
|
|
36
42
|
break;
|
|
@@ -145,8 +151,13 @@ function validateSitAction(state, action) {
|
|
|
145
151
|
if (action.seat < 0 || action.seat >= state.maxPlayers) {
|
|
146
152
|
throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.INVALID_SEAT, `Seat ${action.seat} is invalid (max: ${state.maxPlayers - 1})`, { seat: action.seat, maxPlayers: state.maxPlayers });
|
|
147
153
|
}
|
|
148
|
-
|
|
149
|
-
|
|
154
|
+
const existingPlayer = state.players[action.seat];
|
|
155
|
+
if (existingPlayer !== null) {
|
|
156
|
+
// Allow claiming the seat if it is RESERVED by THIS player
|
|
157
|
+
const isMyReservation = existingPlayer.status === "RESERVED" /* PlayerStatus.RESERVED */ && existingPlayer.id === action.playerId;
|
|
158
|
+
if (!isMyReservation) {
|
|
159
|
+
throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.SEAT_OCCUPIED, `Seat ${action.seat} is already occupied`, { seat: action.seat });
|
|
160
|
+
}
|
|
150
161
|
}
|
|
151
162
|
if (action.stack <= 0) {
|
|
152
163
|
throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.INVALID_STACK, `Stack must be positive, got ${action.stack}`, { stack: action.stack });
|
|
@@ -168,6 +179,20 @@ function validateTimeAction(state, action) {
|
|
|
168
179
|
throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.NOT_YOUR_TURN, `Player ${action.playerId} cannot use time action when it's not their turn`, { playerId: action.playerId, actionTo: state.actionTo });
|
|
169
180
|
}
|
|
170
181
|
}
|
|
182
|
+
function validateAddChipsAction(state, action) {
|
|
183
|
+
const result = (0, positioning_1.getPlayerById)(state, action.playerId);
|
|
184
|
+
if (!result) {
|
|
185
|
+
throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.PLAYER_NOT_FOUND, `Player ${action.playerId} not found`, { playerId: action.playerId });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function validateReserveSeatAction(state, action) {
|
|
189
|
+
if (action.seat < 0 || action.seat >= state.maxPlayers) {
|
|
190
|
+
throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.INVALID_SEAT, `Seat ${action.seat} is invalid (max: ${state.maxPlayers - 1})`, { seat: action.seat, maxPlayers: state.maxPlayers });
|
|
191
|
+
}
|
|
192
|
+
if (state.players[action.seat] !== null) {
|
|
193
|
+
throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.SEAT_OCCUPIED, `Seat ${action.seat} is already occupied`, { seat: action.seat });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
171
196
|
/**
|
|
172
197
|
* Get current highest bet this street
|
|
173
198
|
*/
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-compatible entry point for PokerEngine
|
|
3
|
+
*
|
|
4
|
+
* This file provides a browser-safe RNG using Web Crypto API
|
|
5
|
+
* and re-exports all engine functionality for use in web applications.
|
|
6
|
+
*/
|
|
7
|
+
import { PokerEngine } from "./engine/PokerEngine";
|
|
8
|
+
import type { TableConfig } from "@pokertools/types";
|
|
9
|
+
/**
|
|
10
|
+
* Browser-compatible RNG using Web Crypto API
|
|
11
|
+
* Falls back to Math.random() only in environments without crypto
|
|
12
|
+
*/
|
|
13
|
+
export declare function getBrowserRNG(): () => number;
|
|
14
|
+
/**
|
|
15
|
+
* Create a PokerEngine instance with browser-compatible RNG
|
|
16
|
+
*/
|
|
17
|
+
export declare function createBrowserEngine(config: TableConfig): PokerEngine;
|
|
18
|
+
export * from "./engine/PokerEngine";
|
|
19
|
+
export * from "./actions/betting";
|
|
20
|
+
export * from "./actions/dealing";
|
|
21
|
+
export * from "./actions/management";
|
|
22
|
+
export * from "./actions/showdownActions";
|
|
23
|
+
export * from "./actions/special";
|
|
24
|
+
export * from "./utils/viewMasking";
|
|
25
|
+
export * from "./utils/serialization";
|
|
26
|
+
export * from "./utils/cardUtils";
|
|
27
|
+
export * from "./history/exporter";
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Browser-compatible entry point for PokerEngine
|
|
4
|
+
*
|
|
5
|
+
* This file provides a browser-safe RNG using Web Crypto API
|
|
6
|
+
* and re-exports all engine functionality for use in web applications.
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
20
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
21
|
+
};
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.getBrowserRNG = getBrowserRNG;
|
|
24
|
+
exports.createBrowserEngine = createBrowserEngine;
|
|
25
|
+
const PokerEngine_1 = require("./engine/PokerEngine");
|
|
26
|
+
/**
|
|
27
|
+
* Browser-compatible RNG using Web Crypto API
|
|
28
|
+
* Falls back to Math.random() only in environments without crypto
|
|
29
|
+
*/
|
|
30
|
+
function getBrowserRNG() {
|
|
31
|
+
// Check for Web Crypto API
|
|
32
|
+
if (typeof window !== "undefined" && window.crypto?.getRandomValues) {
|
|
33
|
+
return () => {
|
|
34
|
+
const buffer = new Uint32Array(1);
|
|
35
|
+
window.crypto.getRandomValues(buffer);
|
|
36
|
+
return buffer[0] / 0x100000000;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// Check for Node.js crypto (for SSR/testing)
|
|
40
|
+
if (typeof globalThis !== "undefined" && globalThis.crypto?.getRandomValues) {
|
|
41
|
+
return () => {
|
|
42
|
+
const buffer = new Uint32Array(1);
|
|
43
|
+
globalThis.crypto.getRandomValues(buffer);
|
|
44
|
+
return buffer[0] / 0x100000000;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// Fallback (warn in development)
|
|
48
|
+
if (process.env.NODE_ENV !== "production") {
|
|
49
|
+
console.warn("[PokerEngine Browser] Web Crypto API not available, using Math.random(). " +
|
|
50
|
+
"This is NOT cryptographically secure.");
|
|
51
|
+
}
|
|
52
|
+
return Math.random;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Create a PokerEngine instance with browser-compatible RNG
|
|
56
|
+
*/
|
|
57
|
+
function createBrowserEngine(config) {
|
|
58
|
+
return new PokerEngine_1.PokerEngine({
|
|
59
|
+
...config,
|
|
60
|
+
randomProvider: getBrowserRNG(),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
// Re-export everything from main engine
|
|
64
|
+
__exportStar(require("./engine/PokerEngine"), exports);
|
|
65
|
+
__exportStar(require("./actions/betting"), exports);
|
|
66
|
+
__exportStar(require("./actions/dealing"), exports);
|
|
67
|
+
__exportStar(require("./actions/management"), exports);
|
|
68
|
+
__exportStar(require("./actions/showdownActions"), exports);
|
|
69
|
+
__exportStar(require("./actions/special"), exports);
|
|
70
|
+
__exportStar(require("./utils/viewMasking"), exports);
|
|
71
|
+
__exportStar(require("./utils/serialization"), exports);
|
|
72
|
+
__exportStar(require("./utils/cardUtils"), exports);
|
|
73
|
+
__exportStar(require("./history/exporter"), exports);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { GameState, TableConfig, Action, PublicState } from "@pokertools/types";
|
|
2
2
|
import { Snapshot } from "../utils/serialization";
|
|
3
|
-
import { HandHistory, ExportOptions } from "
|
|
3
|
+
import { HandHistory, ExportOptions } from "@pokertools/types";
|
|
4
4
|
/**
|
|
5
5
|
* Event listener callback type
|
|
6
6
|
*/
|
|
@@ -35,6 +35,27 @@ export declare class PokerEngine {
|
|
|
35
35
|
* If action.timestamp is not provided, the engine will automatically set it
|
|
36
36
|
*/
|
|
37
37
|
act(action: Action): GameState;
|
|
38
|
+
/**
|
|
39
|
+
* Validate an action without executing it
|
|
40
|
+
* Useful for UI state (enabling/disabling buttons)
|
|
41
|
+
*/
|
|
42
|
+
validate(action: Action): {
|
|
43
|
+
valid: true;
|
|
44
|
+
} | {
|
|
45
|
+
valid: false;
|
|
46
|
+
error: string;
|
|
47
|
+
code?: string;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Reconcile local state with server state
|
|
51
|
+
* Smoothly merges server updates into client engine
|
|
52
|
+
*/
|
|
53
|
+
reconcile(serverState: PublicState | GameState): void;
|
|
54
|
+
/**
|
|
55
|
+
* Optimistically execute an action and return the provisional state
|
|
56
|
+
* Does not modify the engine's actual state
|
|
57
|
+
*/
|
|
58
|
+
optimisticAct(action: Action): GameState;
|
|
38
59
|
/**
|
|
39
60
|
* Undo last action
|
|
40
61
|
*/
|
|
@@ -46,7 +67,7 @@ export declare class PokerEngine {
|
|
|
46
67
|
/**
|
|
47
68
|
* Get player view (masked)
|
|
48
69
|
*/
|
|
49
|
-
view(playerId?: string): PublicState;
|
|
70
|
+
view(playerId?: string, version?: number): PublicState;
|
|
50
71
|
/**
|
|
51
72
|
* Get snapshot for serialization
|
|
52
73
|
*/
|
|
@@ -80,6 +80,57 @@ class PokerEngine {
|
|
|
80
80
|
this.dispatch(actionWithTimestamp);
|
|
81
81
|
return this.currentState;
|
|
82
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Validate an action without executing it
|
|
85
|
+
* Useful for UI state (enabling/disabling buttons)
|
|
86
|
+
*/
|
|
87
|
+
validate(action) {
|
|
88
|
+
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.
|
|
92
|
+
(0, gameReducer_1.gameReducer)(this.currentState, action);
|
|
93
|
+
return { valid: true };
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
const message = err?.message ?? "Invalid action";
|
|
97
|
+
const code = err?.code;
|
|
98
|
+
return {
|
|
99
|
+
valid: false,
|
|
100
|
+
error: message,
|
|
101
|
+
code,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Reconcile local state with server state
|
|
107
|
+
* Smoothly merges server updates into client engine
|
|
108
|
+
*/
|
|
109
|
+
reconcile(serverState) {
|
|
110
|
+
// Hydrate PublicState into GameState if needed
|
|
111
|
+
const newState = {
|
|
112
|
+
...serverState,
|
|
113
|
+
// Ensure deck exists (empty for client/public state)
|
|
114
|
+
deck: "deck" in serverState ? serverState.deck : [],
|
|
115
|
+
// Ensure players map correctly (PublicPlayer.hand is compatible with Player.hand)
|
|
116
|
+
players: serverState.players, // Type assertion needed due to deep readonly/mutable mismatch potential
|
|
117
|
+
// Ensure config carries isClient flag if set locally
|
|
118
|
+
config: {
|
|
119
|
+
...serverState.config,
|
|
120
|
+
isClient: this.currentState.config.isClient,
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
this.currentState = newState;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Optimistically execute an action and return the provisional state
|
|
127
|
+
* Does not modify the engine's actual state
|
|
128
|
+
*/
|
|
129
|
+
optimisticAct(action) {
|
|
130
|
+
const timestamp = action.timestamp ?? this.timeProvider();
|
|
131
|
+
const actionWithTimestamp = { ...action, timestamp };
|
|
132
|
+
return (0, gameReducer_1.gameReducer)(this.currentState, actionWithTimestamp);
|
|
133
|
+
}
|
|
83
134
|
/**
|
|
84
135
|
* Undo last action
|
|
85
136
|
*/
|
|
@@ -100,8 +151,8 @@ class PokerEngine {
|
|
|
100
151
|
/**
|
|
101
152
|
* Get player view (masked)
|
|
102
153
|
*/
|
|
103
|
-
view(playerId) {
|
|
104
|
-
return (0, viewMasking_1.createPublicView)(this.currentState, playerId ?? null);
|
|
154
|
+
view(playerId, version) {
|
|
155
|
+
return (0, viewMasking_1.createPublicView)(this.currentState, playerId ?? null, version ?? 0);
|
|
105
156
|
}
|
|
106
157
|
/**
|
|
107
158
|
* Get snapshot for serialization
|
|
@@ -236,6 +287,7 @@ class PokerEngine {
|
|
|
236
287
|
ante: initialBlinds?.ante ?? config.ante ?? 0,
|
|
237
288
|
blindLevel: 0,
|
|
238
289
|
timeBanks: new Map(),
|
|
290
|
+
timeBankActiveSeat: null,
|
|
239
291
|
actionHistory: [],
|
|
240
292
|
previousStates: [],
|
|
241
293
|
timestamp: Date.now(),
|