@idealic/poker-engine
Version:
Professional poker game engine and hand evaluator with built-in iterator utilities
148 lines • 5.87 kB
JavaScript
import { compareHands } from '../actions/showdown';
export function finalizeStacks(game, compare = compareHands) {
// 1) Initialize final stacks
const finishingStacks = game.players.map(p => p.stack);
const winnings = game.players.map(_ => 0);
// Early returns if we can't distribute yet
if (game.isHandComplete || game.pot === 0 || !game.isBettingComplete) {
return finishingStacks;
}
// We'll store how much pot is "fully called" (used for rake) here
game.finalPot = game.pot;
// 3) Identify active (non-folded) players
const activePlayerIndices = game.players
.map((p, i) => (!p.hasFolded ? i : -1))
.filter(x => x !== -1);
// If not all active players have "shown cards," we cannot distribute yet
const allHaveShown = activePlayerIndices.every(i => game.players[i].hasShownCards) ||
activePlayerIndices.length === 1;
if (!allHaveShown) {
return finishingStacks;
}
/**
* STEP A: Determine which players actually contributed (totalBet > 0).
*/
const involved = game.players.map((p, i) => (p.totalBet > 0 ? i : -1)).filter(x => x !== -1);
if (involved.length === 0) {
// No contributions => no pot to distribute
return finishingStacks;
}
/**
* STEP B: We gather all relevant "bet levels":
* 1. The distinct totalBet values of each involved player
* 2. The blindOrStraddle values (e.g. 50, 100) for players who actually posted them
* so we don't skip from 0 -> 200 ignoring a big blind of 100.
*/
const betLevelsSet = new Set();
for (const idx of involved) {
// A player's totalBet
betLevelsSet.add(game.players[idx].totalBet);
}
// Always ensure we start from bet=0 so the first slice covers that range.
betLevelsSet.add(0);
// Convert to a sorted array
const uniqueBetLevels = [...betLevelsSet].sort((a, b) => a - b);
/**
* STEP C: Build pot slices by iterating through consecutive bet levels.
*
* Example betLevels might be [0, 50, 100, 200, 8303].
* We'll slice from 0->50, 50->100, 100->200, 200->8303, etc.
*/
let pots = [];
let prevBet = 0;
let totalCalled = 0;
for (let i = 1; i < uniqueBetLevels.length; i++) {
const currentBet = uniqueBetLevels[i];
if (currentBet <= prevBet) {
// skip duplicates or weirdness
continue;
}
// Find all players whose totalBet >= currentBet
const contributors = involved.filter(idx => game.players[idx].totalBet >= currentBet);
const betDiff = currentBet - prevBet;
const potSlice = contributors.length * betDiff;
// Advance for next iteration
prevBet = currentBet;
if (potSlice === 0) {
continue;
}
// Check for uncalled bet scenario: exactly 1 contributor
if (contributors.length === 1 && !game.players[contributors[0]].hasFolded) {
// Return this slice to that single contributor
finishingStacks[contributors[0]] += potSlice;
// No need to add to pots, not "fully called"
}
else {
// This chunk is fully called
totalCalled += potSlice;
pots.push({
bet: currentBet,
potSize: potSlice,
contributors,
isUncalled: false,
});
}
}
/**
* STEP D: Apply rake only on the portion that was fully called.
*/
const everyoneFolded = activePlayerIndices.length === 1;
const shouldTakeRake = !everyoneFolded && game.street !== 'preflop' && game.board.length > 0;
const rakeBase = shouldTakeRake ? totalCalled : 0;
const rake = typeof game.rake === 'number'
? game.rake // if an explicit amount is provided
: shouldTakeRake
? Math.floor(rakeBase * (game.rakePercentage ?? 0))
: 0;
game.finalPot = totalCalled - rake;
/**
* STEP E: Distribute each pot among eligible winners.
*/
let remainingRake = rake;
pots.forEach(thePot => {
let potSize = thePot.potSize;
// Among contributors, only non-folded players can win
const eligible = thePot.contributors.filter(c => !game.players[c].hasFolded);
// Find best hand(s)
let winners = [];
for (const playerIndex of eligible) {
if (winners.length === 0) {
winners = [playerIndex];
}
else {
const cNew = game.players[playerIndex].cards.concat(game.board);
const cWin = game.players[winners[0]].cards.concat(game.board);
const cmp = compare(cNew, cWin);
if (cmp > 0) {
winners = [playerIndex];
}
else if (cmp === 0) {
winners.push(playerIndex);
}
}
}
// Take proportional rake from this pot
const potRake = (potSize / totalCalled) * rake;
potSize -= potRake;
remainingRake -= potRake;
// Split pot among winners
if (winners.length > 0) {
const share = Math.floor(potSize / winners.length);
const remainder = potSize % winners.length;
for (const w of winners) {
game.players[w].rake += potRake / winners.length;
finishingStacks[w] += share;
winnings[w] += share;
}
// Give leftover remainder to first winner
finishingStacks[winners[0]] += remainder;
winnings[winners[0]] += remainder;
}
});
game.players.forEach((p, i) => {
p.winnings = winnings[i];
p.stack = finishingStacks[i];
});
return finishingStacks;
}
//# sourceMappingURL=stacks.js.map