@pokertools/engine 1.0.1 → 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 +591 -445
- package/dist/.tsbuildinfo +1 -0
- package/dist/actions/betting.js +7 -2
- package/dist/actions/dealing.js +46 -20
- package/dist/actions/management.js +26 -5
- package/dist/actions/special.d.ts +18 -0
- package/dist/actions/special.js +20 -0
- 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/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 +31 -5
- package/dist/history/types.d.ts +0 -73
- package/dist/history/types.js +0 -5
- package/dist/tsconfig.tsbuildinfo +0 -1
|
@@ -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(),
|
|
@@ -1,38 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* ErrorCodes are now managed in @pokertools/types for consistency across packages.
|
|
3
|
+
* This file re-exports them for backwards compatibility.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
* - Type safety at compile time
|
|
6
|
-
* - Autocomplete in IDEs
|
|
7
|
-
* - Documentation of all possible error codes
|
|
8
|
-
* - Easy refactoring
|
|
5
|
+
* @deprecated Import from "@pokertools/types" instead
|
|
9
6
|
*/
|
|
10
|
-
export
|
|
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;
|
|
7
|
+
export { ErrorCodes, type ErrorCode, hasErrorCode } from "@pokertools/types";
|
|
@@ -1,46 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ErrorCodes = void 0;
|
|
4
|
-
exports.hasErrorCode = hasErrorCode;
|
|
3
|
+
exports.hasErrorCode = exports.ErrorCodes = void 0;
|
|
5
4
|
/**
|
|
6
|
-
*
|
|
5
|
+
* ErrorCodes are now managed in @pokertools/types for consistency across packages.
|
|
6
|
+
* This file re-exports them for backwards compatibility.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* - Type safety at compile time
|
|
10
|
-
* - Autocomplete in IDEs
|
|
11
|
-
* - Documentation of all possible error codes
|
|
12
|
-
* - Easy refactoring
|
|
8
|
+
* @deprecated Import from "@pokertools/types" instead
|
|
13
9
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
}
|
|
10
|
+
var types_1 = require("@pokertools/types");
|
|
11
|
+
Object.defineProperty(exports, "ErrorCodes", { enumerable: true, get: function () { return types_1.ErrorCodes; } });
|
|
12
|
+
Object.defineProperty(exports, "hasErrorCode", { enumerable: true, get: function () { return types_1.hasErrorCode; } });
|
package/dist/errors/index.d.ts
CHANGED
package/dist/errors/index.js
CHANGED
|
@@ -19,4 +19,4 @@ __exportStar(require("./PokerEngineError"), exports);
|
|
|
19
19
|
__exportStar(require("./CriticalStateError"), exports);
|
|
20
20
|
__exportStar(require("./IllegalActionError"), exports);
|
|
21
21
|
__exportStar(require("./ConfigError"), exports);
|
|
22
|
-
|
|
22
|
+
// ErrorCodes now exported from @pokertools/types
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Main hand history exporter
|
|
3
3
|
*/
|
|
4
|
-
import { GameState } from "@pokertools/types";
|
|
5
|
-
import { HandHistory, ExportOptions } from "./types";
|
|
4
|
+
import { GameState, HandHistory, ExportOptions } from "@pokertools/types";
|
|
6
5
|
/**
|
|
7
6
|
* Export hand history from game state
|
|
8
7
|
*
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Build hand history from game state and action history
|
|
3
3
|
*/
|
|
4
|
-
import { GameState } from "@pokertools/types";
|
|
5
|
-
import { HandHistory } from "./types";
|
|
4
|
+
import { GameState, HandHistory } from "@pokertools/types";
|
|
6
5
|
/**
|
|
7
6
|
* Build complete hand history from final game state
|
|
8
7
|
* Call this after a hand is complete (winners determined)
|
|
@@ -41,12 +41,15 @@ function buildPlayerHistory(state) {
|
|
|
41
41
|
continue;
|
|
42
42
|
// Calculate starting stack (current + invested)
|
|
43
43
|
const startingStack = player.stack + player.totalInvestedThisHand;
|
|
44
|
+
// Only include cards if they are fully visible (no masked/null cards)
|
|
45
|
+
const hasMaskedCards = player.hand?.some((c) => c === null);
|
|
46
|
+
const cards = player.hand && !hasMaskedCards ? player.hand : undefined;
|
|
44
47
|
players.push({
|
|
45
48
|
seat: player.seat,
|
|
46
49
|
name: player.name,
|
|
47
50
|
startingStack,
|
|
48
51
|
endingStack: player.stack,
|
|
49
|
-
cards
|
|
52
|
+
cards,
|
|
50
53
|
});
|
|
51
54
|
}
|
|
52
55
|
return players;
|
package/dist/index.d.ts
CHANGED
|
@@ -5,4 +5,4 @@ export { createSnapshot, restoreFromSnapshot, Snapshot } from "./utils/serializa
|
|
|
5
5
|
export { createPublicView } from "./utils/viewMasking";
|
|
6
6
|
export { calculateTotalChips, auditChipConservation } from "./utils/invariants";
|
|
7
7
|
export { exportHandHistory, getHandHistory, exportMultipleHands } from "./history/exporter";
|
|
8
|
-
export
|
|
8
|
+
export { HandHistory, HandHistoryPlayer, StreetHistory, ExportOptions } from "@pokertools/types";
|
|
@@ -33,7 +33,7 @@ function getNextToActNormal(state) {
|
|
|
33
33
|
while (seat !== startSeat) {
|
|
34
34
|
const player = state.players[seat];
|
|
35
35
|
// Skip if: no player, folded, all-in, or busted
|
|
36
|
-
if (
|
|
36
|
+
if (player?.status !== "ACTIVE" /* PlayerStatus.ACTIVE */ || player.stack === 0) {
|
|
37
37
|
seat = (0, positioning_1.getNextSeat)(seat, state.maxPlayers);
|
|
38
38
|
continue;
|
|
39
39
|
}
|
|
@@ -77,7 +77,7 @@ function getNextToActHeadsUp(state) {
|
|
|
77
77
|
// Check both players
|
|
78
78
|
for (const seat of actionOrder) {
|
|
79
79
|
const player = state.players[seat];
|
|
80
|
-
if (
|
|
80
|
+
if (player?.status !== "ACTIVE" /* PlayerStatus.ACTIVE */ || player.stack === 0) {
|
|
81
81
|
continue;
|
|
82
82
|
}
|
|
83
83
|
const playerBet = state.currentBets.get(seat) ?? 0;
|
|
@@ -128,7 +128,7 @@ function getNextActionableSeat(startSeat, state) {
|
|
|
128
128
|
// Scan full circle
|
|
129
129
|
while (seat !== endSeat) {
|
|
130
130
|
const player = state.players[seat];
|
|
131
|
-
if (player
|
|
131
|
+
if (player?.status === "ACTIVE" /* PlayerStatus.ACTIVE */ && player.stack > 0) {
|
|
132
132
|
return seat;
|
|
133
133
|
}
|
|
134
134
|
seat = (0, positioning_1.getNextSeat)(seat, state.maxPlayers);
|
|
@@ -173,7 +173,7 @@ function isActionComplete(state) {
|
|
|
173
173
|
if (activeCount === 0) {
|
|
174
174
|
// Only return true if we're in a hand (not pre-deal)
|
|
175
175
|
// Check: Are there all-in players with bets?
|
|
176
|
-
const allInPlayers = state.players.filter((p) => p
|
|
176
|
+
const allInPlayers = state.players.filter((p) => p?.status === "ALL_IN" /* PlayerStatus.ALL_IN */);
|
|
177
177
|
return allInPlayers.length > 0 && state.currentBets.size > 0;
|
|
178
178
|
}
|
|
179
179
|
return activeCount > 0 && activeCount === actedCount;
|
package/dist/rules/blinds.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export interface BlindPositions {
|
|
|
16
16
|
* Normal (Dead Button Rule):
|
|
17
17
|
* - SB = Button + 1 (Can be empty -> Dead Small Blind)
|
|
18
18
|
* - BB = Next Occupied Seat after SB
|
|
19
|
+
*
|
|
20
|
+
* For cash games: Skips sitting-out players (unless they are in tournament mode)
|
|
19
21
|
*/
|
|
20
22
|
export declare function getBlindPositions(state: GameState): BlindPositions | null;
|
|
21
23
|
/**
|
package/dist/rules/blinds.js
CHANGED
|
@@ -14,15 +14,18 @@ const headsUp_1 = require("./headsUp");
|
|
|
14
14
|
* Normal (Dead Button Rule):
|
|
15
15
|
* - SB = Button + 1 (Can be empty -> Dead Small Blind)
|
|
16
16
|
* - BB = Next Occupied Seat after SB
|
|
17
|
+
*
|
|
18
|
+
* For cash games: Skips sitting-out players (unless they are in tournament mode)
|
|
17
19
|
*/
|
|
18
20
|
function getBlindPositions(state) {
|
|
19
21
|
if (state.buttonSeat === null) {
|
|
20
22
|
return null;
|
|
21
23
|
}
|
|
22
24
|
const buttonSeat = state.buttonSeat;
|
|
25
|
+
const isTournament = !!state.config.blindStructure;
|
|
23
26
|
// Heads-up specific logic (Button is SB)
|
|
24
27
|
if ((0, headsUp_1.isHeadsUp)(state)) {
|
|
25
|
-
const bbSeat = (
|
|
28
|
+
const bbSeat = getNextActiveOrOccupiedSeat(buttonSeat, state.players, state.maxPlayers, isTournament);
|
|
26
29
|
if (bbSeat === null) {
|
|
27
30
|
return null;
|
|
28
31
|
}
|
|
@@ -34,8 +37,10 @@ function getBlindPositions(state) {
|
|
|
34
37
|
// Normal Play (Dead Button / Dead Small Blind Logic)
|
|
35
38
|
// 1. SB is ALWAYS the immediate next seat, even if empty
|
|
36
39
|
const sbSeat = (0, positioning_1.getNextSeat)(buttonSeat, state.maxPlayers);
|
|
37
|
-
// 2. BB is the next ACTIVE
|
|
38
|
-
|
|
40
|
+
// 2. BB is the next ACTIVE player after the SB position
|
|
41
|
+
// In cash games, skip sitting-out players
|
|
42
|
+
// In tournaments, include sitting-out players (they must post blinds)
|
|
43
|
+
const bbSeat = getNextActiveOrOccupiedSeat(sbSeat, state.players, state.maxPlayers, isTournament);
|
|
39
44
|
if (bbSeat === null) {
|
|
40
45
|
return null;
|
|
41
46
|
}
|
|
@@ -44,6 +49,25 @@ function getBlindPositions(state) {
|
|
|
44
49
|
bigBlindSeat: bbSeat,
|
|
45
50
|
};
|
|
46
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Get next seat that is occupied and (if cash game) not sitting out
|
|
54
|
+
*/
|
|
55
|
+
function getNextActiveOrOccupiedSeat(currentSeat, players, maxPlayers, isTournament) {
|
|
56
|
+
let seat = (0, positioning_1.getNextSeat)(currentSeat, maxPlayers);
|
|
57
|
+
const startSeat = currentSeat;
|
|
58
|
+
while (seat !== startSeat) {
|
|
59
|
+
const player = players[seat];
|
|
60
|
+
if (player !== null && player.stack > 0) {
|
|
61
|
+
// In tournaments, include sitting-out players (they must blind off)
|
|
62
|
+
// In cash games, skip sitting-out players
|
|
63
|
+
if (isTournament || !player.isSittingOut) {
|
|
64
|
+
return seat;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
seat = (0, positioning_1.getNextSeat)(seat, maxPlayers);
|
|
68
|
+
}
|
|
69
|
+
return null; // No eligible seats
|
|
70
|
+
}
|
|
47
71
|
/**
|
|
48
72
|
* Calculate blind amounts for antes
|
|
49
73
|
*/
|
package/dist/rules/headsUp.js
CHANGED
|
@@ -32,6 +32,24 @@ function getHeadsUpActionOrder(state, street) {
|
|
|
32
32
|
}
|
|
33
33
|
// Find the two seats
|
|
34
34
|
const [seat1, seat2] = activePlayers.sort((a, b) => a - b);
|
|
35
|
+
// Check if button is one of the active players
|
|
36
|
+
const isButtonActive = activePlayers.includes(buttonSeat);
|
|
37
|
+
if (!isButtonActive) {
|
|
38
|
+
// Dead button scenario - button is not one of the active players
|
|
39
|
+
// In this case, the "button" for action purposes is the first active player
|
|
40
|
+
// after the actual button position
|
|
41
|
+
const effectiveButton = seat1 > buttonSeat || seat2 < buttonSeat ? seat1 : seat2;
|
|
42
|
+
const otherSeat = effectiveButton === seat1 ? seat2 : seat1;
|
|
43
|
+
if (street === "PREFLOP" /* Street.PREFLOP */) {
|
|
44
|
+
// Effective button acts first preflop
|
|
45
|
+
return [effectiveButton, otherSeat];
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
// Effective button acts last postflop
|
|
49
|
+
return [otherSeat, effectiveButton];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Normal case: button is one of the active players
|
|
35
53
|
const otherSeat = seat1 === buttonSeat ? seat2 : seat1;
|
|
36
54
|
if (street === "PREFLOP" /* Street.PREFLOP */) {
|
|
37
55
|
// Button acts first preflop
|
package/dist/rules/showdown.js
CHANGED
|
@@ -23,6 +23,9 @@ function determineWinners(state) {
|
|
|
23
23
|
});
|
|
24
24
|
for (const pot of sortedPots) {
|
|
25
25
|
const potWinners = evaluatePot(state, pot);
|
|
26
|
+
if (potWinners.length === 0) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
26
29
|
// Calculate and deduct rake (cash games only)
|
|
27
30
|
// Apply GLOBAL rake cap across all pots (per-hand, not per-pot)
|
|
28
31
|
const { rake } = (0, rake_1.calculateRake)(state, pot.amount, totalRake);
|
|
@@ -122,6 +125,10 @@ function evaluatePot(state, pot) {
|
|
|
122
125
|
for (const player of eligible) {
|
|
123
126
|
if (!player?.hand)
|
|
124
127
|
continue;
|
|
128
|
+
// Skip masked hands (client mode)
|
|
129
|
+
if (player.hand.some((c) => c === null)) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
125
132
|
// Combine hole cards + board (7 cards total for river)
|
|
126
133
|
const allCards = [...player.hand, ...state.board];
|
|
127
134
|
if (allCards.length < 5) {
|
|
@@ -140,6 +147,9 @@ function evaluatePot(state, pot) {
|
|
|
140
147
|
description,
|
|
141
148
|
});
|
|
142
149
|
}
|
|
150
|
+
if (evaluations.length === 0) {
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
143
153
|
// Find best hand(s)
|
|
144
154
|
const bestScore = Math.min(...evaluations.map((e) => e.score));
|
|
145
155
|
const winners = evaluations.filter((e) => e.score === bestScore);
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
export declare function cardCodesToStrings(codes: readonly number[]): string[];
|
|
5
5
|
/**
|
|
6
6
|
* Convert string card array to integer codes
|
|
7
|
+
* Filters out null (masked) cards
|
|
7
8
|
*/
|
|
8
|
-
export declare function cardStringsToCards(cards:
|
|
9
|
+
export declare function cardStringsToCards(cards: ReadonlyArray<string | null>): number[];
|
|
9
10
|
/**
|
|
10
11
|
* Validate card string format
|
|
11
12
|
*/
|
package/dist/utils/cardUtils.js
CHANGED
|
@@ -12,9 +12,10 @@ function cardCodesToStrings(codes) {
|
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
14
|
* Convert string card array to integer codes
|
|
15
|
+
* Filters out null (masked) cards
|
|
15
16
|
*/
|
|
16
17
|
function cardStringsToCards(cards) {
|
|
17
|
-
return cards.map((card) => (0, evaluator_1.getCardCode)(card));
|
|
18
|
+
return cards.filter((c) => c !== null).map((card) => (0, evaluator_1.getCardCode)(card));
|
|
18
19
|
}
|
|
19
20
|
/**
|
|
20
21
|
* Validate card string format
|
package/dist/utils/invariants.js
CHANGED
|
@@ -105,6 +105,10 @@ function getInitialChips(state) {
|
|
|
105
105
|
* Checks multiple invariants beyond just chip conservation
|
|
106
106
|
*/
|
|
107
107
|
function validateGameStateIntegrity(state) {
|
|
108
|
+
// Skip strict integrity checks in client mode to prevent UI crashes on minor sync issues
|
|
109
|
+
if (state.config.isClient) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
108
112
|
// 1. Chip conservation
|
|
109
113
|
const initialChips = getInitialChips(state);
|
|
110
114
|
auditChipConservation(state, initialChips);
|
|
@@ -30,7 +30,7 @@ function getActivePlayers(state) {
|
|
|
30
30
|
const active = [];
|
|
31
31
|
for (let i = 0; i < state.players.length; i++) {
|
|
32
32
|
const player = state.players[i];
|
|
33
|
-
if (player
|
|
33
|
+
if (player?.status === "ACTIVE" /* PlayerStatus.ACTIVE */ && player.stack > 0) {
|
|
34
34
|
active.push(i);
|
|
35
35
|
}
|
|
36
36
|
}
|
|
@@ -70,7 +70,7 @@ function getNextActionableSeat(currentSeat, state) {
|
|
|
70
70
|
const startSeat = currentSeat;
|
|
71
71
|
while (seat !== startSeat) {
|
|
72
72
|
const player = state.players[seat];
|
|
73
|
-
if (player
|
|
73
|
+
if (player?.status === "ACTIVE" /* PlayerStatus.ACTIVE */ && player.stack > 0) {
|
|
74
74
|
return seat;
|
|
75
75
|
}
|
|
76
76
|
seat = getNextSeat(seat, state.maxPlayers);
|
|
@@ -25,6 +25,7 @@ export interface Snapshot {
|
|
|
25
25
|
readonly ante: number;
|
|
26
26
|
readonly blindLevel: number;
|
|
27
27
|
readonly timeBanks: Record<number, number>;
|
|
28
|
+
readonly timeBankActiveSeat: number | null;
|
|
28
29
|
readonly actionHistory: ActionRecord[];
|
|
29
30
|
readonly previousStates: Snapshot[];
|
|
30
31
|
readonly timestamp: number;
|
|
@@ -44,6 +44,7 @@ function createSnapshot(state) {
|
|
|
44
44
|
ante: state.ante,
|
|
45
45
|
blindLevel: state.blindLevel,
|
|
46
46
|
timeBanks,
|
|
47
|
+
timeBankActiveSeat: state.timeBankActiveSeat,
|
|
47
48
|
actionHistory: Array.from(state.actionHistory),
|
|
48
49
|
previousStates,
|
|
49
50
|
timestamp: state.timestamp,
|
|
@@ -69,6 +70,7 @@ function restoreFromSnapshot(snapshot) {
|
|
|
69
70
|
...snapshot,
|
|
70
71
|
currentBets,
|
|
71
72
|
timeBanks,
|
|
73
|
+
timeBankActiveSeat: snapshot.timeBankActiveSeat ?? null, // Backward compatibility
|
|
72
74
|
previousStates,
|
|
73
75
|
rakeThisHand: snapshot.rakeThisHand || 0, // Add missing field with default
|
|
74
76
|
};
|
|
@@ -6,9 +6,10 @@ import { GameState, PublicState } from "@pokertools/types";
|
|
|
6
6
|
*
|
|
7
7
|
* @param state Full game state
|
|
8
8
|
* @param playerId Player requesting view (null = spectator)
|
|
9
|
+
* @param version State version number (defaults to 0 if not provided)
|
|
9
10
|
* @returns Masked public state
|
|
10
11
|
*/
|
|
11
|
-
export declare function createPublicView(state: GameState, playerId?: string | null): PublicState;
|
|
12
|
+
export declare function createPublicView(state: GameState, playerId?: string | null, version?: number): PublicState;
|
|
12
13
|
/**
|
|
13
14
|
* Create spectator view (no player-specific information)
|
|
14
15
|
*/
|