@idealic/poker-engine
Version:
Professional poker game engine and hand evaluator with built-in iterator utilities
198 lines • 7.89 kB
JavaScript
export function cloneState(state) {
return {
...state,
players: state.players.map(p => ({ ...p, cards: [...(p.cards || [])] })),
board: [...state.board],
};
}
/**
* Creates a sanitized copy of the table state where cards are hidden for players who haven't shown them.
* This is used when sharing table state with players to maintain game integrity.
* Cards are only visible if:
* 1. The player has shown their cards (hasShownCards is true)
* 2. The hand is complete and the player hasn't folded
* @param table The table state to sanitize
* @param playerPosition Optional position of the player viewing the table. If provided, their cards will be visible.
*/
export function sanitizeTable(game, playerPosition) {
const sanitized = cloneState(game);
sanitized.players = sanitized.players.map((player, index) => {
// Hide cards unless:
// 1. This is the viewing player's position
// 2. Player has shown cards
// 3. Hand is complete and player hasn't folded
const shouldShowCards = index === playerPosition ||
player.hasShownCards ||
(game.isHandComplete && !player.hasFolded);
return {
...player,
cards: shouldShowCards ? player.cards : player.cards?.map(() => '??'),
};
});
return sanitized;
}
/** @todo: implement this */
export function formatAction(action) {
return String(action);
}
/**
* Applies an action to a game state.
* @param game - The game state to apply the action to
* @param action - The action to apply
* @returns The updated game state
*/
export function applyGameAction(hand, action) {
return {
...hand,
actions: [...hand.actions, action],
};
}
/**
* Intelligently merges hole card actions, combining card information
* @param action1 - First action (may have ???? for hidden cards)
* @param action2 - Second action (may have actual cards)
* @returns Merged action with most complete card information
*/
function mergeHoleCardActions(action1, action2) {
const match1 = action1.match(/^d\s+dh\s+p(\d+)\s+(.+)$/);
const match2 = action2.match(/^d\s+dh\s+p(\d+)\s+(.+)$/);
if (!match1 || !match2 || match1[1] !== match2[1]) {
// Not hole card actions or different players - return the second action
return action2;
}
const cards1 = match1[2].split('#')[0].trim();
const cards2 = match2[2].split('#')[0].trim();
// If one has ???? and the other has actual cards, use the actual cards
if (cards1 === '????') {
return action2;
}
if (cards2 === '????') {
return action1;
}
// Both have actual cards - use the second one (newer information)
return action2;
}
/**
* Tries to merge two game states, assuming they are from the same table, have common players and are in the same hand.
* @param oldGame - The first game state
* @param newGame - The second game state
* @returns The merged game state
*/
export function mergeGames(oldHand, newHand) {
// If games are from different hands – keep the most recent one based on hand number
if (oldHand.hand !== newHand.hand) {
return (oldHand.hand ?? 0) > (newHand.hand ?? 0) ? oldHand : newHand;
}
const oldActions = oldHand.actions;
const newActions = newHand.actions;
// Create a map to track the best version of each action
const actionMap = new Map();
// Helper function to generate consistent action keys with position context
const getActionKey = (action, actionIndex) => {
const baseAction = action.split('#')[0].trim(); // Remove timestamp/comments
// For hole card actions, use player-based key (d dh pX) to enable merging
const holeCardMatch = baseAction.match(/^d\s+dh\s+p(\d+)\s+/);
if (holeCardMatch) {
return `d dh p${holeCardMatch[1]}`;
}
// For other actions, include position to differentiate identical actions
return `${actionIndex}:${baseAction}`;
};
// Process old actions first
for (let i = 0; i < oldActions.length; i++) {
const action = oldActions[i];
const actionKey = getActionKey(action, i);
actionMap.set(actionKey, action);
}
// Process new actions, intelligently merging hole card actions
for (let i = 0; i < newActions.length; i++) {
const action = newActions[i];
const actionKey = getActionKey(action, i);
const existingAction = actionMap.get(actionKey);
if (existingAction && actionKey.startsWith('d dh p')) {
// This is a hole card action - merge intelligently
const mergedAction = mergeHoleCardActions(existingAction, action);
actionMap.set(actionKey, mergedAction);
}
else if (existingAction) {
// For non-hole-card actions that exist, keep the existing version
// (already in the map, no action needed)
}
else {
// For new actions, add them
actionMap.set(actionKey, action);
}
}
// Convert back to ordered list, always starting with old actions first
const mergedActions = [];
const usedKeys = new Set();
// First, add all old actions in order
for (let i = 0; i < oldActions.length; i++) {
const action = oldActions[i];
const actionKey = getActionKey(action, i);
if (!usedKeys.has(actionKey)) {
mergedActions.push(actionMap.get(actionKey) || action);
usedKeys.add(actionKey);
}
}
// Then, add new actions that aren't already included
for (let i = 0; i < newActions.length; i++) {
const action = newActions[i];
const actionKey = getActionKey(action, i);
if (!usedKeys.has(actionKey)) {
mergedActions.push(actionMap.get(actionKey) || action);
usedKeys.add(actionKey);
}
}
// Remove consecutive duplicate actions
const deduplicatedActions = [];
for (let i = 0; i < mergedActions.length; i++) {
const currentAction = mergedActions[i];
const previousAction = deduplicatedActions[deduplicatedActions.length - 1];
// Only add if it's different from the previous action
if (currentAction !== previousAction) {
deduplicatedActions.push(currentAction);
}
}
// Start with the newer game (it might contain fresher meta-data) and override the
// actions with the merged list.
const mergedHand = {
...newHand,
actions: deduplicatedActions,
};
// If the two games were authored by different people, clear the author field to
// avoid mis-attribution.
if (oldHand.author && newHand.author && oldHand.author !== newHand.author) {
delete mergedHand.author;
}
return mergedHand;
}
export function getGameId(hand) {
if (hand.id)
return hand.id;
return String((hand.table ? hand.table + '-' : '') + hand.hand);
}
/**
* Replaces the cards of a player in the game actions.
* @param game - The game to modify
* @param playerId - The ID of the player to replace cards for
* @param playerName - The name of the player to replace cards for
* @param cards - The new cards to replace with
*/
export function replacePlayerCards(hand, playerId, playerName, cards) {
const heroIndex = !!hand?._venueIds
? ((hand?._venueIds).findIndex(pId => pId === playerId) ?? -1)
: -1;
const heroDhAction = `d dh p${heroIndex + 1} ${cards.join('')} #0 ${playerName}`;
return {
...hand,
actions: hand.actions.map(action => {
// wtf: replacing all actions by all players? at least scope it to d dh action
if (action.includes(`p${heroIndex + 1}`)) {
return heroDhAction;
}
return action;
}),
};
}
//# sourceMappingURL=state.js.map