@pokertools/engine 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +607 -0
- package/dist/actions/betting.d.ts +21 -0
- package/dist/actions/betting.js +410 -0
- package/dist/actions/dealing.d.ts +9 -0
- package/dist/actions/dealing.js +206 -0
- package/dist/actions/management.d.ts +9 -0
- package/dist/actions/management.js +58 -0
- package/dist/actions/showdownActions.d.ts +9 -0
- package/dist/actions/showdownActions.js +119 -0
- package/dist/actions/special.d.ts +14 -0
- package/dist/actions/special.js +98 -0
- package/dist/actions/streetProgression.d.ts +13 -0
- package/dist/actions/streetProgression.js +157 -0
- package/dist/actions/tournament.d.ts +5 -0
- package/dist/actions/tournament.js +38 -0
- package/dist/actions/validation.d.ts +6 -0
- package/dist/actions/validation.js +182 -0
- package/dist/engine/PokerEngine.d.ts +92 -0
- package/dist/engine/PokerEngine.js +246 -0
- package/dist/engine/gameReducer.d.ts +10 -0
- package/dist/engine/gameReducer.js +135 -0
- package/dist/errors/ConfigError.d.ts +8 -0
- package/dist/errors/ConfigError.js +15 -0
- package/dist/errors/CriticalStateError.d.ts +8 -0
- package/dist/errors/CriticalStateError.js +15 -0
- package/dist/errors/ErrorCodes.d.ts +38 -0
- package/dist/errors/ErrorCodes.js +46 -0
- package/dist/errors/IllegalActionError.d.ts +9 -0
- package/dist/errors/IllegalActionError.js +15 -0
- package/dist/errors/PokerEngineError.d.ts +8 -0
- package/dist/errors/PokerEngineError.js +19 -0
- package/dist/errors/index.d.ts +5 -0
- package/dist/errors/index.js +22 -0
- package/dist/history/exporter.d.ts +28 -0
- package/dist/history/exporter.js +60 -0
- package/dist/history/formats/json.d.ts +14 -0
- package/dist/history/formats/json.js +46 -0
- package/dist/history/formats/pokerstars.d.ts +10 -0
- package/dist/history/formats/pokerstars.js +188 -0
- package/dist/history/handHistoryBuilder.d.ts +10 -0
- package/dist/history/handHistoryBuilder.js +179 -0
- package/dist/history/types.d.ts +73 -0
- package/dist/history/types.js +5 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +38 -0
- package/dist/rules/actionOrder.d.ts +14 -0
- package/dist/rules/actionOrder.js +211 -0
- package/dist/rules/blinds.d.ts +24 -0
- package/dist/rules/blinds.js +64 -0
- package/dist/rules/headsUp.d.ts +15 -0
- package/dist/rules/headsUp.js +44 -0
- package/dist/rules/showdown.d.ts +9 -0
- package/dist/rules/showdown.js +164 -0
- package/dist/rules/sidePots.d.ts +32 -0
- package/dist/rules/sidePots.js +173 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/utils/cardUtils.d.ts +12 -0
- package/dist/utils/cardUtils.js +30 -0
- package/dist/utils/constants.d.ts +38 -0
- package/dist/utils/constants.js +41 -0
- package/dist/utils/deck.d.ts +46 -0
- package/dist/utils/deck.js +126 -0
- package/dist/utils/invariants.d.ts +39 -0
- package/dist/utils/invariants.js +163 -0
- package/dist/utils/positioning.d.ts +36 -0
- package/dist/utils/positioning.js +97 -0
- package/dist/utils/rake.d.ts +13 -0
- package/dist/utils/rake.js +45 -0
- package/dist/utils/serialization.d.ts +53 -0
- package/dist/utils/serialization.js +106 -0
- package/dist/utils/validation.d.ts +20 -0
- package/dist/utils/validation.js +52 -0
- package/dist/utils/viewMasking.d.ts +20 -0
- package/dist/utils/viewMasking.js +90 -0
- package/package.json +58 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { GameState, TableConfig, Action, PublicState } from "@pokertools/types";
|
|
2
|
+
import { Snapshot } from "../utils/serialization";
|
|
3
|
+
import { HandHistory, ExportOptions } from "../history/types";
|
|
4
|
+
/**
|
|
5
|
+
* Event listener callback type
|
|
6
|
+
*/
|
|
7
|
+
type EventListener = (action: Action, oldState: GameState, newState: GameState) => void;
|
|
8
|
+
/**
|
|
9
|
+
* Time provider function type for dependency injection
|
|
10
|
+
*/
|
|
11
|
+
type TimeProvider = () => number;
|
|
12
|
+
/**
|
|
13
|
+
* Main Poker Engine class
|
|
14
|
+
* Wraps the pure reducer with a stateful API
|
|
15
|
+
*/
|
|
16
|
+
export declare class PokerEngine {
|
|
17
|
+
private currentState;
|
|
18
|
+
private listeners;
|
|
19
|
+
private timeProvider;
|
|
20
|
+
constructor(config: TableConfig, timeProvider?: TimeProvider);
|
|
21
|
+
/**
|
|
22
|
+
* Add a player to the table
|
|
23
|
+
*/
|
|
24
|
+
sit(seat: number, id: string, name: string, stack: number): void;
|
|
25
|
+
/**
|
|
26
|
+
* Remove a player from the table
|
|
27
|
+
*/
|
|
28
|
+
stand(id: string): void;
|
|
29
|
+
/**
|
|
30
|
+
* Deal a new hand
|
|
31
|
+
*/
|
|
32
|
+
deal(): void;
|
|
33
|
+
/**
|
|
34
|
+
* Execute an action
|
|
35
|
+
* If action.timestamp is not provided, the engine will automatically set it
|
|
36
|
+
*/
|
|
37
|
+
act(action: Action): GameState;
|
|
38
|
+
/**
|
|
39
|
+
* Undo last action
|
|
40
|
+
*/
|
|
41
|
+
undo(): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Get current game state (full, unmasked)
|
|
44
|
+
*/
|
|
45
|
+
get state(): GameState;
|
|
46
|
+
/**
|
|
47
|
+
* Get player view (masked)
|
|
48
|
+
*/
|
|
49
|
+
view(playerId?: string): PublicState;
|
|
50
|
+
/**
|
|
51
|
+
* Get snapshot for serialization
|
|
52
|
+
*/
|
|
53
|
+
get snapshot(): Snapshot;
|
|
54
|
+
/**
|
|
55
|
+
* Restore from snapshot (static factory method)
|
|
56
|
+
*/
|
|
57
|
+
static restore(snapshot: Snapshot): PokerEngine;
|
|
58
|
+
/**
|
|
59
|
+
* Subscribe to state changes
|
|
60
|
+
*/
|
|
61
|
+
on(callback: EventListener): () => void;
|
|
62
|
+
/**
|
|
63
|
+
* Advance to next blind level (tournament)
|
|
64
|
+
*/
|
|
65
|
+
nextBlindLevel(): void;
|
|
66
|
+
/**
|
|
67
|
+
* Export hand history in specified format
|
|
68
|
+
*
|
|
69
|
+
* @param options Export format options
|
|
70
|
+
* @returns Formatted hand history string
|
|
71
|
+
*/
|
|
72
|
+
history(options?: ExportOptions): string;
|
|
73
|
+
/**
|
|
74
|
+
* Get structured hand history object
|
|
75
|
+
*
|
|
76
|
+
* @returns Hand history object
|
|
77
|
+
*/
|
|
78
|
+
getHandHistory(): HandHistory;
|
|
79
|
+
/**
|
|
80
|
+
* Dispatch action through reducer
|
|
81
|
+
*/
|
|
82
|
+
private dispatch;
|
|
83
|
+
/**
|
|
84
|
+
* Validate configuration
|
|
85
|
+
*/
|
|
86
|
+
private validateConfig;
|
|
87
|
+
/**
|
|
88
|
+
* Create initial game state
|
|
89
|
+
*/
|
|
90
|
+
private createInitialState;
|
|
91
|
+
}
|
|
92
|
+
export {};
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PokerEngine = void 0;
|
|
4
|
+
const gameReducer_1 = require("./gameReducer");
|
|
5
|
+
const viewMasking_1 = require("../utils/viewMasking");
|
|
6
|
+
const serialization_1 = require("../utils/serialization");
|
|
7
|
+
const ConfigError_1 = require("../errors/ConfigError");
|
|
8
|
+
const exporter_1 = require("../history/exporter");
|
|
9
|
+
const validation_1 = require("../utils/validation");
|
|
10
|
+
/**
|
|
11
|
+
* Main Poker Engine class
|
|
12
|
+
* Wraps the pure reducer with a stateful API
|
|
13
|
+
*/
|
|
14
|
+
class PokerEngine {
|
|
15
|
+
constructor(config, timeProvider = () => Date.now()) {
|
|
16
|
+
this.listeners = [];
|
|
17
|
+
// Validate config
|
|
18
|
+
this.validateConfig(config);
|
|
19
|
+
// Initialize time provider
|
|
20
|
+
this.timeProvider = timeProvider;
|
|
21
|
+
// Initialize state
|
|
22
|
+
this.currentState = this.createInitialState(config);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Add a player to the table
|
|
26
|
+
*/
|
|
27
|
+
sit(seat, id, name, stack) {
|
|
28
|
+
// Validate chip amount is a non-negative integer
|
|
29
|
+
(0, validation_1.validateChipAmount)(stack, "Sit stack");
|
|
30
|
+
const action = {
|
|
31
|
+
type: "SIT" /* ActionType.SIT */,
|
|
32
|
+
playerId: id,
|
|
33
|
+
playerName: name,
|
|
34
|
+
seat,
|
|
35
|
+
stack,
|
|
36
|
+
timestamp: this.timeProvider(),
|
|
37
|
+
};
|
|
38
|
+
this.dispatch(action);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Remove a player from the table
|
|
42
|
+
*/
|
|
43
|
+
stand(id) {
|
|
44
|
+
const action = {
|
|
45
|
+
type: "STAND" /* ActionType.STAND */,
|
|
46
|
+
playerId: id,
|
|
47
|
+
timestamp: this.timeProvider(),
|
|
48
|
+
};
|
|
49
|
+
this.dispatch(action);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Deal a new hand
|
|
53
|
+
*/
|
|
54
|
+
deal() {
|
|
55
|
+
const action = {
|
|
56
|
+
type: "DEAL" /* ActionType.DEAL */,
|
|
57
|
+
timestamp: this.timeProvider(),
|
|
58
|
+
};
|
|
59
|
+
this.dispatch(action);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Execute an action
|
|
63
|
+
* If action.timestamp is not provided, the engine will automatically set it
|
|
64
|
+
*/
|
|
65
|
+
act(action) {
|
|
66
|
+
// Ensure timestamp is set
|
|
67
|
+
const timestamp = action.timestamp ?? this.timeProvider();
|
|
68
|
+
// Validate timestamp if provided by caller
|
|
69
|
+
if (action.timestamp !== undefined) {
|
|
70
|
+
(0, validation_1.validateTimestamp)(timestamp, this.currentState.timestamp);
|
|
71
|
+
}
|
|
72
|
+
// Validate chip amounts for betting actions
|
|
73
|
+
if ("amount" in action && typeof action.amount === "number") {
|
|
74
|
+
(0, validation_1.validateChipAmount)(action.amount, `${action.type} amount`);
|
|
75
|
+
}
|
|
76
|
+
const actionWithTimestamp = {
|
|
77
|
+
...action,
|
|
78
|
+
timestamp,
|
|
79
|
+
};
|
|
80
|
+
this.dispatch(actionWithTimestamp);
|
|
81
|
+
return this.currentState;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Undo last action
|
|
85
|
+
*/
|
|
86
|
+
undo() {
|
|
87
|
+
if (this.currentState.previousStates.length === 0) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
const previousState = this.currentState.previousStates[this.currentState.previousStates.length - 1];
|
|
91
|
+
this.currentState = previousState;
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get current game state (full, unmasked)
|
|
96
|
+
*/
|
|
97
|
+
get state() {
|
|
98
|
+
return this.currentState;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Get player view (masked)
|
|
102
|
+
*/
|
|
103
|
+
view(playerId) {
|
|
104
|
+
return (0, viewMasking_1.createPublicView)(this.currentState, playerId ?? null);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Get snapshot for serialization
|
|
108
|
+
*/
|
|
109
|
+
get snapshot() {
|
|
110
|
+
return (0, serialization_1.createSnapshot)(this.currentState);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Restore from snapshot (static factory method)
|
|
114
|
+
*/
|
|
115
|
+
static restore(snapshot) {
|
|
116
|
+
const state = (0, serialization_1.restoreFromSnapshot)(snapshot);
|
|
117
|
+
const engine = new PokerEngine(state.config);
|
|
118
|
+
engine.currentState = state;
|
|
119
|
+
return engine;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Subscribe to state changes
|
|
123
|
+
*/
|
|
124
|
+
on(callback) {
|
|
125
|
+
this.listeners.push(callback);
|
|
126
|
+
// Return unsubscribe function
|
|
127
|
+
return () => {
|
|
128
|
+
const index = this.listeners.indexOf(callback);
|
|
129
|
+
if (index > -1) {
|
|
130
|
+
this.listeners.splice(index, 1);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Advance to next blind level (tournament)
|
|
136
|
+
*/
|
|
137
|
+
nextBlindLevel() {
|
|
138
|
+
if (!this.currentState.config.blindStructure) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const nextLevel = this.currentState.blindLevel + 1;
|
|
142
|
+
if (nextLevel >= this.currentState.config.blindStructure.length) {
|
|
143
|
+
return; // At max level
|
|
144
|
+
}
|
|
145
|
+
// Dispatch NEXT_BLIND_LEVEL action to notify listeners
|
|
146
|
+
this.dispatch({
|
|
147
|
+
type: "NEXT_BLIND_LEVEL" /* ActionType.NEXT_BLIND_LEVEL */,
|
|
148
|
+
timestamp: this.timeProvider(),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Export hand history in specified format
|
|
153
|
+
*
|
|
154
|
+
* @param options Export format options
|
|
155
|
+
* @returns Formatted hand history string
|
|
156
|
+
*/
|
|
157
|
+
history(options) {
|
|
158
|
+
return (0, exporter_1.exportHandHistory)(this.currentState, options);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Get structured hand history object
|
|
162
|
+
*
|
|
163
|
+
* @returns Hand history object
|
|
164
|
+
*/
|
|
165
|
+
getHandHistory() {
|
|
166
|
+
return (0, exporter_1.getHandHistory)(this.currentState);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Dispatch action through reducer
|
|
170
|
+
*/
|
|
171
|
+
dispatch(action) {
|
|
172
|
+
const oldState = this.currentState;
|
|
173
|
+
try {
|
|
174
|
+
this.currentState = (0, gameReducer_1.gameReducer)(this.currentState, action);
|
|
175
|
+
// Notify listeners
|
|
176
|
+
for (const listener of this.listeners) {
|
|
177
|
+
listener(action, oldState, this.currentState);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
// Re-throw error but keep old state
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Validate configuration
|
|
187
|
+
*/
|
|
188
|
+
validateConfig(config) {
|
|
189
|
+
if (config.smallBlind <= 0) {
|
|
190
|
+
throw new ConfigError_1.ConfigError("Small blind must be positive", {
|
|
191
|
+
smallBlind: config.smallBlind,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
if (config.bigBlind <= config.smallBlind) {
|
|
195
|
+
throw new ConfigError_1.ConfigError("Big blind must be greater than small blind", {
|
|
196
|
+
smallBlind: config.smallBlind,
|
|
197
|
+
bigBlind: config.bigBlind,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
const maxPlayers = config.maxPlayers ?? 9;
|
|
201
|
+
if (maxPlayers < 2 || maxPlayers > 10) {
|
|
202
|
+
throw new ConfigError_1.ConfigError("Max players must be between 2 and 10", {
|
|
203
|
+
maxPlayers,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Create initial game state
|
|
209
|
+
*/
|
|
210
|
+
createInitialState(config) {
|
|
211
|
+
const maxPlayers = config.maxPlayers ?? 9;
|
|
212
|
+
const players = Array(maxPlayers).fill(null);
|
|
213
|
+
// For tournaments, use blindStructure[0] for initial blinds/ante
|
|
214
|
+
const isTournament = !!config.blindStructure;
|
|
215
|
+
const initialBlinds = isTournament ? config.blindStructure[0] : null;
|
|
216
|
+
return {
|
|
217
|
+
config,
|
|
218
|
+
players,
|
|
219
|
+
maxPlayers,
|
|
220
|
+
handNumber: 0,
|
|
221
|
+
buttonSeat: null,
|
|
222
|
+
deck: [],
|
|
223
|
+
board: [],
|
|
224
|
+
street: "PREFLOP" /* Street.PREFLOP */,
|
|
225
|
+
pots: [],
|
|
226
|
+
currentBets: new Map(),
|
|
227
|
+
minRaise: initialBlinds?.bigBlind ?? config.bigBlind,
|
|
228
|
+
lastRaiseAmount: 0,
|
|
229
|
+
actionTo: null,
|
|
230
|
+
lastAggressorSeat: null,
|
|
231
|
+
activePlayers: [],
|
|
232
|
+
winners: null,
|
|
233
|
+
rakeThisHand: 0,
|
|
234
|
+
smallBlind: initialBlinds?.smallBlind ?? config.smallBlind,
|
|
235
|
+
bigBlind: initialBlinds?.bigBlind ?? config.bigBlind,
|
|
236
|
+
ante: initialBlinds?.ante ?? config.ante ?? 0,
|
|
237
|
+
blindLevel: 0,
|
|
238
|
+
timeBanks: new Map(),
|
|
239
|
+
actionHistory: [],
|
|
240
|
+
previousStates: [],
|
|
241
|
+
timestamp: Date.now(),
|
|
242
|
+
handId: "initial",
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
exports.PokerEngine = PokerEngine;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { GameState, Action } from "@pokertools/types";
|
|
2
|
+
/**
|
|
3
|
+
* Pure game reducer: f(state, action) => newState
|
|
4
|
+
* This is the heart of the poker engine
|
|
5
|
+
*
|
|
6
|
+
* @param state Current game state
|
|
7
|
+
* @param action Action to apply
|
|
8
|
+
* @returns New game state
|
|
9
|
+
*/
|
|
10
|
+
export declare function gameReducer(state: GameState, action: Action): GameState;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.gameReducer = gameReducer;
|
|
4
|
+
const validation_1 = require("../actions/validation");
|
|
5
|
+
const dealing_1 = require("../actions/dealing");
|
|
6
|
+
const betting_1 = require("../actions/betting");
|
|
7
|
+
const management_1 = require("../actions/management");
|
|
8
|
+
const showdownActions_1 = require("../actions/showdownActions");
|
|
9
|
+
const tournament_1 = require("../actions/tournament");
|
|
10
|
+
const special_1 = require("../actions/special");
|
|
11
|
+
const streetProgression_1 = require("../actions/streetProgression");
|
|
12
|
+
const sidePots_1 = require("../rules/sidePots");
|
|
13
|
+
const showdown_1 = require("../rules/showdown");
|
|
14
|
+
const invariants_1 = require("../utils/invariants");
|
|
15
|
+
const constants_1 = require("../utils/constants");
|
|
16
|
+
/**
|
|
17
|
+
* Pure game reducer: f(state, action) => newState
|
|
18
|
+
* This is the heart of the poker engine
|
|
19
|
+
*
|
|
20
|
+
* @param state Current game state
|
|
21
|
+
* @param action Action to apply
|
|
22
|
+
* @returns New game state
|
|
23
|
+
*/
|
|
24
|
+
function gameReducer(state, action) {
|
|
25
|
+
// Validate action
|
|
26
|
+
(0, validation_1.validateAction)(state, action);
|
|
27
|
+
// Apply action based on type
|
|
28
|
+
let newState;
|
|
29
|
+
switch (action.type) {
|
|
30
|
+
// Management actions
|
|
31
|
+
case "SIT" /* ActionType.SIT */:
|
|
32
|
+
newState = (0, management_1.handleSit)(state, action);
|
|
33
|
+
break;
|
|
34
|
+
case "STAND" /* ActionType.STAND */:
|
|
35
|
+
newState = (0, management_1.handleStand)(state, action);
|
|
36
|
+
break;
|
|
37
|
+
// Dealing
|
|
38
|
+
case "DEAL" /* ActionType.DEAL */:
|
|
39
|
+
newState = (0, dealing_1.handleDeal)(state, action);
|
|
40
|
+
break;
|
|
41
|
+
// Betting actions
|
|
42
|
+
case "FOLD" /* ActionType.FOLD */:
|
|
43
|
+
newState = (0, betting_1.handleFold)(state, action);
|
|
44
|
+
break;
|
|
45
|
+
case "CHECK" /* ActionType.CHECK */:
|
|
46
|
+
newState = (0, betting_1.handleCheck)(state, action);
|
|
47
|
+
break;
|
|
48
|
+
case "CALL" /* ActionType.CALL */:
|
|
49
|
+
newState = (0, betting_1.handleCall)(state, action);
|
|
50
|
+
break;
|
|
51
|
+
case "BET" /* ActionType.BET */:
|
|
52
|
+
// Auto-convert BET to appropriate action if there's already a bet
|
|
53
|
+
// This handles UI implementations that don't distinguish between BET/CALL/RAISE buttons
|
|
54
|
+
// Note: action.amount is the TOTAL bet size, not the amount to add
|
|
55
|
+
const currentBet = Math.max(...Array.from(state.currentBets.values()), 0);
|
|
56
|
+
if (currentBet > 0 && "amount" in action) {
|
|
57
|
+
if (action.amount === currentBet) {
|
|
58
|
+
// Amount equals current bet -> Convert to CALL
|
|
59
|
+
const callAction = {
|
|
60
|
+
type: "CALL" /* ActionType.CALL */,
|
|
61
|
+
playerId: action.playerId,
|
|
62
|
+
timestamp: action.timestamp,
|
|
63
|
+
};
|
|
64
|
+
newState = (0, betting_1.handleCall)(state, callAction);
|
|
65
|
+
}
|
|
66
|
+
else if (action.amount > currentBet) {
|
|
67
|
+
// Amount exceeds current bet -> Convert to RAISE
|
|
68
|
+
const raiseAction = {
|
|
69
|
+
type: "RAISE" /* ActionType.RAISE */,
|
|
70
|
+
playerId: action.playerId,
|
|
71
|
+
amount: action.amount,
|
|
72
|
+
timestamp: action.timestamp,
|
|
73
|
+
};
|
|
74
|
+
newState = (0, betting_1.handleRaise)(state, raiseAction);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
// Amount is less than current bet -> Keep as BET (will fail validation)
|
|
78
|
+
newState = (0, betting_1.handleBet)(state, action);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
newState = (0, betting_1.handleBet)(state, action);
|
|
83
|
+
}
|
|
84
|
+
break;
|
|
85
|
+
case "RAISE" /* ActionType.RAISE */:
|
|
86
|
+
newState = (0, betting_1.handleRaise)(state, action);
|
|
87
|
+
break;
|
|
88
|
+
// Showdown actions
|
|
89
|
+
case "SHOW" /* ActionType.SHOW */:
|
|
90
|
+
newState = (0, showdownActions_1.handleShow)(state, action);
|
|
91
|
+
break;
|
|
92
|
+
case "MUCK" /* ActionType.MUCK */:
|
|
93
|
+
newState = (0, showdownActions_1.handleMuck)(state, action);
|
|
94
|
+
break;
|
|
95
|
+
// Special actions
|
|
96
|
+
case "TIMEOUT" /* ActionType.TIMEOUT */:
|
|
97
|
+
newState = (0, special_1.handleTimeout)(state, action);
|
|
98
|
+
break;
|
|
99
|
+
case "TIME_BANK" /* ActionType.TIME_BANK */:
|
|
100
|
+
newState = (0, special_1.handleTimeBank)(state, action);
|
|
101
|
+
break;
|
|
102
|
+
case "NEXT_BLIND_LEVEL" /* ActionType.NEXT_BLIND_LEVEL */:
|
|
103
|
+
newState = (0, tournament_1.handleNextBlindLevel)(state, action);
|
|
104
|
+
break;
|
|
105
|
+
default:
|
|
106
|
+
// Unknown action type
|
|
107
|
+
newState = state;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
// Check if we should progress to next street
|
|
111
|
+
if ((0, streetProgression_1.shouldProgressStreet)(newState)) {
|
|
112
|
+
// Recalculate pots before progressing
|
|
113
|
+
newState = (0, sidePots_1.recalculatePots)(newState);
|
|
114
|
+
newState = (0, streetProgression_1.progressStreet)(newState);
|
|
115
|
+
}
|
|
116
|
+
// Check if we should go to showdown
|
|
117
|
+
if ((0, showdown_1.shouldShowdown)(newState)) {
|
|
118
|
+
newState = {
|
|
119
|
+
...newState,
|
|
120
|
+
street: newState.street, // Keep current street for showdown
|
|
121
|
+
};
|
|
122
|
+
newState = (0, showdown_1.determineWinners)(newState);
|
|
123
|
+
}
|
|
124
|
+
// Audit chip conservation (throws on failure)
|
|
125
|
+
// Enabled by default, can be disabled via config (not recommended for production)
|
|
126
|
+
if (newState.config.validateIntegrity !== false) {
|
|
127
|
+
(0, invariants_1.validateGameStateIntegrity)(newState);
|
|
128
|
+
}
|
|
129
|
+
// Add to previous states for undo
|
|
130
|
+
const previousStates = [...newState.previousStates, state].slice(-constants_1.MAX_UNDO_HISTORY);
|
|
131
|
+
return {
|
|
132
|
+
...newState,
|
|
133
|
+
previousStates,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { PokerEngineError } from "./PokerEngineError";
|
|
2
|
+
/**
|
|
3
|
+
* Error indicating invalid configuration
|
|
4
|
+
* Should fail at engine initialization
|
|
5
|
+
*/
|
|
6
|
+
export declare class ConfigError extends PokerEngineError {
|
|
7
|
+
constructor(message: string, context?: Record<string, unknown>);
|
|
8
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConfigError = void 0;
|
|
4
|
+
const PokerEngineError_1 = require("./PokerEngineError");
|
|
5
|
+
/**
|
|
6
|
+
* Error indicating invalid configuration
|
|
7
|
+
* Should fail at engine initialization
|
|
8
|
+
*/
|
|
9
|
+
class ConfigError extends PokerEngineError_1.PokerEngineError {
|
|
10
|
+
constructor(message, context = {}) {
|
|
11
|
+
super("INVALID_CONFIG", message, context);
|
|
12
|
+
this.name = "ConfigError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
exports.ConfigError = ConfigError;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { PokerEngineError } from "./PokerEngineError";
|
|
2
|
+
/**
|
|
3
|
+
* Critical error indicating state corruption or invariant violation
|
|
4
|
+
* When this occurs, the table should be FROZEN immediately
|
|
5
|
+
*/
|
|
6
|
+
export declare class CriticalStateError extends PokerEngineError {
|
|
7
|
+
constructor(message: string, context?: Record<string, unknown>);
|
|
8
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CriticalStateError = void 0;
|
|
4
|
+
const PokerEngineError_1 = require("./PokerEngineError");
|
|
5
|
+
/**
|
|
6
|
+
* Critical error indicating state corruption or invariant violation
|
|
7
|
+
* When this occurs, the table should be FROZEN immediately
|
|
8
|
+
*/
|
|
9
|
+
class CriticalStateError extends PokerEngineError_1.PokerEngineError {
|
|
10
|
+
constructor(message, context = {}) {
|
|
11
|
+
super("CRITICAL_INVARIANT_FAILURE", message, context);
|
|
12
|
+
this.name = "CriticalStateError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
exports.CriticalStateError = CriticalStateError;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standardized error codes for IllegalActionError
|
|
3
|
+
*
|
|
4
|
+
* Using a const enum pattern for:
|
|
5
|
+
* - Type safety at compile time
|
|
6
|
+
* - Autocomplete in IDEs
|
|
7
|
+
* - Documentation of all possible error codes
|
|
8
|
+
* - Easy refactoring
|
|
9
|
+
*/
|
|
10
|
+
export declare const ErrorCodes: {
|
|
11
|
+
readonly INVALID_ACTION: "INVALID_ACTION";
|
|
12
|
+
readonly PLAYER_NOT_FOUND: "PLAYER_NOT_FOUND";
|
|
13
|
+
readonly PLAYER_NOT_ACTIVE: "PLAYER_NOT_ACTIVE";
|
|
14
|
+
readonly NOT_YOUR_TURN: "NOT_YOUR_TURN";
|
|
15
|
+
readonly NO_CHIPS: "NO_CHIPS";
|
|
16
|
+
readonly CANNOT_CHECK: "CANNOT_CHECK";
|
|
17
|
+
readonly NOTHING_TO_CALL: "NOTHING_TO_CALL";
|
|
18
|
+
readonly CANNOT_BET: "CANNOT_BET";
|
|
19
|
+
readonly BET_TOO_SMALL: "BET_TOO_SMALL";
|
|
20
|
+
readonly CANNOT_RAISE: "CANNOT_RAISE";
|
|
21
|
+
readonly CANNOT_RERAISE: "CANNOT_RERAISE";
|
|
22
|
+
readonly RAISE_TOO_SMALL: "RAISE_TOO_SMALL";
|
|
23
|
+
readonly CANNOT_DEAL: "CANNOT_DEAL";
|
|
24
|
+
readonly NOT_ENOUGH_PLAYERS: "NOT_ENOUGH_PLAYERS";
|
|
25
|
+
readonly INVALID_SEAT: "INVALID_SEAT";
|
|
26
|
+
readonly SEAT_OCCUPIED: "SEAT_OCCUPIED";
|
|
27
|
+
readonly INVALID_STACK: "INVALID_STACK";
|
|
28
|
+
readonly INVALID_AMOUNT: "INVALID_AMOUNT";
|
|
29
|
+
readonly INVALID_TIMESTAMP: "INVALID_TIMESTAMP";
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Type representing all valid error codes
|
|
33
|
+
*/
|
|
34
|
+
export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
|
|
35
|
+
/**
|
|
36
|
+
* Helper to check if an error message contains a specific error code
|
|
37
|
+
*/
|
|
38
|
+
export declare function hasErrorCode(error: Error, code: ErrorCode): boolean;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ErrorCodes = void 0;
|
|
4
|
+
exports.hasErrorCode = hasErrorCode;
|
|
5
|
+
/**
|
|
6
|
+
* Standardized error codes for IllegalActionError
|
|
7
|
+
*
|
|
8
|
+
* Using a const enum pattern for:
|
|
9
|
+
* - Type safety at compile time
|
|
10
|
+
* - Autocomplete in IDEs
|
|
11
|
+
* - Documentation of all possible error codes
|
|
12
|
+
* - Easy refactoring
|
|
13
|
+
*/
|
|
14
|
+
exports.ErrorCodes = {
|
|
15
|
+
// Generic errors
|
|
16
|
+
INVALID_ACTION: "INVALID_ACTION",
|
|
17
|
+
// Player errors
|
|
18
|
+
PLAYER_NOT_FOUND: "PLAYER_NOT_FOUND",
|
|
19
|
+
PLAYER_NOT_ACTIVE: "PLAYER_NOT_ACTIVE",
|
|
20
|
+
NOT_YOUR_TURN: "NOT_YOUR_TURN",
|
|
21
|
+
NO_CHIPS: "NO_CHIPS",
|
|
22
|
+
// Betting action errors
|
|
23
|
+
CANNOT_CHECK: "CANNOT_CHECK",
|
|
24
|
+
NOTHING_TO_CALL: "NOTHING_TO_CALL",
|
|
25
|
+
CANNOT_BET: "CANNOT_BET",
|
|
26
|
+
BET_TOO_SMALL: "BET_TOO_SMALL",
|
|
27
|
+
CANNOT_RAISE: "CANNOT_RAISE",
|
|
28
|
+
CANNOT_RERAISE: "CANNOT_RERAISE",
|
|
29
|
+
RAISE_TOO_SMALL: "RAISE_TOO_SMALL",
|
|
30
|
+
// Deal errors
|
|
31
|
+
CANNOT_DEAL: "CANNOT_DEAL",
|
|
32
|
+
NOT_ENOUGH_PLAYERS: "NOT_ENOUGH_PLAYERS",
|
|
33
|
+
// Seat errors
|
|
34
|
+
INVALID_SEAT: "INVALID_SEAT",
|
|
35
|
+
SEAT_OCCUPIED: "SEAT_OCCUPIED",
|
|
36
|
+
INVALID_STACK: "INVALID_STACK",
|
|
37
|
+
// Validation errors
|
|
38
|
+
INVALID_AMOUNT: "INVALID_AMOUNT",
|
|
39
|
+
INVALID_TIMESTAMP: "INVALID_TIMESTAMP",
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Helper to check if an error message contains a specific error code
|
|
43
|
+
*/
|
|
44
|
+
function hasErrorCode(error, code) {
|
|
45
|
+
return error.message.includes(code);
|
|
46
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { PokerEngineError } from "./PokerEngineError";
|
|
2
|
+
import { ErrorCode } from "./ErrorCodes";
|
|
3
|
+
/**
|
|
4
|
+
* Error indicating an illegal or invalid action
|
|
5
|
+
* Action should be rejected and error sent to client
|
|
6
|
+
*/
|
|
7
|
+
export declare class IllegalActionError extends PokerEngineError {
|
|
8
|
+
constructor(code: ErrorCode, message: string, context?: Record<string, unknown>);
|
|
9
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.IllegalActionError = void 0;
|
|
4
|
+
const PokerEngineError_1 = require("./PokerEngineError");
|
|
5
|
+
/**
|
|
6
|
+
* Error indicating an illegal or invalid action
|
|
7
|
+
* Action should be rejected and error sent to client
|
|
8
|
+
*/
|
|
9
|
+
class IllegalActionError extends PokerEngineError_1.PokerEngineError {
|
|
10
|
+
constructor(code, message, context = {}) {
|
|
11
|
+
super(code, message, context);
|
|
12
|
+
this.name = "IllegalActionError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
exports.IllegalActionError = IllegalActionError;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base error class for all poker engine errors
|
|
3
|
+
*/
|
|
4
|
+
export declare class PokerEngineError extends Error {
|
|
5
|
+
readonly code: string;
|
|
6
|
+
readonly context: Record<string, unknown>;
|
|
7
|
+
constructor(code: string, message: string, context?: Record<string, unknown>);
|
|
8
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PokerEngineError = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Base error class for all poker engine errors
|
|
6
|
+
*/
|
|
7
|
+
class PokerEngineError extends Error {
|
|
8
|
+
constructor(code, message, context = {}) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "PokerEngineError";
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.context = context;
|
|
13
|
+
// Maintains proper stack trace for where error was thrown (V8 only)
|
|
14
|
+
if (Error.captureStackTrace) {
|
|
15
|
+
Error.captureStackTrace(this, this.constructor);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.PokerEngineError = PokerEngineError;
|