Files
2026-09-08 19:37:02 +02:00

2150 lines
38 KiB
JavaScript

const SUITS = [
"spades",
"hearts",
"diamonds",
"clubs"
];
const RANKS = [
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"10",
"J",
"Q",
"K",
"A"
];
const RANK_VALUES = {
"2": 2,
"3": 3,
"4": 4,
"5": 5,
"6": 6,
"7": 7,
"8": 8,
"9": 9,
"10": 10,
J: 11,
Q: 12,
K: 13,
A: 14
};
const HAND_CATEGORIES = {
HIGH_CARD: 0,
PAIR: 1,
TWO_PAIR: 2,
THREE_OF_A_KIND: 3,
STRAIGHT: 4,
FLUSH: 5,
FULL_HOUSE: 6,
FOUR_OF_A_KIND: 7,
STRAIGHT_FLUSH: 8,
ROYAL_FLUSH: 9
};
const HAND_CATEGORY_NAMES = {
[HAND_CATEGORIES.HIGH_CARD]:
"High Card",
[HAND_CATEGORIES.PAIR]:
"Pair",
[HAND_CATEGORIES.TWO_PAIR]:
"Two Pair",
[HAND_CATEGORIES.THREE_OF_A_KIND]:
"Three of a Kind",
[HAND_CATEGORIES.STRAIGHT]:
"Straight",
[HAND_CATEGORIES.FLUSH]:
"Flush",
[HAND_CATEGORIES.FULL_HOUSE]:
"Full House",
[HAND_CATEGORIES.FOUR_OF_A_KIND]:
"Four of a Kind",
[HAND_CATEGORIES.STRAIGHT_FLUSH]:
"Straight Flush",
[HAND_CATEGORIES.ROYAL_FLUSH]:
"Royal Flush"
};
const STREETS = [
"PREFLOP",
"FLOP",
"TURN",
"RIVER"
];
class PokerEngineError extends Error {
constructor(
code,
message
) {
super(message);
this.name =
"PokerEngineError";
this.code = code;
}
}
function createDeck() {
const deck = [];
for (const suit of SUITS) {
for (const rank of RANKS) {
deck.push({
rank,
suit
});
}
}
return deck;
}
function shuffleDeck(
deck,
rng = Math.random
) {
const shuffled =
deck.map(card => ({
...card
}));
for (
let index =
shuffled.length - 1;
index > 0;
index--
) {
const swapIndex =
Math.floor(
rng() *
(index + 1)
);
[
shuffled[index],
shuffled[swapIndex]
] = [
shuffled[swapIndex],
shuffled[index]
];
}
return shuffled;
}
function cardToString(
card
) {
if (!card) {
return "";
}
const symbols = {
spades: "♠",
hearts: "♥",
diamonds: "♦",
clubs: "♣"
};
return (
`${card.rank}${symbols[card.suit] ?? "?"}`
);
}
function cloneState(
state
) {
if (
typeof structuredClone ===
"function"
) {
return structuredClone(
state
);
}
return JSON.parse(
JSON.stringify(state)
);
}
function combinations(
items,
choose
) {
const output = [];
function walk(
start,
current
) {
if (
current.length ===
choose
) {
output.push(
current.slice()
);
return;
}
for (
let index = start;
index < items.length;
index++
) {
current.push(
items[index]
);
walk(
index + 1,
current
);
current.pop();
}
}
walk(
0,
[]
);
return output;
}
function evaluateFiveCardHand(
cards
) {
if (
!Array.isArray(cards) ||
cards.length !== 5
) {
throw new PokerEngineError(
"INVALID_HAND",
"Exactly five cards are required."
);
}
const values =
cards
.map(
card =>
RANK_VALUES[card.rank]
)
.sort(
(a, b) =>
b - a
);
if (
values.some(
value =>
!Number.isInteger(value)
)
) {
throw new PokerEngineError(
"INVALID_CARD",
"A card has an invalid rank."
);
}
const flush =
cards.every(
card =>
card.suit ===
cards[0].suit
);
const uniqueValues =
[...new Set(values)];
let straightHigh = null;
if (
uniqueValues.length === 5
) {
if (
uniqueValues[0] -
uniqueValues[4] ===
4
) {
straightHigh =
uniqueValues[0];
}
else if (
uniqueValues.join(",") ===
"14,5,4,3,2"
) {
straightHigh = 5;
}
}
const counts =
new Map();
for (const value of values) {
counts.set(
value,
(counts.get(value) ?? 0) + 1
);
}
const groups =
[...counts.entries()]
.map(
([value, count]) => ({
value,
count
})
)
.sort(
(a, b) =>
b.count - a.count ||
b.value - a.value
);
if (
flush &&
straightHigh !== null
) {
if (
straightHigh === 14
) {
return buildRank(
HAND_CATEGORIES.ROYAL_FLUSH,
[14],
cards
);
}
return buildRank(
HAND_CATEGORIES.STRAIGHT_FLUSH,
[straightHigh],
cards
);
}
if (
groups[0].count === 4
) {
return buildRank(
HAND_CATEGORIES.FOUR_OF_A_KIND,
[
groups[0].value,
groups[1].value
],
cards
);
}
if (
groups[0].count === 3 &&
groups[1].count === 2
) {
return buildRank(
HAND_CATEGORIES.FULL_HOUSE,
[
groups[0].value,
groups[1].value
],
cards
);
}
if (flush) {
return buildRank(
HAND_CATEGORIES.FLUSH,
values,
cards
);
}
if (
straightHigh !== null
) {
return buildRank(
HAND_CATEGORIES.STRAIGHT,
[straightHigh],
cards
);
}
if (
groups[0].count === 3
) {
const kickers =
groups
.slice(1)
.map(
group =>
group.value
)
.sort(
(a, b) =>
b - a
);
return buildRank(
HAND_CATEGORIES.THREE_OF_A_KIND,
[
groups[0].value,
...kickers
],
cards
);
}
if (
groups[0].count === 2 &&
groups[1].count === 2
) {
const pairs =
[
groups[0].value,
groups[1].value
].sort(
(a, b) =>
b - a
);
const kicker =
groups.find(
group =>
group.count === 1
).value;
return buildRank(
HAND_CATEGORIES.TWO_PAIR,
[
...pairs,
kicker
],
cards
);
}
if (
groups[0].count === 2
) {
const kickers =
groups
.slice(1)
.map(
group =>
group.value
)
.sort(
(a, b) =>
b - a
);
return buildRank(
HAND_CATEGORIES.PAIR,
[
groups[0].value,
...kickers
],
cards
);
}
return buildRank(
HAND_CATEGORIES.HIGH_CARD,
values,
cards
);
}
function buildRank(
category,
tiebreak,
cards
) {
return {
category,
name:
HAND_CATEGORY_NAMES[
category
],
tiebreak,
cards:
cards.map(
card => ({
...card
})
)
};
}
function compareHandRanks(
left,
right
) {
if (
left.category !==
right.category
) {
return (
left.category >
right.category
? 1
: -1
);
}
const length =
Math.max(
left.tiebreak.length,
right.tiebreak.length
);
for (
let index = 0;
index < length;
index++
) {
const leftValue =
left.tiebreak[index] ?? 0;
const rightValue =
right.tiebreak[index] ?? 0;
if (
leftValue !==
rightValue
) {
return (
leftValue > rightValue
? 1
: -1
);
}
}
return 0;
}
function evaluateHand(
cards
) {
if (
!Array.isArray(cards) ||
cards.length < 5 ||
cards.length > 7
) {
throw new PokerEngineError(
"INVALID_HAND",
"A poker hand must contain between five and seven cards."
);
}
let best = null;
for (
const candidate
of combinations(
cards,
5
)
) {
const rank =
evaluateFiveCardHand(
candidate
);
if (
!best ||
compareHandRanks(
rank,
best
) > 0
) {
best = rank;
}
}
return best;
}
function splitPot(
pot,
winnerSeats,
dealerSeat = -1
) {
if (
!Number.isSafeInteger(pot) ||
pot < 0
) {
throw new PokerEngineError(
"INVALID_POT",
"Pot must be a non-negative safe integer."
);
}
if (
!Array.isArray(winnerSeats) ||
winnerSeats.length === 0
) {
throw new PokerEngineError(
"NO_WINNERS",
"At least one winner is required."
);
}
const uniqueSeats =
[...new Set(winnerSeats)]
.sort(
(a, b) =>
a - b
);
const base =
Math.floor(
pot /
uniqueSeats.length
);
let remainder =
pot % uniqueSeats.length;
const ordered =
orderSeatsAfter(
uniqueSeats,
dealerSeat
);
const payouts = {};
for (const seat of ordered) {
payouts[seat] = base;
}
for (
let index = 0;
remainder > 0;
index++, remainder--
) {
const seat =
ordered[
index %
ordered.length
];
payouts[seat]++;
}
return payouts;
}
function resolveShowdown(
state
) {
const active =
state.players.filter(
player =>
!player.folded
);
if (
active.length === 0
) {
throw new PokerEngineError(
"NO_ACTIVE_PLAYERS",
"Showdown has no active players."
);
}
let winners = [];
let bestRank = null;
for (const player of active) {
const rank =
evaluateHand([
...player.holeCards,
...state.communityCards
]);
player.handRank = rank;
if (!bestRank) {
bestRank = rank;
winners = [player];
continue;
}
const comparison =
compareHandRanks(
rank,
bestRank
);
if (comparison > 0) {
bestRank = rank;
winners = [player];
}
else if (
comparison === 0
) {
winners.push(player);
}
}
const payouts =
splitPot(
state.pot,
winners.map(
player =>
player.seat
),
state.dealerSeat
);
for (const player of state.players) {
const payout =
payouts[player.seat] ?? 0;
player.stack += payout;
}
state.result = {
type:
"showdown",
winnerSeats:
winners.map(
player =>
player.seat
),
payouts,
handRank:
bestRank
};
state.pot = 0;
state.state =
"PAYOUT";
state.currentSeat = null;
return state;
}
function startHand({
players,
dealerSeat = null,
smallBlind = 100,
bigBlind = 200,
deck = null
}) {
validatePlayers(
players
);
if (
!Number.isSafeInteger(
smallBlind
) ||
!Number.isSafeInteger(
bigBlind
) ||
smallBlind <= 0 ||
bigBlind <= 0 ||
smallBlind >= bigBlind
) {
throw new PokerEngineError(
"INVALID_BLINDS",
"Blinds must be positive integers and the small blind must be lower than the big blind."
);
}
const sortedPlayers =
players
.map(
player => ({
userId:
String(
player.userId
),
seat:
Number(
player.seat
),
stack:
Number(
player.stack
)
})
)
.sort(
(a, b) =>
a.seat - b.seat
);
const handCap =
Math.min(
...sortedPlayers.map(
player =>
player.stack
)
);
if (
handCap < bigBlind
) {
throw new PokerEngineError(
"STACK_BELOW_BIG_BLIND",
"Every player must have at least one big blind available to start a hand."
);
}
const seats =
sortedPlayers.map(
player =>
player.seat
);
const resolvedDealerSeat =
dealerSeat === null
? seats[0]
: Number(dealerSeat);
if (
!seats.includes(
resolvedDealerSeat
)
) {
throw new PokerEngineError(
"INVALID_DEALER",
"Dealer seat does not belong to an active player."
);
}
const workingDeck =
deck
? deck.map(
card => ({
...card
})
)
: shuffleDeck(
createDeck()
);
if (
workingDeck.length <
sortedPlayers.length * 2 + 5
) {
throw new PokerEngineError(
"INSUFFICIENT_DECK",
"The deck does not contain enough cards."
);
}
const state = {
state:
"PREFLOP",
street:
"PREFLOP",
players:
sortedPlayers.map(
player => ({
...player,
startingStack:
player.stack,
handCap,
totalBet: 0,
streetBet: 0,
folded: false,
allIn: false,
actedThisStreet:
false,
holeCards: []
})
),
dealerSeat:
resolvedDealerSeat,
smallBlind,
bigBlind,
smallBlindSeat: null,
bigBlindSeat: null,
currentSeat: null,
communityCards: [],
deck:
workingDeck,
pot: 0,
currentBet: 0,
lastRaiseSize:
bigBlind,
handCap,
result: null
};
dealHoleCards(
state
);
const headsUp =
state.players.length === 2;
const smallBlindSeat =
headsUp
? state.dealerSeat
: nextSeatAfter(
state.players,
state.dealerSeat,
() => true
);
const bigBlindSeat =
nextSeatAfter(
state.players,
smallBlindSeat,
() => true
);
state.smallBlindSeat =
smallBlindSeat;
state.bigBlindSeat =
bigBlindSeat;
postForcedBet(
state,
smallBlindSeat,
smallBlind
);
postForcedBet(
state,
bigBlindSeat,
bigBlind
);
state.currentBet =
bigBlind;
state.lastRaiseSize =
bigBlind;
state.currentSeat =
nextSeatAfter(
state.players,
bigBlindSeat,
canPlayerAct
);
if (
state.currentSeat === null
) {
progressUntilActionOrPayout(
state
);
}
return state;
}
function validatePlayers(
players
) {
if (
!Array.isArray(players) ||
players.length < 2 ||
players.length > 6
) {
throw new PokerEngineError(
"INVALID_PLAYER_COUNT",
"Poker requires between 2 and 6 players."
);
}
const userIds =
new Set();
const seats =
new Set();
for (const player of players) {
const userId =
String(
player?.userId ?? ""
);
const seat =
Number(
player?.seat
);
const stack =
Number(
player?.stack
);
if (!userId) {
throw new PokerEngineError(
"INVALID_PLAYER",
"Every player needs a userId."
);
}
if (
!Number.isSafeInteger(seat) ||
seat < 0
) {
throw new PokerEngineError(
"INVALID_SEAT",
"Every player needs a non-negative integer seat."
);
}
if (
!Number.isSafeInteger(stack) ||
stack <= 0
) {
throw new PokerEngineError(
"INVALID_STACK",
"Every player needs a positive integer stack."
);
}
if (
userIds.has(userId)
) {
throw new PokerEngineError(
"DUPLICATE_PLAYER",
"A player cannot occupy multiple seats."
);
}
if (
seats.has(seat)
) {
throw new PokerEngineError(
"DUPLICATE_SEAT",
"Two players cannot occupy the same seat."
);
}
userIds.add(userId);
seats.add(seat);
}
}
function dealHoleCards(
state
) {
for (
let round = 0;
round < 2;
round++
) {
const orderedSeats =
orderSeatsAfter(
state.players.map(
player =>
player.seat
),
state.dealerSeat
);
for (const seat of orderedSeats) {
const player =
getPlayerBySeat(
state,
seat
);
player.holeCards.push(
state.deck.pop()
);
}
}
}
function postForcedBet(
state,
seat,
amount
) {
const player =
getPlayerBySeat(
state,
seat
);
const wager =
Math.min(
amount,
remainingHandBudget(
player
)
);
commitChips(
state,
player,
wager
);
if (
remainingHandBudget(
player
) === 0
) {
player.allIn = true;
}
}
function getLegalActions(
state,
userId
) {
if (
!STREETS.includes(
state.state
)
) {
return {
actions: [],
reason:
"HAND_NOT_ACTIVE"
};
}
const player =
state.players.find(
candidate =>
candidate.userId ===
String(userId)
);
if (!player) {
return {
actions: [],
reason:
"PLAYER_NOT_FOUND"
};
}
if (
player.seat !==
state.currentSeat
) {
return {
actions: [],
reason:
"OUT_OF_TURN"
};
}
if (
!canPlayerAct(player)
) {
return {
actions: [],
reason:
"PLAYER_CANNOT_ACT"
};
}
const toCall =
Math.max(
0,
state.currentBet -
player.streetBet
);
const remaining =
remainingHandBudget(
player
);
const maxBetTo =
player.streetBet +
remaining;
const actions = [
"fold"
];
if (
toCall === 0
) {
actions.push(
"check"
);
}
else if (
remaining > 0
) {
actions.push(
"call"
);
}
if (
remaining > 0 &&
maxBetTo >
state.currentBet
) {
actions.push(
state.currentBet === 0
? "bet"
: "raise"
);
}
if (
remaining > 0
) {
actions.push(
"all_in"
);
}
const minimumRaiseTo =
state.currentBet === 0
? state.bigBlind
: state.currentBet +
state.lastRaiseSize;
return {
actions,
toCall,
minRaiseTo:
Math.min(
minimumRaiseTo,
maxBetTo
),
maxRaiseTo:
maxBetTo,
handCap:
state.handCap,
remainingHandBudget:
remaining
};
}
function applyAction(
originalState,
userId,
action
) {
const state =
cloneState(
originalState
);
if (
!action ||
!action.type
) {
throw new PokerEngineError(
"INVALID_ACTION",
"An action type is required."
);
}
if (
!STREETS.includes(
state.state
)
) {
throw new PokerEngineError(
"HAND_NOT_ACTIVE",
"The hand is not accepting actions."
);
}
const player =
state.players.find(
candidate =>
candidate.userId ===
String(userId)
);
if (!player) {
throw new PokerEngineError(
"PLAYER_NOT_FOUND",
"Player is not part of this hand."
);
}
if (
player.seat !==
state.currentSeat
) {
throw new PokerEngineError(
"OUT_OF_TURN",
"It is not this player's turn."
);
}
if (
!canPlayerAct(player)
) {
throw new PokerEngineError(
"PLAYER_CANNOT_ACT",
"This player cannot act."
);
}
const type =
String(
action.type
).toLowerCase();
const toCall =
Math.max(
0,
state.currentBet -
player.streetBet
);
if (type === "fold") {
player.folded = true;
player.actedThisStreet = true;
}
else if (
type === "check"
) {
if (
toCall !== 0
) {
throw new PokerEngineError(
"CANNOT_CHECK",
"Player cannot check while facing a bet."
);
}
player.actedThisStreet = true;
}
else if (
type === "call"
) {
if (
toCall <= 0
) {
throw new PokerEngineError(
"NOTHING_TO_CALL",
"There is no bet to call."
);
}
const remaining =
remainingHandBudget(
player
);
if (
remaining < toCall
) {
throw new PokerEngineError(
"CALL_EXCEEDS_CAP",
"Call would exceed the single-pot hand cap."
);
}
commitChips(
state,
player,
toCall
);
player.actedThisStreet = true;
if (
remainingHandBudget(
player
) === 0
) {
player.allIn = true;
}
}
else if (
type === "bet" ||
type === "raise"
) {
const target =
Number(
action.amount
);
if (
!Number.isSafeInteger(
target
) ||
target <= 0
) {
throw new PokerEngineError(
"INVALID_RAISE",
"Bet or raise amount must be a positive integer target."
);
}
if (
type === "bet" &&
state.currentBet !== 0
) {
throw new PokerEngineError(
"BET_NOT_AVAILABLE",
"Use raise when a bet already exists."
);
}
if (
type === "raise" &&
state.currentBet === 0
) {
throw new PokerEngineError(
"RAISE_NOT_AVAILABLE",
"Use bet when no bet exists."
);
}
applyRaiseTo(
state,
player,
target
);
}
else if (
type === "all_in"
) {
const remaining =
remainingHandBudget(
player
);
if (
remaining <= 0
) {
throw new PokerEngineError(
"NO_CHIPS_AVAILABLE",
"Player has no playable chips remaining this hand."
);
}
const oldCurrentBet =
state.currentBet;
const target =
player.streetBet +
remaining;
commitChips(
state,
player,
remaining
);
player.allIn = true;
player.actedThisStreet = true;
if (
target >
oldCurrentBet
) {
const raiseSize =
target -
oldCurrentBet;
if (
raiseSize >=
state.lastRaiseSize
) {
state.lastRaiseSize =
raiseSize;
reopenBettingAfterRaise(
state,
player.seat
);
}
state.currentBet =
target;
}
}
else {
throw new PokerEngineError(
"UNKNOWN_ACTION",
"Unknown poker action."
);
}
progressAfterAction(
state,
player.seat
);
return state;
}
function applyRaiseTo(
state,
player,
target
) {
const remaining =
remainingHandBudget(
player
);
const maxTarget =
player.streetBet +
remaining;
if (
target > maxTarget
) {
throw new PokerEngineError(
"RAISE_EXCEEDS_CAP",
"Raise would exceed the single-pot hand cap."
);
}
if (
target <=
state.currentBet
) {
throw new PokerEngineError(
"RAISE_TOO_SMALL",
"Raise target must exceed the current bet."
);
}
const minimumTarget =
state.currentBet === 0
? state.bigBlind
: state.currentBet +
state.lastRaiseSize;
if (
target < minimumTarget
) {
throw new PokerEngineError(
"RAISE_TOO_SMALL",
`Minimum raise target is ${minimumTarget}.`
);
}
const oldCurrentBet =
state.currentBet;
const amount =
target -
player.streetBet;
commitChips(
state,
player,
amount
);
state.currentBet =
target;
state.lastRaiseSize =
target -
oldCurrentBet;
reopenBettingAfterRaise(
state,
player.seat
);
player.actedThisStreet = true;
if (
remainingHandBudget(
player
) === 0
) {
player.allIn = true;
}
}
function reopenBettingAfterRaise(
state,
raiserSeat
) {
for (const candidate of state.players) {
if (
candidate.seat !==
raiserSeat &&
canPlayerAct(candidate)
) {
candidate.actedThisStreet =
false;
}
}
}
function commitChips(
state,
player,
amount
) {
if (
!Number.isSafeInteger(amount) ||
amount < 0
) {
throw new PokerEngineError(
"INVALID_COMMIT",
"Committed chips must be a non-negative integer."
);
}
if (
amount >
remainingHandBudget(player)
) {
throw new PokerEngineError(
"HAND_CAP_EXCEEDED",
"Player cannot contribute beyond the single-pot hand cap."
);
}
if (
amount > player.stack
) {
throw new PokerEngineError(
"STACK_EXCEEDED",
"Player does not have enough chips."
);
}
player.stack -= amount;
player.totalBet += amount;
player.streetBet += amount;
state.pot += amount;
}
function remainingHandBudget(
player
) {
return Math.max(
0,
player.handCap -
player.totalBet
);
}
function canPlayerAct(
player
) {
return (
!player.folded &&
!player.allIn &&
remainingHandBudget(
player
) > 0
);
}
function progressAfterAction(
state,
actedSeat
) {
const activePlayers =
state.players.filter(
player =>
!player.folded
);
if (
activePlayers.length === 1
) {
awardFoldWin(
state,
activePlayers[0]
);
return;
}
if (
isBettingRoundComplete(
state
)
) {
advanceStreet(
state
);
progressUntilActionOrPayout(
state
);
return;
}
state.currentSeat =
nextSeatAfter(
state.players,
actedSeat,
canPlayerAct
);
if (
state.currentSeat === null
) {
advanceStreet(
state
);
progressUntilActionOrPayout(
state
);
}
}
function progressUntilActionOrPayout(
state
) {
while (
STREETS.includes(
state.state
)
) {
const activePlayers =
state.players.filter(
player =>
!player.folded
);
if (
activePlayers.length === 1
) {
awardFoldWin(
state,
activePlayers[0]
);
return;
}
const actionable =
state.players.filter(
canPlayerAct
);
if (
actionable.length === 0
) {
advanceStreet(
state
);
continue;
}
if (
state.currentSeat === null ||
!actionable.some(
player =>
player.seat ===
state.currentSeat
)
) {
state.currentSeat =
firstPostflopSeat(
state
);
}
return;
}
}
function isBettingRoundComplete(
state
) {
const actionable =
state.players.filter(
canPlayerAct
);
if (
actionable.length === 0
) {
return true;
}
return actionable.every(
player =>
player.actedThisStreet &&
player.streetBet ===
state.currentBet
);
}
function advanceStreet(
state
) {
if (
state.state ===
"PREFLOP"
) {
state.state = "FLOP";
state.street = "FLOP";
state.communityCards.push(
state.deck.pop(),
state.deck.pop(),
state.deck.pop()
);
}
else if (
state.state ===
"FLOP"
) {
state.state = "TURN";
state.street = "TURN";
state.communityCards.push(
state.deck.pop()
);
}
else if (
state.state ===
"TURN"
) {
state.state = "RIVER";
state.street = "RIVER";
state.communityCards.push(
state.deck.pop()
);
}
else if (
state.state ===
"RIVER"
) {
state.state =
"SHOWDOWN";
state.street =
"SHOWDOWN";
resolveShowdown(
state
);
return state;
}
else {
return state;
}
state.currentBet = 0;
state.lastRaiseSize =
state.bigBlind;
for (const player of state.players) {
player.streetBet = 0;
player.actedThisStreet =
false;
}
state.currentSeat =
firstPostflopSeat(
state
);
return state;
}
function awardFoldWin(
state,
winner
) {
const payout =
state.pot;
winner.stack += payout;
state.result = {
type:
"fold",
winnerSeats: [
winner.seat
],
payouts: {
[winner.seat]:
payout
},
handRank: null
};
state.pot = 0;
state.state =
"PAYOUT";
state.currentSeat = null;
}
function firstPostflopSeat(
state
) {
return nextSeatAfter(
state.players,
state.dealerSeat,
canPlayerAct
);
}
function nextSeatAfter(
players,
seat,
predicate = () => true
) {
const seats =
players
.map(
player =>
player.seat
)
.sort(
(a, b) =>
a - b
);
if (
seats.length === 0
) {
return null;
}
const startIndex =
seats.findIndex(
candidate =>
candidate > seat
);
const ordered =
startIndex === -1
? seats.slice()
: [
...seats.slice(
startIndex
),
...seats.slice(
0,
startIndex
)
];
if (
startIndex === -1
) {
ordered.sort(
(a, b) =>
a - b
);
}
for (const candidateSeat of ordered) {
const player =
players.find(
candidate =>
candidate.seat ===
candidateSeat
);
if (
player &&
predicate(player)
) {
return candidateSeat;
}
}
return null;
}
function orderSeatsAfter(
seats,
seat
) {
const sorted =
[...new Set(seats)]
.sort(
(a, b) =>
a - b
);
if (
sorted.length <= 1
) {
return sorted;
}
const after =
sorted.filter(
candidate =>
candidate > seat
);
const before =
sorted.filter(
candidate =>
candidate <= seat
);
return [
...after,
...before
];
}
function getPlayerBySeat(
state,
seat
) {
const player =
state.players.find(
candidate =>
candidate.seat ===
seat
);
if (!player) {
throw new PokerEngineError(
"PLAYER_NOT_FOUND",
`No player occupies seat ${seat}.`
);
}
return player;
}
function getPublicState(
state
) {
return {
state:
state.state,
street:
state.street,
dealerSeat:
state.dealerSeat,
smallBlindSeat:
state.smallBlindSeat,
bigBlindSeat:
state.bigBlindSeat,
currentSeat:
state.currentSeat,
smallBlind:
state.smallBlind,
bigBlind:
state.bigBlind,
handCap:
state.handCap,
pot:
state.pot,
currentBet:
state.currentBet,
communityCards:
state.communityCards.map(
card => ({
...card
})
),
players:
state.players.map(
player => ({
userId:
player.userId,
seat:
player.seat,
stack:
player.stack,
folded:
player.folded,
allIn:
player.allIn,
streetBet:
player.streetBet,
totalBet:
player.totalBet
})
),
result:
state.result
};
}
function getPrivateCards(
state,
userId
) {
const player =
state.players.find(
candidate =>
candidate.userId ===
String(userId)
);
if (!player) {
throw new PokerEngineError(
"PLAYER_NOT_FOUND",
"Player is not part of this hand."
);
}
return player.holeCards.map(
card => ({
...card
})
);
}
module.exports = {
SUITS,
RANKS,
RANK_VALUES,
HAND_CATEGORIES,
HAND_CATEGORY_NAMES,
STREETS,
PokerEngineError,
createDeck,
shuffleDeck,
cardToString,
evaluateFiveCardHand,
evaluateHand,
compareHandRanks,
splitPot,
resolveShowdown,
startHand,
getLegalActions,
applyAction,
advanceStreet,
getPublicState,
getPrivateCards,
remainingHandBudget,
canPlayerAct,
nextSeatAfter,
orderSeatsAfter
};