@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,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
// Export all errors
|
|
18
|
+
__exportStar(require("./PokerEngineError"), exports);
|
|
19
|
+
__exportStar(require("./CriticalStateError"), exports);
|
|
20
|
+
__exportStar(require("./IllegalActionError"), exports);
|
|
21
|
+
__exportStar(require("./ConfigError"), exports);
|
|
22
|
+
__exportStar(require("./ErrorCodes"), exports);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main hand history exporter
|
|
3
|
+
*/
|
|
4
|
+
import { GameState } from "@pokertools/types";
|
|
5
|
+
import { HandHistory, ExportOptions } from "./types";
|
|
6
|
+
/**
|
|
7
|
+
* Export hand history from game state
|
|
8
|
+
*
|
|
9
|
+
* @param state Final game state (after hand is complete)
|
|
10
|
+
* @param options Export options
|
|
11
|
+
* @returns Formatted hand history string
|
|
12
|
+
*/
|
|
13
|
+
export declare function exportHandHistory(state: GameState, options?: ExportOptions): string;
|
|
14
|
+
/**
|
|
15
|
+
* Build hand history object without formatting
|
|
16
|
+
*
|
|
17
|
+
* @param state Final game state
|
|
18
|
+
* @returns Structured hand history object
|
|
19
|
+
*/
|
|
20
|
+
export declare function getHandHistory(state: GameState): HandHistory;
|
|
21
|
+
/**
|
|
22
|
+
* Export multiple hands to a single file
|
|
23
|
+
*
|
|
24
|
+
* @param states Array of final game states
|
|
25
|
+
* @param options Export options
|
|
26
|
+
* @returns Formatted multi-hand history
|
|
27
|
+
*/
|
|
28
|
+
export declare function exportMultipleHands(states: GameState[], options?: ExportOptions): string;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Main hand history exporter
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.exportHandHistory = exportHandHistory;
|
|
7
|
+
exports.getHandHistory = getHandHistory;
|
|
8
|
+
exports.exportMultipleHands = exportMultipleHands;
|
|
9
|
+
const handHistoryBuilder_1 = require("./handHistoryBuilder");
|
|
10
|
+
const pokerstars_1 = require("./formats/pokerstars");
|
|
11
|
+
const json_1 = require("./formats/json");
|
|
12
|
+
/**
|
|
13
|
+
* Export hand history from game state
|
|
14
|
+
*
|
|
15
|
+
* @param state Final game state (after hand is complete)
|
|
16
|
+
* @param options Export options
|
|
17
|
+
* @returns Formatted hand history string
|
|
18
|
+
*/
|
|
19
|
+
function exportHandHistory(state, options = { format: "json" }) {
|
|
20
|
+
// Build structured history
|
|
21
|
+
const history = (0, handHistoryBuilder_1.buildHandHistory)(state);
|
|
22
|
+
// Export to requested format
|
|
23
|
+
switch (options.format) {
|
|
24
|
+
case "pokerstars":
|
|
25
|
+
return (0, pokerstars_1.exportToPokerStars)(history, options);
|
|
26
|
+
case "json":
|
|
27
|
+
return (0, json_1.exportToJSON)(history, options);
|
|
28
|
+
case "compact":
|
|
29
|
+
return (0, json_1.exportToJSON)(history, { ...options, format: "compact" });
|
|
30
|
+
default:
|
|
31
|
+
return (0, json_1.exportToJSON)(history, options);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Build hand history object without formatting
|
|
36
|
+
*
|
|
37
|
+
* @param state Final game state
|
|
38
|
+
* @returns Structured hand history object
|
|
39
|
+
*/
|
|
40
|
+
function getHandHistory(state) {
|
|
41
|
+
return (0, handHistoryBuilder_1.buildHandHistory)(state);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Export multiple hands to a single file
|
|
45
|
+
*
|
|
46
|
+
* @param states Array of final game states
|
|
47
|
+
* @param options Export options
|
|
48
|
+
* @returns Formatted multi-hand history
|
|
49
|
+
*/
|
|
50
|
+
function exportMultipleHands(states, options = { format: "json" }) {
|
|
51
|
+
if (options.format === "pokerstars") {
|
|
52
|
+
// PokerStars format: separate hands with blank lines
|
|
53
|
+
return states.map((state) => exportHandHistory(state, options)).join("\n\n\n");
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
// JSON format: array of hands
|
|
57
|
+
const histories = states.map((state) => (0, handHistoryBuilder_1.buildHandHistory)(state));
|
|
58
|
+
return JSON.stringify(histories, null, options.format === "compact" ? 0 : 2);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON hand history format exporter
|
|
3
|
+
*
|
|
4
|
+
* Machine-readable format for analysis and storage
|
|
5
|
+
*/
|
|
6
|
+
import { HandHistory, ExportOptions } from "../types";
|
|
7
|
+
/**
|
|
8
|
+
* Export hand history to JSON format
|
|
9
|
+
*/
|
|
10
|
+
export declare function exportToJSON(history: HandHistory, options?: ExportOptions): string;
|
|
11
|
+
/**
|
|
12
|
+
* Parse hand history from JSON
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseFromJSON(json: string): HandHistory;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* JSON hand history format exporter
|
|
4
|
+
*
|
|
5
|
+
* Machine-readable format for analysis and storage
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.exportToJSON = exportToJSON;
|
|
9
|
+
exports.parseFromJSON = parseFromJSON;
|
|
10
|
+
/**
|
|
11
|
+
* Export hand history to JSON format
|
|
12
|
+
*/
|
|
13
|
+
function exportToJSON(history, options = { format: "json" }) {
|
|
14
|
+
// Create sanitized version based on options
|
|
15
|
+
const sanitized = sanitizeHandHistory(history, options);
|
|
16
|
+
if (options.format === "compact") {
|
|
17
|
+
return JSON.stringify(sanitized);
|
|
18
|
+
}
|
|
19
|
+
return JSON.stringify(sanitized, null, 2);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Sanitize hand history based on export options
|
|
23
|
+
*/
|
|
24
|
+
function sanitizeHandHistory(history, options) {
|
|
25
|
+
if (options.includeHoleCards) {
|
|
26
|
+
return history;
|
|
27
|
+
}
|
|
28
|
+
// Remove hole cards for privacy
|
|
29
|
+
return {
|
|
30
|
+
...history,
|
|
31
|
+
players: history.players.map((player) => ({
|
|
32
|
+
...player,
|
|
33
|
+
cards: undefined,
|
|
34
|
+
})),
|
|
35
|
+
winners: history.winners.map((winner) => ({
|
|
36
|
+
...winner,
|
|
37
|
+
hand: winner.handRank ? winner.hand : undefined, // Keep if shown at showdown
|
|
38
|
+
})),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Parse hand history from JSON
|
|
43
|
+
*/
|
|
44
|
+
function parseFromJSON(json) {
|
|
45
|
+
return JSON.parse(json);
|
|
46
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PokerStars hand history format exporter
|
|
3
|
+
*
|
|
4
|
+
* Format specification based on PokerStars hand history text format
|
|
5
|
+
*/
|
|
6
|
+
import { HandHistory, ExportOptions } from "../types";
|
|
7
|
+
/**
|
|
8
|
+
* Export hand history to PokerStars format
|
|
9
|
+
*/
|
|
10
|
+
export declare function exportToPokerStars(history: HandHistory, options?: ExportOptions): string;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* PokerStars hand history format exporter
|
|
4
|
+
*
|
|
5
|
+
* Format specification based on PokerStars hand history text format
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.exportToPokerStars = exportToPokerStars;
|
|
9
|
+
/**
|
|
10
|
+
* Export hand history to PokerStars format
|
|
11
|
+
*/
|
|
12
|
+
function exportToPokerStars(history, options = { format: "pokerstars" }) {
|
|
13
|
+
const lines = [];
|
|
14
|
+
// Header line
|
|
15
|
+
lines.push(buildHeader(history));
|
|
16
|
+
lines.push(buildTableInfo(history));
|
|
17
|
+
// Button and seats
|
|
18
|
+
lines.push(`Button: seat #${history.buttonSeat + 1}`);
|
|
19
|
+
lines.push("");
|
|
20
|
+
// Player stacks
|
|
21
|
+
for (const player of history.players) {
|
|
22
|
+
lines.push(`Seat ${player.seat + 1}: ${player.name} ($${player.startingStack.toFixed(2)} in chips)`);
|
|
23
|
+
}
|
|
24
|
+
lines.push("");
|
|
25
|
+
// Blinds and antes
|
|
26
|
+
lines.push(buildBlindsLine(history));
|
|
27
|
+
if (history.stakes.ante > 0) {
|
|
28
|
+
lines.push(buildAntesLine(history));
|
|
29
|
+
}
|
|
30
|
+
// Hole cards (dealt to)
|
|
31
|
+
lines.push(buildHoleCardsLine(history, options));
|
|
32
|
+
// Each street
|
|
33
|
+
for (const street of history.streets) {
|
|
34
|
+
lines.push("");
|
|
35
|
+
lines.push(buildStreetHeader(street));
|
|
36
|
+
for (const action of street.actions) {
|
|
37
|
+
lines.push(buildActionLine(action, history));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Summary
|
|
41
|
+
lines.push("");
|
|
42
|
+
lines.push(buildSummary(history));
|
|
43
|
+
return lines.join("\n");
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Build header line
|
|
47
|
+
*/
|
|
48
|
+
function buildHeader(history) {
|
|
49
|
+
const date = new Date(history.timestamp);
|
|
50
|
+
const dateStr = formatPokerStarsDate(date);
|
|
51
|
+
return `PokerStars Hand #${history.handId}: Hold'em No Limit ($${history.stakes.smallBlind}/$${history.stakes.bigBlind}) - ${dateStr}`;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Build table info line
|
|
55
|
+
*/
|
|
56
|
+
function buildTableInfo(history) {
|
|
57
|
+
return `Table '${history.tableName}' ${history.maxPlayers}-max`;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Build blinds posting line
|
|
61
|
+
*/
|
|
62
|
+
function buildBlindsLine(history) {
|
|
63
|
+
const lines = [];
|
|
64
|
+
// Find SB and BB seats (button+1 and button+2)
|
|
65
|
+
const sbSeat = (history.buttonSeat + 1) % history.maxPlayers;
|
|
66
|
+
const bbSeat = (history.buttonSeat + 2) % history.maxPlayers;
|
|
67
|
+
const sbPlayer = history.players.find((p) => p.seat === sbSeat);
|
|
68
|
+
const bbPlayer = history.players.find((p) => p.seat === bbSeat);
|
|
69
|
+
if (sbPlayer) {
|
|
70
|
+
lines.push(`${sbPlayer.name}: posts small blind $${history.stakes.smallBlind.toFixed(2)}`);
|
|
71
|
+
}
|
|
72
|
+
if (bbPlayer) {
|
|
73
|
+
lines.push(`${bbPlayer.name}: posts big blind $${history.stakes.bigBlind.toFixed(2)}`);
|
|
74
|
+
}
|
|
75
|
+
return lines.join("\n");
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Build antes line
|
|
79
|
+
*/
|
|
80
|
+
function buildAntesLine(history) {
|
|
81
|
+
const lines = [];
|
|
82
|
+
for (const player of history.players) {
|
|
83
|
+
lines.push(`${player.name}: posts ante $${history.stakes.ante.toFixed(2)}`);
|
|
84
|
+
}
|
|
85
|
+
return lines.join("\n");
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Build hole cards line
|
|
89
|
+
*/
|
|
90
|
+
function buildHoleCardsLine(history, options) {
|
|
91
|
+
// In PokerStars format, only show hero's cards
|
|
92
|
+
// For analysis mode, show all cards
|
|
93
|
+
if (options.includeHoleCards) {
|
|
94
|
+
const lines = ["*** HOLE CARDS ***"];
|
|
95
|
+
for (const player of history.players) {
|
|
96
|
+
if (player.cards && player.cards.length > 0) {
|
|
97
|
+
lines.push(`Dealt to ${player.name} [${player.cards.join(" ")}]`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return lines.join("\n");
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
return "*** HOLE CARDS ***";
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Build street header
|
|
108
|
+
*/
|
|
109
|
+
function buildStreetHeader(street) {
|
|
110
|
+
switch (street.street) {
|
|
111
|
+
case "FLOP" /* Street.FLOP */:
|
|
112
|
+
return `*** FLOP *** [${street.board.join(" ")}]`;
|
|
113
|
+
case "TURN" /* Street.TURN */:
|
|
114
|
+
return `*** TURN *** [${street.board.slice(0, 3).join(" ")}] [${street.board[3]}]`;
|
|
115
|
+
case "RIVER" /* Street.RIVER */:
|
|
116
|
+
return `*** RIVER *** [${street.board.slice(0, 4).join(" ")}] [${street.board[4]}]`;
|
|
117
|
+
case "SHOWDOWN" /* Street.SHOWDOWN */:
|
|
118
|
+
return "*** SHOW DOWN ***";
|
|
119
|
+
default:
|
|
120
|
+
return "";
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Build action line
|
|
125
|
+
*/
|
|
126
|
+
function buildActionLine(action, history) {
|
|
127
|
+
const player = history.players.find((p) => p.seat === action.seat);
|
|
128
|
+
const name = player?.name ?? `Player ${action.seat + 1}`;
|
|
129
|
+
switch (action.action.type) {
|
|
130
|
+
case "FOLD" /* ActionType.FOLD */:
|
|
131
|
+
return `${name}: folds`;
|
|
132
|
+
case "CHECK" /* ActionType.CHECK */:
|
|
133
|
+
return `${name}: checks`;
|
|
134
|
+
case "CALL" /* ActionType.CALL */:
|
|
135
|
+
const callAmount = action.amount ?? 0;
|
|
136
|
+
if (action.isAllIn) {
|
|
137
|
+
return `${name}: calls $${callAmount.toFixed(2)} and is all-in`;
|
|
138
|
+
}
|
|
139
|
+
return `${name}: calls $${callAmount.toFixed(2)}`;
|
|
140
|
+
case "BET" /* ActionType.BET */:
|
|
141
|
+
const betAmount = action.amount ?? 0;
|
|
142
|
+
if (action.isAllIn) {
|
|
143
|
+
return `${name}: bets $${betAmount.toFixed(2)} and is all-in`;
|
|
144
|
+
}
|
|
145
|
+
return `${name}: bets $${betAmount.toFixed(2)}`;
|
|
146
|
+
case "RAISE" /* ActionType.RAISE */:
|
|
147
|
+
const raiseAmount = action.amount ?? 0;
|
|
148
|
+
if (action.isAllIn) {
|
|
149
|
+
return `${name}: raises to $${raiseAmount.toFixed(2)} and is all-in`;
|
|
150
|
+
}
|
|
151
|
+
return `${name}: raises to $${raiseAmount.toFixed(2)}`;
|
|
152
|
+
default:
|
|
153
|
+
return `${name}: ${action.action.type.toLowerCase()}`;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Build summary section
|
|
158
|
+
*/
|
|
159
|
+
function buildSummary(history) {
|
|
160
|
+
const lines = ["*** SUMMARY ***"];
|
|
161
|
+
lines.push(`Total pot $${history.totalPot.toFixed(2)}`);
|
|
162
|
+
lines.push(`Button: seat #${history.buttonSeat + 1}`);
|
|
163
|
+
// Board
|
|
164
|
+
const lastStreet = history.streets[history.streets.length - 1];
|
|
165
|
+
if (lastStreet && lastStreet.board.length > 0) {
|
|
166
|
+
lines.push(`Board [${lastStreet.board.join(" ")}]`);
|
|
167
|
+
}
|
|
168
|
+
// Winners
|
|
169
|
+
for (const winner of history.winners) {
|
|
170
|
+
const handInfo = winner.handRank ? ` with ${winner.handRank}` : "";
|
|
171
|
+
lines.push(`Seat ${winner.seat + 1}: ${winner.playerName} won ($${winner.amount.toFixed(2)})${handInfo}`);
|
|
172
|
+
}
|
|
173
|
+
return lines.join("\n");
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Format date in PokerStars format
|
|
177
|
+
*/
|
|
178
|
+
function formatPokerStarsDate(date) {
|
|
179
|
+
// Use UTC to avoid timezone misrepresentation
|
|
180
|
+
// Server's local time zone should not affect hand history exports
|
|
181
|
+
const year = date.getUTCFullYear();
|
|
182
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
183
|
+
const day = String(date.getUTCDate()).padStart(2, "0");
|
|
184
|
+
const hours = String(date.getUTCHours()).padStart(2, "0");
|
|
185
|
+
const minutes = String(date.getUTCMinutes()).padStart(2, "0");
|
|
186
|
+
const seconds = String(date.getUTCSeconds()).padStart(2, "0");
|
|
187
|
+
return `${year}/${month}/${day} ${hours}:${minutes}:${seconds} UTC`;
|
|
188
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build hand history from game state and action history
|
|
3
|
+
*/
|
|
4
|
+
import { GameState } from "@pokertools/types";
|
|
5
|
+
import { HandHistory } from "./types";
|
|
6
|
+
/**
|
|
7
|
+
* Build complete hand history from final game state
|
|
8
|
+
* Call this after a hand is complete (winners determined)
|
|
9
|
+
*/
|
|
10
|
+
export declare function buildHandHistory(finalState: GameState, tableName?: string, gameType?: "Cash" | "Tournament"): HandHistory;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Build hand history from game state and action history
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.buildHandHistory = buildHandHistory;
|
|
7
|
+
/**
|
|
8
|
+
* Build complete hand history from final game state
|
|
9
|
+
* Call this after a hand is complete (winners determined)
|
|
10
|
+
*/
|
|
11
|
+
function buildHandHistory(finalState, tableName = "Table 1", gameType = "Cash") {
|
|
12
|
+
const players = buildPlayerHistory(finalState);
|
|
13
|
+
const streets = buildStreetHistory(finalState);
|
|
14
|
+
const winners = buildWinnerHistory(finalState);
|
|
15
|
+
const totalPot = winners.reduce((sum, w) => sum + w.amount, 0);
|
|
16
|
+
return {
|
|
17
|
+
handId: finalState.handId,
|
|
18
|
+
timestamp: finalState.timestamp,
|
|
19
|
+
tableName,
|
|
20
|
+
gameType,
|
|
21
|
+
stakes: {
|
|
22
|
+
smallBlind: finalState.smallBlind,
|
|
23
|
+
bigBlind: finalState.bigBlind,
|
|
24
|
+
ante: finalState.ante,
|
|
25
|
+
},
|
|
26
|
+
maxPlayers: finalState.maxPlayers,
|
|
27
|
+
buttonSeat: finalState.buttonSeat ?? 0,
|
|
28
|
+
players,
|
|
29
|
+
streets,
|
|
30
|
+
winners,
|
|
31
|
+
totalPot,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Build player history records
|
|
36
|
+
*/
|
|
37
|
+
function buildPlayerHistory(state) {
|
|
38
|
+
const players = [];
|
|
39
|
+
for (const player of state.players) {
|
|
40
|
+
if (!player)
|
|
41
|
+
continue;
|
|
42
|
+
// Calculate starting stack (current + invested)
|
|
43
|
+
const startingStack = player.stack + player.totalInvestedThisHand;
|
|
44
|
+
players.push({
|
|
45
|
+
seat: player.seat,
|
|
46
|
+
name: player.name,
|
|
47
|
+
startingStack,
|
|
48
|
+
endingStack: player.stack,
|
|
49
|
+
cards: player.hand ? [...player.hand] : undefined,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return players;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Build street-by-street history
|
|
56
|
+
*/
|
|
57
|
+
function buildStreetHistory(state) {
|
|
58
|
+
const streets = [];
|
|
59
|
+
const actionsByStreet = groupActionsByStreet(state);
|
|
60
|
+
// Build history for each street that had actions
|
|
61
|
+
for (const [street, actions] of actionsByStreet) {
|
|
62
|
+
const board = getBoardForStreet(state, street);
|
|
63
|
+
const pot = calculatePotAtStreet(actions);
|
|
64
|
+
streets.push({
|
|
65
|
+
street,
|
|
66
|
+
board,
|
|
67
|
+
actions,
|
|
68
|
+
pot,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return streets;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Group actions by street
|
|
75
|
+
*/
|
|
76
|
+
function groupActionsByStreet(state) {
|
|
77
|
+
const grouped = new Map();
|
|
78
|
+
// Initialize with all streets
|
|
79
|
+
grouped.set("PREFLOP" /* Street.PREFLOP */, []);
|
|
80
|
+
grouped.set("FLOP" /* Street.FLOP */, []);
|
|
81
|
+
grouped.set("TURN" /* Street.TURN */, []);
|
|
82
|
+
grouped.set("RIVER" /* Street.RIVER */, []);
|
|
83
|
+
grouped.set("SHOWDOWN" /* Street.SHOWDOWN */, []);
|
|
84
|
+
// Group action history by street
|
|
85
|
+
for (const record of state.actionHistory) {
|
|
86
|
+
if (record.seat === null)
|
|
87
|
+
continue; // Skip table-level actions
|
|
88
|
+
const street = record.street ?? state.street;
|
|
89
|
+
const existing = grouped.get(street) ?? [];
|
|
90
|
+
const actionRecord = {
|
|
91
|
+
seat: record.seat,
|
|
92
|
+
playerName: state.players[record.seat]?.name ?? `Player ${record.seat}`,
|
|
93
|
+
action: record.action,
|
|
94
|
+
amount: getActionAmount(record.action),
|
|
95
|
+
isAllIn: isAllInAction(record.action, state, record.seat),
|
|
96
|
+
timestamp: record.action.timestamp ?? 0,
|
|
97
|
+
};
|
|
98
|
+
existing.push(actionRecord);
|
|
99
|
+
grouped.set(street, existing);
|
|
100
|
+
}
|
|
101
|
+
// Remove empty streets
|
|
102
|
+
for (const [street, actions] of grouped) {
|
|
103
|
+
if (actions.length === 0) {
|
|
104
|
+
grouped.delete(street);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return grouped;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Get board cards for a given street
|
|
111
|
+
*/
|
|
112
|
+
function getBoardForStreet(state, street) {
|
|
113
|
+
switch (street) {
|
|
114
|
+
case "PREFLOP" /* Street.PREFLOP */:
|
|
115
|
+
return [];
|
|
116
|
+
case "FLOP" /* Street.FLOP */:
|
|
117
|
+
return state.board.slice(0, 3);
|
|
118
|
+
case "TURN" /* Street.TURN */:
|
|
119
|
+
return state.board.slice(0, 4);
|
|
120
|
+
case "RIVER" /* Street.RIVER */:
|
|
121
|
+
case "SHOWDOWN" /* Street.SHOWDOWN */:
|
|
122
|
+
return state.board.slice(0, 5);
|
|
123
|
+
default:
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Calculate pot size at end of street actions
|
|
129
|
+
*/
|
|
130
|
+
function calculatePotAtStreet(actions) {
|
|
131
|
+
let pot = 0;
|
|
132
|
+
for (const action of actions) {
|
|
133
|
+
if (action.amount) {
|
|
134
|
+
pot += action.amount;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return pot;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Extract amount from action
|
|
141
|
+
*/
|
|
142
|
+
function getActionAmount(action) {
|
|
143
|
+
switch (action.type) {
|
|
144
|
+
case "BET" /* ActionType.BET */:
|
|
145
|
+
case "RAISE" /* ActionType.RAISE */:
|
|
146
|
+
return action.amount;
|
|
147
|
+
case "CALL" /* ActionType.CALL */:
|
|
148
|
+
// Call amount would need to be tracked in action history
|
|
149
|
+
return undefined;
|
|
150
|
+
default:
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Check if action resulted in all-in
|
|
156
|
+
*/
|
|
157
|
+
function isAllInAction(action, state, seat) {
|
|
158
|
+
const player = state.players[seat];
|
|
159
|
+
if (!player)
|
|
160
|
+
return false;
|
|
161
|
+
return player.stack === 0 && player.totalInvestedThisHand > 0;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Build winner records
|
|
165
|
+
*/
|
|
166
|
+
function buildWinnerHistory(state) {
|
|
167
|
+
if (!state.winners)
|
|
168
|
+
return [];
|
|
169
|
+
return state.winners.map((winner) => {
|
|
170
|
+
const player = state.players[winner.seat];
|
|
171
|
+
return {
|
|
172
|
+
seat: winner.seat,
|
|
173
|
+
playerName: player?.name ?? `Player ${winner.seat}`,
|
|
174
|
+
amount: winner.amount,
|
|
175
|
+
hand: winner.hand ?? undefined,
|
|
176
|
+
handRank: winner.handRank ?? undefined,
|
|
177
|
+
};
|
|
178
|
+
});
|
|
179
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand history types
|
|
3
|
+
*/
|
|
4
|
+
import { Street, Action } from "@pokertools/types";
|
|
5
|
+
/**
|
|
6
|
+
* Hand history record
|
|
7
|
+
* Contains complete information about a single hand
|
|
8
|
+
*/
|
|
9
|
+
export interface HandHistory {
|
|
10
|
+
readonly handId: string;
|
|
11
|
+
readonly timestamp: number;
|
|
12
|
+
readonly tableName: string;
|
|
13
|
+
readonly gameType: "Cash" | "Tournament";
|
|
14
|
+
readonly stakes: {
|
|
15
|
+
readonly smallBlind: number;
|
|
16
|
+
readonly bigBlind: number;
|
|
17
|
+
readonly ante: number;
|
|
18
|
+
};
|
|
19
|
+
readonly maxPlayers: number;
|
|
20
|
+
readonly buttonSeat: number;
|
|
21
|
+
readonly players: readonly HandHistoryPlayer[];
|
|
22
|
+
readonly streets: readonly StreetHistory[];
|
|
23
|
+
readonly winners: readonly WinnerRecord[];
|
|
24
|
+
readonly totalPot: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Player information in hand history
|
|
28
|
+
*/
|
|
29
|
+
export interface HandHistoryPlayer {
|
|
30
|
+
readonly seat: number;
|
|
31
|
+
readonly name: string;
|
|
32
|
+
readonly startingStack: number;
|
|
33
|
+
readonly endingStack: number;
|
|
34
|
+
readonly cards?: readonly string[];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* History for a single street
|
|
38
|
+
*/
|
|
39
|
+
export interface StreetHistory {
|
|
40
|
+
readonly street: Street;
|
|
41
|
+
readonly board: readonly string[];
|
|
42
|
+
readonly actions: readonly ActionRecord[];
|
|
43
|
+
readonly pot: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Action record in history
|
|
47
|
+
*/
|
|
48
|
+
export interface ActionRecord {
|
|
49
|
+
readonly seat: number;
|
|
50
|
+
readonly playerName: string;
|
|
51
|
+
readonly action: Action;
|
|
52
|
+
readonly amount?: number;
|
|
53
|
+
readonly isAllIn?: boolean;
|
|
54
|
+
readonly timestamp: number;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Winner record
|
|
58
|
+
*/
|
|
59
|
+
export interface WinnerRecord {
|
|
60
|
+
readonly seat: number;
|
|
61
|
+
readonly playerName: string;
|
|
62
|
+
readonly amount: number;
|
|
63
|
+
readonly hand?: readonly string[];
|
|
64
|
+
readonly handRank?: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Hand history format options
|
|
68
|
+
*/
|
|
69
|
+
export interface ExportOptions {
|
|
70
|
+
readonly format: "pokerstars" | "json" | "compact";
|
|
71
|
+
readonly includeHoleCards?: boolean;
|
|
72
|
+
readonly timezone?: string;
|
|
73
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { PokerEngine } from "./engine/PokerEngine";
|
|
2
|
+
export * from "@pokertools/types";
|
|
3
|
+
export * from "./errors";
|
|
4
|
+
export { createSnapshot, restoreFromSnapshot, Snapshot } from "./utils/serialization";
|
|
5
|
+
export { createPublicView } from "./utils/viewMasking";
|
|
6
|
+
export { calculateTotalChips, auditChipConservation } from "./utils/invariants";
|
|
7
|
+
export { exportHandHistory, getHandHistory, exportMultipleHands } from "./history/exporter";
|
|
8
|
+
export type { HandHistory, HandHistoryPlayer, StreetHistory, ExportOptions } from "./history/types";
|