@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,36 @@
|
|
|
1
|
+
import { GameState, Player, PlayerStatus } from "@pokertools/types";
|
|
2
|
+
/**
|
|
3
|
+
* Get the next seat clockwise from current seat
|
|
4
|
+
*/
|
|
5
|
+
export declare function getNextSeat(currentSeat: number, maxPlayers: number): number;
|
|
6
|
+
/**
|
|
7
|
+
* Get distance from button to a seat (clockwise)
|
|
8
|
+
*/
|
|
9
|
+
export declare function getDistanceFromButton(seat: number, buttonSeat: number, maxPlayers: number): number;
|
|
10
|
+
/**
|
|
11
|
+
* Get all active player seats (not folded, not busted, has chips)
|
|
12
|
+
*/
|
|
13
|
+
export declare function getActivePlayers(state: GameState): number[];
|
|
14
|
+
/**
|
|
15
|
+
* Get all seated players (including sitting out, but not empty seats)
|
|
16
|
+
*/
|
|
17
|
+
export declare function getSeatedPlayers(state: GameState): number[];
|
|
18
|
+
/**
|
|
19
|
+
* Find next occupied seat clockwise from current seat
|
|
20
|
+
*/
|
|
21
|
+
export declare function getNextOccupiedSeat(currentSeat: number, players: ReadonlyArray<Player | null>, maxPlayers: number): number | null;
|
|
22
|
+
/**
|
|
23
|
+
* Find next player who can act (ACTIVE status, not all-in)
|
|
24
|
+
*/
|
|
25
|
+
export declare function getNextActionableSeat(currentSeat: number, state: GameState): number | null;
|
|
26
|
+
/**
|
|
27
|
+
* Count players with specific status
|
|
28
|
+
*/
|
|
29
|
+
export declare function countPlayersByStatus(state: GameState, status: PlayerStatus): number;
|
|
30
|
+
/**
|
|
31
|
+
* Get player by ID
|
|
32
|
+
*/
|
|
33
|
+
export declare function getPlayerById(state: GameState, playerId: string): {
|
|
34
|
+
player: Player;
|
|
35
|
+
seat: number;
|
|
36
|
+
} | null;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getNextSeat = getNextSeat;
|
|
4
|
+
exports.getDistanceFromButton = getDistanceFromButton;
|
|
5
|
+
exports.getActivePlayers = getActivePlayers;
|
|
6
|
+
exports.getSeatedPlayers = getSeatedPlayers;
|
|
7
|
+
exports.getNextOccupiedSeat = getNextOccupiedSeat;
|
|
8
|
+
exports.getNextActionableSeat = getNextActionableSeat;
|
|
9
|
+
exports.countPlayersByStatus = countPlayersByStatus;
|
|
10
|
+
exports.getPlayerById = getPlayerById;
|
|
11
|
+
/**
|
|
12
|
+
* Get the next seat clockwise from current seat
|
|
13
|
+
*/
|
|
14
|
+
function getNextSeat(currentSeat, maxPlayers) {
|
|
15
|
+
return (currentSeat + 1) % maxPlayers;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Get distance from button to a seat (clockwise)
|
|
19
|
+
*/
|
|
20
|
+
function getDistanceFromButton(seat, buttonSeat, maxPlayers) {
|
|
21
|
+
if (seat >= buttonSeat) {
|
|
22
|
+
return seat - buttonSeat;
|
|
23
|
+
}
|
|
24
|
+
return maxPlayers - buttonSeat + seat;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Get all active player seats (not folded, not busted, has chips)
|
|
28
|
+
*/
|
|
29
|
+
function getActivePlayers(state) {
|
|
30
|
+
const active = [];
|
|
31
|
+
for (let i = 0; i < state.players.length; i++) {
|
|
32
|
+
const player = state.players[i];
|
|
33
|
+
if (player && player.status === "ACTIVE" /* PlayerStatus.ACTIVE */ && player.stack > 0) {
|
|
34
|
+
active.push(i);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return active;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Get all seated players (including sitting out, but not empty seats)
|
|
41
|
+
*/
|
|
42
|
+
function getSeatedPlayers(state) {
|
|
43
|
+
const seated = [];
|
|
44
|
+
for (let i = 0; i < state.players.length; i++) {
|
|
45
|
+
if (state.players[i] !== null) {
|
|
46
|
+
seated.push(i);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return seated;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Find next occupied seat clockwise from current seat
|
|
53
|
+
*/
|
|
54
|
+
function getNextOccupiedSeat(currentSeat, players, maxPlayers) {
|
|
55
|
+
let seat = getNextSeat(currentSeat, maxPlayers);
|
|
56
|
+
const startSeat = currentSeat;
|
|
57
|
+
while (seat !== startSeat) {
|
|
58
|
+
if (players[seat] !== null && players[seat].stack > 0) {
|
|
59
|
+
return seat;
|
|
60
|
+
}
|
|
61
|
+
seat = getNextSeat(seat, maxPlayers);
|
|
62
|
+
}
|
|
63
|
+
return null; // No other occupied seats
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Find next player who can act (ACTIVE status, not all-in)
|
|
67
|
+
*/
|
|
68
|
+
function getNextActionableSeat(currentSeat, state) {
|
|
69
|
+
let seat = getNextSeat(currentSeat, state.maxPlayers);
|
|
70
|
+
const startSeat = currentSeat;
|
|
71
|
+
while (seat !== startSeat) {
|
|
72
|
+
const player = state.players[seat];
|
|
73
|
+
if (player && player.status === "ACTIVE" /* PlayerStatus.ACTIVE */ && player.stack > 0) {
|
|
74
|
+
return seat;
|
|
75
|
+
}
|
|
76
|
+
seat = getNextSeat(seat, state.maxPlayers);
|
|
77
|
+
}
|
|
78
|
+
return null; // No actionable players
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Count players with specific status
|
|
82
|
+
*/
|
|
83
|
+
function countPlayersByStatus(state, status) {
|
|
84
|
+
return state.players.filter((p) => p?.status === status).length;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Get player by ID
|
|
88
|
+
*/
|
|
89
|
+
function getPlayerById(state, playerId) {
|
|
90
|
+
for (let seat = 0; seat < state.players.length; seat++) {
|
|
91
|
+
const player = state.players[seat];
|
|
92
|
+
if (player?.id === playerId) {
|
|
93
|
+
return { player, seat };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { GameState } from "@pokertools/types";
|
|
2
|
+
/**
|
|
3
|
+
* Calculate rake for a pot amount, respecting global per-hand rake cap
|
|
4
|
+
*
|
|
5
|
+
* @param state Current game state
|
|
6
|
+
* @param potAmount Amount in the pot to calculate rake from
|
|
7
|
+
* @param rakeTakenSoFar Total rake already taken this hand (for cap enforcement)
|
|
8
|
+
* @returns Object with rake amount and whether cap was hit
|
|
9
|
+
*/
|
|
10
|
+
export declare function calculateRake(state: GameState, potAmount: number, rakeTakenSoFar?: number): {
|
|
11
|
+
rake: number;
|
|
12
|
+
capReached: boolean;
|
|
13
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.calculateRake = calculateRake;
|
|
4
|
+
/**
|
|
5
|
+
* Calculate rake for a pot amount, respecting global per-hand rake cap
|
|
6
|
+
*
|
|
7
|
+
* @param state Current game state
|
|
8
|
+
* @param potAmount Amount in the pot to calculate rake from
|
|
9
|
+
* @param rakeTakenSoFar Total rake already taken this hand (for cap enforcement)
|
|
10
|
+
* @returns Object with rake amount and whether cap was hit
|
|
11
|
+
*/
|
|
12
|
+
function calculateRake(state, potAmount, rakeTakenSoFar = 0) {
|
|
13
|
+
// No rake for tournaments (identified by presence of blind structure)
|
|
14
|
+
if (state.config.blindStructure) {
|
|
15
|
+
return { rake: 0, capReached: false };
|
|
16
|
+
}
|
|
17
|
+
// No rake if not configured
|
|
18
|
+
const rakePercent = state.config.rakePercent ?? 0;
|
|
19
|
+
if (rakePercent === 0) {
|
|
20
|
+
return { rake: 0, capReached: false };
|
|
21
|
+
}
|
|
22
|
+
// "No Flop, No Drop" rule (standard in most cash games)
|
|
23
|
+
// If enabled (default true), no rake is taken if no flop was dealt
|
|
24
|
+
// This applies whether hand ends preflop OR players go all-in preflop without seeing flop
|
|
25
|
+
const noFlopNoDrop = state.config.noFlopNoDrop !== false; // Default true
|
|
26
|
+
if (noFlopNoDrop && state.board.length === 0) {
|
|
27
|
+
return { rake: 0, capReached: false };
|
|
28
|
+
}
|
|
29
|
+
// Calculate rake as percentage
|
|
30
|
+
let rake = Math.floor((potAmount * rakePercent) / 100);
|
|
31
|
+
// Apply GLOBAL rake cap (per-hand, not per-pot)
|
|
32
|
+
let capReached = false;
|
|
33
|
+
if (state.config.rakeCap !== undefined) {
|
|
34
|
+
const rakeAllowed = state.config.rakeCap - rakeTakenSoFar;
|
|
35
|
+
if (rakeAllowed <= 0) {
|
|
36
|
+
// Cap already reached
|
|
37
|
+
return { rake: 0, capReached: true };
|
|
38
|
+
}
|
|
39
|
+
if (rake > rakeAllowed) {
|
|
40
|
+
rake = rakeAllowed;
|
|
41
|
+
capReached = true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { rake, capReached };
|
|
45
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ActionRecord, GameState, Player, Pot, TableConfig, Winner } from "@pokertools/types";
|
|
2
|
+
/**
|
|
3
|
+
* Snapshot format (JSON-serializable)
|
|
4
|
+
*/
|
|
5
|
+
export interface Snapshot {
|
|
6
|
+
readonly config: TableConfig;
|
|
7
|
+
readonly players: Array<Player | null>;
|
|
8
|
+
readonly maxPlayers: number;
|
|
9
|
+
readonly handNumber: number;
|
|
10
|
+
readonly buttonSeat: number | null;
|
|
11
|
+
readonly deck: number[];
|
|
12
|
+
readonly board: string[];
|
|
13
|
+
readonly street: string;
|
|
14
|
+
readonly pots: Pot[];
|
|
15
|
+
readonly currentBets: Record<number, number>;
|
|
16
|
+
readonly minRaise: number;
|
|
17
|
+
readonly lastRaiseAmount: number;
|
|
18
|
+
readonly actionTo: number | null;
|
|
19
|
+
readonly lastAggressorSeat: number | null;
|
|
20
|
+
readonly activePlayers: number[];
|
|
21
|
+
readonly winners: Winner[] | null;
|
|
22
|
+
readonly rakeThisHand: number;
|
|
23
|
+
readonly smallBlind: number;
|
|
24
|
+
readonly bigBlind: number;
|
|
25
|
+
readonly ante: number;
|
|
26
|
+
readonly blindLevel: number;
|
|
27
|
+
readonly timeBanks: Record<number, number>;
|
|
28
|
+
readonly actionHistory: ActionRecord[];
|
|
29
|
+
readonly previousStates: Snapshot[];
|
|
30
|
+
readonly timestamp: number;
|
|
31
|
+
readonly handId: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Create JSON-serializable snapshot of game state
|
|
35
|
+
* Converts Maps to objects and truncates history
|
|
36
|
+
*/
|
|
37
|
+
export declare function createSnapshot(state: GameState): Snapshot;
|
|
38
|
+
/**
|
|
39
|
+
* Restore game state from snapshot
|
|
40
|
+
*/
|
|
41
|
+
export declare function restoreFromSnapshot(snapshot: Snapshot): GameState;
|
|
42
|
+
/**
|
|
43
|
+
* Serialize snapshot to JSON string
|
|
44
|
+
*/
|
|
45
|
+
export declare function serializeSnapshot(snapshot: Snapshot): string;
|
|
46
|
+
/**
|
|
47
|
+
* Deserialize JSON string to snapshot
|
|
48
|
+
*/
|
|
49
|
+
export declare function deserializeSnapshot(json: string): Snapshot;
|
|
50
|
+
/**
|
|
51
|
+
* Validate snapshot integrity
|
|
52
|
+
*/
|
|
53
|
+
export declare function validateSnapshot(snapshot: Snapshot): boolean;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createSnapshot = createSnapshot;
|
|
4
|
+
exports.restoreFromSnapshot = restoreFromSnapshot;
|
|
5
|
+
exports.serializeSnapshot = serializeSnapshot;
|
|
6
|
+
exports.deserializeSnapshot = deserializeSnapshot;
|
|
7
|
+
exports.validateSnapshot = validateSnapshot;
|
|
8
|
+
/**
|
|
9
|
+
* Create JSON-serializable snapshot of game state
|
|
10
|
+
* Converts Maps to objects and truncates history
|
|
11
|
+
*/
|
|
12
|
+
function createSnapshot(state) {
|
|
13
|
+
// Convert Maps to plain objects
|
|
14
|
+
const currentBets = {};
|
|
15
|
+
for (const [seat, bet] of state.currentBets.entries()) {
|
|
16
|
+
currentBets[seat] = bet;
|
|
17
|
+
}
|
|
18
|
+
const timeBanks = {};
|
|
19
|
+
for (const [seat, time] of state.timeBanks.entries()) {
|
|
20
|
+
timeBanks[seat] = time;
|
|
21
|
+
}
|
|
22
|
+
// Truncate previous states (keep last 10 only)
|
|
23
|
+
const previousStates = state.previousStates.slice(-10).map((s) => createSnapshot(s));
|
|
24
|
+
return {
|
|
25
|
+
config: state.config,
|
|
26
|
+
players: [...state.players],
|
|
27
|
+
maxPlayers: state.maxPlayers,
|
|
28
|
+
handNumber: state.handNumber,
|
|
29
|
+
buttonSeat: state.buttonSeat,
|
|
30
|
+
deck: Array.from(state.deck),
|
|
31
|
+
board: Array.from(state.board),
|
|
32
|
+
street: state.street,
|
|
33
|
+
pots: Array.from(state.pots),
|
|
34
|
+
currentBets,
|
|
35
|
+
minRaise: state.minRaise,
|
|
36
|
+
lastRaiseAmount: state.lastRaiseAmount,
|
|
37
|
+
actionTo: state.actionTo,
|
|
38
|
+
lastAggressorSeat: state.lastAggressorSeat,
|
|
39
|
+
activePlayers: Array.from(state.activePlayers),
|
|
40
|
+
winners: state.winners ? Array.from(state.winners) : null,
|
|
41
|
+
rakeThisHand: state.rakeThisHand,
|
|
42
|
+
smallBlind: state.smallBlind,
|
|
43
|
+
bigBlind: state.bigBlind,
|
|
44
|
+
ante: state.ante,
|
|
45
|
+
blindLevel: state.blindLevel,
|
|
46
|
+
timeBanks,
|
|
47
|
+
actionHistory: Array.from(state.actionHistory),
|
|
48
|
+
previousStates,
|
|
49
|
+
timestamp: state.timestamp,
|
|
50
|
+
handId: state.handId,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Restore game state from snapshot
|
|
55
|
+
*/
|
|
56
|
+
function restoreFromSnapshot(snapshot) {
|
|
57
|
+
// Convert plain objects back to Maps
|
|
58
|
+
const currentBets = new Map();
|
|
59
|
+
for (const [seatStr, bet] of Object.entries(snapshot.currentBets)) {
|
|
60
|
+
currentBets.set(parseInt(seatStr), bet);
|
|
61
|
+
}
|
|
62
|
+
const timeBanks = new Map();
|
|
63
|
+
for (const [seatStr, time] of Object.entries(snapshot.timeBanks)) {
|
|
64
|
+
timeBanks.set(parseInt(seatStr), time);
|
|
65
|
+
}
|
|
66
|
+
// Restore previous states recursively
|
|
67
|
+
const previousStates = snapshot.previousStates.map((s) => restoreFromSnapshot(s));
|
|
68
|
+
return {
|
|
69
|
+
...snapshot,
|
|
70
|
+
currentBets,
|
|
71
|
+
timeBanks,
|
|
72
|
+
previousStates,
|
|
73
|
+
rakeThisHand: snapshot.rakeThisHand || 0, // Add missing field with default
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Serialize snapshot to JSON string
|
|
78
|
+
*/
|
|
79
|
+
function serializeSnapshot(snapshot) {
|
|
80
|
+
return JSON.stringify(snapshot, null, 2);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Deserialize JSON string to snapshot
|
|
84
|
+
*/
|
|
85
|
+
function deserializeSnapshot(json) {
|
|
86
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
87
|
+
return JSON.parse(json);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Validate snapshot integrity
|
|
91
|
+
*/
|
|
92
|
+
function validateSnapshot(snapshot) {
|
|
93
|
+
try {
|
|
94
|
+
// Basic validation
|
|
95
|
+
if (!snapshot.handId)
|
|
96
|
+
return false;
|
|
97
|
+
if (snapshot.maxPlayers < 2 || snapshot.maxPlayers > 10)
|
|
98
|
+
return false;
|
|
99
|
+
if (snapshot.players.length !== snapshot.maxPlayers)
|
|
100
|
+
return false;
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation utilities for chip amounts and other game values
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Validate that a chip amount is a non-negative integer
|
|
6
|
+
* Prevents fractional chips and negative amounts
|
|
7
|
+
*
|
|
8
|
+
* @param amount The chip amount to validate
|
|
9
|
+
* @param context Description of what this amount represents (for error messages)
|
|
10
|
+
* @throws IllegalActionError if amount is invalid
|
|
11
|
+
*/
|
|
12
|
+
export declare function validateChipAmount(amount: number, context: string): void;
|
|
13
|
+
/**
|
|
14
|
+
* Validate that a timestamp is valid and not in the future
|
|
15
|
+
*
|
|
16
|
+
* @param timestamp The timestamp to validate
|
|
17
|
+
* @param previousTimestamp The previous action's timestamp (for monotonic check)
|
|
18
|
+
* @throws IllegalActionError if timestamp is invalid
|
|
19
|
+
*/
|
|
20
|
+
export declare function validateTimestamp(timestamp: number, previousTimestamp?: number): void;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Validation utilities for chip amounts and other game values
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.validateChipAmount = validateChipAmount;
|
|
7
|
+
exports.validateTimestamp = validateTimestamp;
|
|
8
|
+
const IllegalActionError_1 = require("../errors/IllegalActionError");
|
|
9
|
+
const ErrorCodes_1 = require("../errors/ErrorCodes");
|
|
10
|
+
const constants_1 = require("./constants");
|
|
11
|
+
/**
|
|
12
|
+
* Validate that a chip amount is a non-negative integer
|
|
13
|
+
* Prevents fractional chips and negative amounts
|
|
14
|
+
*
|
|
15
|
+
* @param amount The chip amount to validate
|
|
16
|
+
* @param context Description of what this amount represents (for error messages)
|
|
17
|
+
* @throws IllegalActionError if amount is invalid
|
|
18
|
+
*/
|
|
19
|
+
function validateChipAmount(amount, context) {
|
|
20
|
+
if (!Number.isFinite(amount)) {
|
|
21
|
+
throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.INVALID_AMOUNT, `${context}: ${amount} is not a valid number`, { amount, context });
|
|
22
|
+
}
|
|
23
|
+
if (!Number.isInteger(amount)) {
|
|
24
|
+
throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.INVALID_AMOUNT, `${context}: ${amount} must be an integer (fractional chips not allowed)`, { amount, context });
|
|
25
|
+
}
|
|
26
|
+
if (amount < 0) {
|
|
27
|
+
throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.INVALID_AMOUNT, `${context}: ${amount} cannot be negative`, { amount, context });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Validate that a timestamp is valid and not in the future
|
|
32
|
+
*
|
|
33
|
+
* @param timestamp The timestamp to validate
|
|
34
|
+
* @param previousTimestamp The previous action's timestamp (for monotonic check)
|
|
35
|
+
* @throws IllegalActionError if timestamp is invalid
|
|
36
|
+
*/
|
|
37
|
+
function validateTimestamp(timestamp, previousTimestamp) {
|
|
38
|
+
if (!Number.isFinite(timestamp) || timestamp < 0) {
|
|
39
|
+
throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.INVALID_TIMESTAMP, `Invalid timestamp: ${timestamp}`, {
|
|
40
|
+
timestamp,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
// Allow some clock drift tolerance for "future" timestamps
|
|
44
|
+
const now = Date.now() + constants_1.TIMESTAMP_FUTURE_TOLERANCE_MS;
|
|
45
|
+
if (timestamp > now) {
|
|
46
|
+
throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.INVALID_TIMESTAMP, `Timestamp ${timestamp} is in the future (current: ${Date.now()})`, { timestamp, currentTime: Date.now() });
|
|
47
|
+
}
|
|
48
|
+
// Ensure timestamps are monotonically increasing (or equal for same-time actions)
|
|
49
|
+
if (previousTimestamp !== undefined && timestamp < previousTimestamp) {
|
|
50
|
+
throw new IllegalActionError_1.IllegalActionError(ErrorCodes_1.ErrorCodes.INVALID_TIMESTAMP, `Timestamp ${timestamp} is before previous action timestamp ${previousTimestamp}`, { timestamp, previousTimestamp });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { GameState, PublicState } from "@pokertools/types";
|
|
2
|
+
/**
|
|
3
|
+
* Create public view of game state for a specific player
|
|
4
|
+
* Masks opponent hole cards and deck to prevent cheating
|
|
5
|
+
* Respects shownCards for granular card visibility
|
|
6
|
+
*
|
|
7
|
+
* @param state Full game state
|
|
8
|
+
* @param playerId Player requesting view (null = spectator)
|
|
9
|
+
* @returns Masked public state
|
|
10
|
+
*/
|
|
11
|
+
export declare function createPublicView(state: GameState, playerId?: string | null): PublicState;
|
|
12
|
+
/**
|
|
13
|
+
* Create spectator view (no player-specific information)
|
|
14
|
+
*/
|
|
15
|
+
export declare function createSpectatorView(state: GameState): PublicState;
|
|
16
|
+
/**
|
|
17
|
+
* Sanitize action history to remove sensitive information
|
|
18
|
+
* (Currently action history doesn't contain card info, but this is for future-proofing)
|
|
19
|
+
*/
|
|
20
|
+
export declare function sanitizeActionHistory(state: GameState, _viewerId: string | null): typeof state.actionHistory;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createPublicView = createPublicView;
|
|
4
|
+
exports.createSpectatorView = createSpectatorView;
|
|
5
|
+
exports.sanitizeActionHistory = sanitizeActionHistory;
|
|
6
|
+
/**
|
|
7
|
+
* Create public view of game state for a specific player
|
|
8
|
+
* Masks opponent hole cards and deck to prevent cheating
|
|
9
|
+
* Respects shownCards for granular card visibility
|
|
10
|
+
*
|
|
11
|
+
* @param state Full game state
|
|
12
|
+
* @param playerId Player requesting view (null = spectator)
|
|
13
|
+
* @returns Masked public state
|
|
14
|
+
*/
|
|
15
|
+
function createPublicView(state, playerId = null) {
|
|
16
|
+
const maskedPlayers = state.players.map((player, _seat) => {
|
|
17
|
+
if (!player)
|
|
18
|
+
return null;
|
|
19
|
+
// Determine which cards to show (if any)
|
|
20
|
+
const visibleHand = getVisibleHand(state, player, playerId);
|
|
21
|
+
return {
|
|
22
|
+
...player,
|
|
23
|
+
hand: visibleHand,
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
return {
|
|
27
|
+
...state,
|
|
28
|
+
deck: [], // Always hide deck
|
|
29
|
+
players: maskedPlayers,
|
|
30
|
+
viewingPlayerId: playerId,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Get visible cards for a player based on shownCards and viewer permissions
|
|
35
|
+
* Returns null (all hidden), full hand, or partial hand with positional context preserved
|
|
36
|
+
*
|
|
37
|
+
* Examples:
|
|
38
|
+
* - Full hand: ["As", "Kd"]
|
|
39
|
+
* - Mucked: null
|
|
40
|
+
* - Right card only (index 1): [null, "Kd"]
|
|
41
|
+
* - Left card only (index 0): ["As", null]
|
|
42
|
+
*/
|
|
43
|
+
function getVisibleHand(state, player, viewerId) {
|
|
44
|
+
// Always hide if player has no hand
|
|
45
|
+
if (!player.hand || player.hand.length === 0) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
// Show full hand to the player themselves
|
|
49
|
+
if (viewerId === player.id) {
|
|
50
|
+
return player.hand;
|
|
51
|
+
}
|
|
52
|
+
// For opponents/spectators, respect shownCards at showdown
|
|
53
|
+
if (state.street === "SHOWDOWN" /* Street.SHOWDOWN */) {
|
|
54
|
+
if (player.status === "ACTIVE" /* PlayerStatus.ACTIVE */ || player.status === "ALL_IN" /* PlayerStatus.ALL_IN */) {
|
|
55
|
+
// Check shownCards to determine visibility
|
|
56
|
+
if (player.shownCards === null) {
|
|
57
|
+
// Mucked - hide all cards
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
else if (player.shownCards && player.shownCards.length > 0) {
|
|
61
|
+
// Map over original hand, showing only specified indices
|
|
62
|
+
// Preserve positional context by using null for hidden cards
|
|
63
|
+
const visibleCards = player.hand.map((card, idx) => {
|
|
64
|
+
const isShown = player.shownCards.includes(idx);
|
|
65
|
+
return isShown ? card : null;
|
|
66
|
+
});
|
|
67
|
+
return visibleCards;
|
|
68
|
+
}
|
|
69
|
+
// If shownCards is empty array, hide all but preserve structure
|
|
70
|
+
return player.hand.map(() => null);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Hide in all other cases (pre-showdown)
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Create spectator view (no player-specific information)
|
|
78
|
+
*/
|
|
79
|
+
function createSpectatorView(state) {
|
|
80
|
+
return createPublicView(state, null);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Sanitize action history to remove sensitive information
|
|
84
|
+
* (Currently action history doesn't contain card info, but this is for future-proofing)
|
|
85
|
+
*/
|
|
86
|
+
function sanitizeActionHistory(state, _viewerId) {
|
|
87
|
+
// For now, action history is safe to show
|
|
88
|
+
// Future: might want to hide bet amounts in tournament play
|
|
89
|
+
return state.actionHistory;
|
|
90
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pokertools/engine",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Enterprise-grade Texas Hold'em poker engine",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"require": "./dist/index.js",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc",
|
|
22
|
+
"test": "NODE_OPTIONS='--no-warnings' jest"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"poker",
|
|
26
|
+
"texas-holdem",
|
|
27
|
+
"engine",
|
|
28
|
+
"game-engine",
|
|
29
|
+
"redux",
|
|
30
|
+
"immutable"
|
|
31
|
+
],
|
|
32
|
+
"author": "A.Aurelius",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/aaurelions/pokertools.git",
|
|
37
|
+
"directory": "packages/engine"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/aaurelions/pokertools/tree/main/packages/engine#readme",
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/aaurelions/pokertools/issues"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@pokertools/evaluator": "*",
|
|
48
|
+
"@pokertools/types": "*"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/jest": "^30.0.0",
|
|
52
|
+
"@types/node": "^24.10.1",
|
|
53
|
+
"fast-check": "^3.15.0",
|
|
54
|
+
"jest": "^30.2.0",
|
|
55
|
+
"ts-jest": "^29.4.5",
|
|
56
|
+
"typescript": "^5.9.3"
|
|
57
|
+
}
|
|
58
|
+
}
|