@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.
Files changed (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +607 -0
  3. package/dist/actions/betting.d.ts +21 -0
  4. package/dist/actions/betting.js +410 -0
  5. package/dist/actions/dealing.d.ts +9 -0
  6. package/dist/actions/dealing.js +206 -0
  7. package/dist/actions/management.d.ts +9 -0
  8. package/dist/actions/management.js +58 -0
  9. package/dist/actions/showdownActions.d.ts +9 -0
  10. package/dist/actions/showdownActions.js +119 -0
  11. package/dist/actions/special.d.ts +14 -0
  12. package/dist/actions/special.js +98 -0
  13. package/dist/actions/streetProgression.d.ts +13 -0
  14. package/dist/actions/streetProgression.js +157 -0
  15. package/dist/actions/tournament.d.ts +5 -0
  16. package/dist/actions/tournament.js +38 -0
  17. package/dist/actions/validation.d.ts +6 -0
  18. package/dist/actions/validation.js +182 -0
  19. package/dist/engine/PokerEngine.d.ts +92 -0
  20. package/dist/engine/PokerEngine.js +246 -0
  21. package/dist/engine/gameReducer.d.ts +10 -0
  22. package/dist/engine/gameReducer.js +135 -0
  23. package/dist/errors/ConfigError.d.ts +8 -0
  24. package/dist/errors/ConfigError.js +15 -0
  25. package/dist/errors/CriticalStateError.d.ts +8 -0
  26. package/dist/errors/CriticalStateError.js +15 -0
  27. package/dist/errors/ErrorCodes.d.ts +38 -0
  28. package/dist/errors/ErrorCodes.js +46 -0
  29. package/dist/errors/IllegalActionError.d.ts +9 -0
  30. package/dist/errors/IllegalActionError.js +15 -0
  31. package/dist/errors/PokerEngineError.d.ts +8 -0
  32. package/dist/errors/PokerEngineError.js +19 -0
  33. package/dist/errors/index.d.ts +5 -0
  34. package/dist/errors/index.js +22 -0
  35. package/dist/history/exporter.d.ts +28 -0
  36. package/dist/history/exporter.js +60 -0
  37. package/dist/history/formats/json.d.ts +14 -0
  38. package/dist/history/formats/json.js +46 -0
  39. package/dist/history/formats/pokerstars.d.ts +10 -0
  40. package/dist/history/formats/pokerstars.js +188 -0
  41. package/dist/history/handHistoryBuilder.d.ts +10 -0
  42. package/dist/history/handHistoryBuilder.js +179 -0
  43. package/dist/history/types.d.ts +73 -0
  44. package/dist/history/types.js +5 -0
  45. package/dist/index.d.ts +8 -0
  46. package/dist/index.js +38 -0
  47. package/dist/rules/actionOrder.d.ts +14 -0
  48. package/dist/rules/actionOrder.js +211 -0
  49. package/dist/rules/blinds.d.ts +24 -0
  50. package/dist/rules/blinds.js +64 -0
  51. package/dist/rules/headsUp.d.ts +15 -0
  52. package/dist/rules/headsUp.js +44 -0
  53. package/dist/rules/showdown.d.ts +9 -0
  54. package/dist/rules/showdown.js +164 -0
  55. package/dist/rules/sidePots.d.ts +32 -0
  56. package/dist/rules/sidePots.js +173 -0
  57. package/dist/tsconfig.tsbuildinfo +1 -0
  58. package/dist/utils/cardUtils.d.ts +12 -0
  59. package/dist/utils/cardUtils.js +30 -0
  60. package/dist/utils/constants.d.ts +38 -0
  61. package/dist/utils/constants.js +41 -0
  62. package/dist/utils/deck.d.ts +46 -0
  63. package/dist/utils/deck.js +126 -0
  64. package/dist/utils/invariants.d.ts +39 -0
  65. package/dist/utils/invariants.js +163 -0
  66. package/dist/utils/positioning.d.ts +36 -0
  67. package/dist/utils/positioning.js +97 -0
  68. package/dist/utils/rake.d.ts +13 -0
  69. package/dist/utils/rake.js +45 -0
  70. package/dist/utils/serialization.d.ts +53 -0
  71. package/dist/utils/serialization.js +106 -0
  72. package/dist/utils/validation.d.ts +20 -0
  73. package/dist/utils/validation.js +52 -0
  74. package/dist/utils/viewMasking.d.ts +20 -0
  75. package/dist/utils/viewMasking.js +90 -0
  76. 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
+ }