Your bug report was about a category, not one button, so I went looking for the
rest of the guessing rather than picking a new topic. Two left, both about the
button -- and one of them was in the fix I'd just shipped.
The deal preview lied
The prompt added last time promises who will post. With the button parked on an
empty chair it promised the wrong pair: it read the button where it currently sat,
while dealing relocates the button onto a player first and only then works out the
blinds.
three players in seats 2-4, button parked on empty seat 1
preview said: s2 posts $1, s3 posts $2
dealing gave: s3 posts $1, s4 posts $2
Exactly the fault you reported -- the display and the thing it describes computed
from different moments -- inside the change meant to remove it. buttonSeatForDeal
answers where the button will be without moving it, so the preview asks the same
question the deal will answer. And the move is now stated before it happens:
"Button moves to Ben - seat 1 is empty."
Moving the button after the deal moved the blinds silently
Once the blinds are posted, moving the button takes them back and re-posts them,
because otherwise the badges and the money disagree. That's the right behaviour
and it was completely silent. The toast says so now: "Button on Dee - blinds taken
back and re-posted."
It also only does it when there is actually something posted to move. Before the
deal there is nothing committed, so passing the button is just moving a marker --
which is the whole point of the setup step.
Checked and deliberately left alone
Streets still advance one press at a time during an all-in run-out, which is right
-- the dealer is physically dealing the board between them. A player who joins
mid-hand is still marked folded for that hand and plays the next one. The
base-pot-amount shorthand still stands in for "everyone antes the same". Nothing
mutates state during a render.
4 tests (152 total): the preview matches what dealing produces with the button on
an empty chair, asking where the button will be doesn't move it, a button already
on a player is left alone, and an empty table doesn't break the question.
Driven through the UI: the prompt warns about the move and then the deal matches
it exactly; moving the button after the deal announces the re-post; and the blinds
survive being passed around three seats with the chip count fixed at 400. Layout
and contrast audits clean.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
505 lines
22 KiB
JavaScript
505 lines
22 KiB
JavaScript
// Table state: seats, chips, blinds, notes. Persisted to localStorage so a dealer's
|
|
// browser refresh mid-game doesn't lose the table.
|
|
const HoldemState = {
|
|
STORAGE_KEY: "casino.holdem.state.v1",
|
|
DEFAULT_SEAT_COUNT: 10,
|
|
|
|
DEFAULT_CHIP_LEGEND: [
|
|
{ id: "white", color: "#f5f5f5", value: 1 },
|
|
{ id: "red", color: "#c0392b", value: 5 },
|
|
{ id: "green", color: "#1e8449", value: 25 },
|
|
{ id: "black", color: "#111111", value: 100 },
|
|
{ id: "purple", color: "#6c3483", value: 500 },
|
|
],
|
|
|
|
makeId(prefix) {
|
|
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
},
|
|
|
|
makePlayer() {
|
|
return {
|
|
id: this.makeId("player"),
|
|
name: "",
|
|
inHand: false,
|
|
folded: false,
|
|
cards: ["", ""],
|
|
chipCounts: {},
|
|
amountOverride: null,
|
|
customFields: [],
|
|
// Live-betting fields: chips in front of the player, chips they've put in
|
|
// on the current street, and whether they've answered the current bet.
|
|
stack: null,
|
|
committed: 0,
|
|
hasActed: false,
|
|
// Cash handed over tonight, which only moves when they buy chips.
|
|
buyIns: 0,
|
|
};
|
|
},
|
|
|
|
MAX_HISTORY_ENTRIES: 200,
|
|
|
|
createDefault() {
|
|
const seatCount = this.DEFAULT_SEAT_COUNT;
|
|
return {
|
|
seatCount,
|
|
direction: "clockwise",
|
|
basePotAmount: null,
|
|
dealerSeatIndex: 0,
|
|
dedicatedDealer: false,
|
|
chipLegend: this.DEFAULT_CHIP_LEGEND.map(c => ({ ...c })),
|
|
seats: Array.from({ length: seatCount }, () => this.makePlayer()),
|
|
notes: "",
|
|
handNumber: 1,
|
|
handHistory: [],
|
|
smallBlind: 1,
|
|
bigBlind: 2,
|
|
defaultStack: null,
|
|
street: "preflop",
|
|
currentBet: 0,
|
|
lastRaiseSize: 0,
|
|
actingSeatIndex: null,
|
|
// Whether the betting has actually been opened for this hand.
|
|
//
|
|
// Dealing and betting used to be one button: "Next Hand" moved the button
|
|
// and posted the blinds in the same breath. That meant the moment between
|
|
// hands — the one where people are still sitting down, standing up and
|
|
// arguing about whose turn it is — was spent with money already committed,
|
|
// and the blind badges recomputing off `inHand` while the posted chips
|
|
// stayed frozen from when the hand began. Seat somebody after pressing it
|
|
// and the table would show a big blind who had posted nothing.
|
|
//
|
|
// So the dealer says when. False means the table is being set up: nothing
|
|
// is committed, the badges are a preview, and seating or moving the button
|
|
// costs nothing.
|
|
bettingOpen: false,
|
|
// Set when the dealer names the blinds by hand instead of letting them
|
|
// follow the button — { sbIndex, bbIndex }. Home games do this constantly
|
|
// ("you had it last time", someone arriving late owing a blind), and the
|
|
// button order is the wrong answer often enough that it has to be
|
|
// overridable. Cleared at the start of every hand.
|
|
blindOverride: null,
|
|
};
|
|
},
|
|
|
|
load() {
|
|
try {
|
|
const raw = localStorage.getItem(this.STORAGE_KEY);
|
|
if (!raw) return this.createDefault();
|
|
const parsed = JSON.parse(raw);
|
|
if (!parsed || !Array.isArray(parsed.seats)) return this.createDefault();
|
|
if (typeof parsed.handNumber !== "number" || parsed.handNumber < 1) parsed.handNumber = 1;
|
|
if (!Array.isArray(parsed.handHistory)) parsed.handHistory = [];
|
|
|
|
// Tables saved before live betting existed are missing its fields; fill
|
|
// them in rather than making the dealer reset a table mid-session.
|
|
const defaults = this.createDefault();
|
|
["smallBlind", "bigBlind", "defaultStack", "street", "currentBet", "lastRaiseSize",
|
|
"actingSeatIndex", "blindOverride"]
|
|
.forEach(key => {
|
|
if (parsed[key] === undefined) parsed[key] = defaults[key];
|
|
});
|
|
|
|
// A table saved before dealing and betting were separate has no flag, and
|
|
// guessing false would show "deal the hand" over a hand already in
|
|
// progress. Anything already committed, or anyone already on the clock,
|
|
// means the betting was open when they closed the tab.
|
|
if (parsed.bettingOpen === undefined) {
|
|
parsed.bettingOpen =
|
|
parsed.actingSeatIndex !== null && parsed.actingSeatIndex !== undefined ||
|
|
parsed.seats.some(p =>
|
|
(Number(p.committed) || 0) > 0 || (Number(p.amountOverride) || 0) > 0);
|
|
}
|
|
parsed.seats.forEach(player => {
|
|
if (player.stack === undefined) player.stack = null;
|
|
if (player.committed === undefined) player.committed = 0;
|
|
if (player.hasActed === undefined) player.hasActed = false;
|
|
if (player.buyIns === undefined) player.buyIns = 0;
|
|
});
|
|
|
|
return parsed;
|
|
} catch {
|
|
return this.createDefault();
|
|
}
|
|
},
|
|
|
|
save(state) {
|
|
try {
|
|
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(state));
|
|
} catch {
|
|
// Storage full/unavailable (private browsing, etc.) — the table still works
|
|
// for the rest of the session, it just won't survive a refresh.
|
|
}
|
|
},
|
|
|
|
clear() {
|
|
try {
|
|
localStorage.removeItem(this.STORAGE_KEY);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
},
|
|
|
|
resizeSeats(state, newCount) {
|
|
const count = Math.max(2, Math.min(24, Math.floor(newCount) || this.DEFAULT_SEAT_COUNT));
|
|
const seats = state.seats.slice(0, count);
|
|
while (seats.length < count) seats.push(this.makePlayer());
|
|
state.seats = seats;
|
|
state.seatCount = count;
|
|
if (state.dealerSeatIndex >= count) state.dealerSeatIndex = 0;
|
|
return state;
|
|
},
|
|
|
|
// What to call a seat in logs, payouts and standings when the dealer hasn't
|
|
// typed a name in yet.
|
|
displayName(player, seatIndex) {
|
|
return player.name && player.name.trim() ? player.name.trim() : `Seat ${seatIndex + 1}`;
|
|
},
|
|
|
|
// Contribution the table calculator will treat this player as having put in.
|
|
//
|
|
// Chips already pushed into the middle count whether or not the player is still
|
|
// in the hand. Someone who storms off, gets taken out by the dealer, or is
|
|
// dealt out mid-hand leaves their money on the felt — it's dead money owed to
|
|
// whoever wins, and it is not theirs to take back. Reading `inHand` first is
|
|
// how those chips used to disappear out of the pot the instant a seat was
|
|
// switched off, quietly shrinking the payout by whatever they had posted.
|
|
computeContribution(player, chipLegend, basePotAmount) {
|
|
const override = player.amountOverride;
|
|
if (override !== null && override !== undefined && String(override).trim() !== "") {
|
|
const parsed = Number(override);
|
|
if (!Number.isNaN(parsed)) return Math.max(0, parsed);
|
|
}
|
|
|
|
const chipTotal = chipLegend.reduce((sum, chip) => {
|
|
const count = Number(player.chipCounts[chip.id]) || 0;
|
|
return sum + count * Number(chip.value || 0);
|
|
}, 0);
|
|
if (chipTotal > 0) return chipTotal;
|
|
|
|
// The table-wide default is the one thing that stays tied to being dealt in:
|
|
// it's a stand-in for "everyone put the same amount in", so applying it to
|
|
// empty chairs would invent a contribution from every seat nobody is using.
|
|
if (!player.inHand) return 0;
|
|
|
|
const base = Number(basePotAmount);
|
|
return Number.isNaN(base) ? 0 : Math.max(0, base);
|
|
},
|
|
|
|
// Anyone whose chips are in the pot, whether or not they're still in the hand.
|
|
// A seat that's been switched off mid-hand is dead money: it counts towards the
|
|
// pot but can never be picked as a winner, exactly like a fold.
|
|
isDeadMoney(player) {
|
|
return !player.inHand;
|
|
},
|
|
|
|
// What starting the next hand would take off the table for good: exactly the
|
|
// fields startNextHand clears.
|
|
//
|
|
// Deliberately not totalPot(). The table-wide base pot is a standing ante — a
|
|
// stand-in for "everyone puts the same in every hand", used by a dealer who
|
|
// isn't tracking stacks — so it re-applies to the next hand rather than being
|
|
// lost with this one. Counting it would mean warning about money that isn't
|
|
// going anywhere, on every single hand, for exactly the dealers who chose the
|
|
// simplest way to run the table.
|
|
unresolvedContributions(state) {
|
|
const total = state.seats.reduce((sum, player) => {
|
|
const override = player.amountOverride;
|
|
let banked = 0;
|
|
|
|
if (override !== null && override !== undefined && String(override).trim() !== "") {
|
|
const parsed = Number(override);
|
|
if (!Number.isNaN(parsed)) banked = Math.max(0, parsed);
|
|
}
|
|
|
|
if (banked === 0) {
|
|
banked = (state.chipLegend || []).reduce((chips, chip) => {
|
|
const count = Number(player.chipCounts[chip.id]) || 0;
|
|
return chips + count * Number(chip.value || 0);
|
|
}, 0);
|
|
}
|
|
|
|
return sum + banked + Math.max(0, Number(player.committed) || 0);
|
|
}, 0);
|
|
|
|
return Money.round(total);
|
|
},
|
|
|
|
totalPot(state) {
|
|
return state.seats.reduce(
|
|
(sum, p) => sum + this.computeContribution(p, state.chipLegend, state.basePotAmount),
|
|
0
|
|
);
|
|
},
|
|
|
|
// Next occupied-by-index seat in the current blind direction. Every seat index
|
|
// counts (there's no separate "empty chair" concept) — a dealer running fewer
|
|
// players just keeps the seat count matched to who's actually sitting down.
|
|
nextSeatIndex(state, fromIndex, step = 1) {
|
|
const count = state.seatCount;
|
|
const delta = state.direction === "counterclockwise" ? -step : step;
|
|
return ((fromIndex + delta) % count + count) % count;
|
|
},
|
|
|
|
// The button passes to the next player, not the next chair. Moving it one seat at
|
|
// a time parks it on empty chairs — with four players spread around ten seats it
|
|
// sits on nobody most hands — and that isn't only untidy: position and blinds are
|
|
// supposed to come round to each player at the same rate, and they don't if the
|
|
// button is rotating past chairs nobody is sitting in.
|
|
advanceButton(state) {
|
|
for (let step = 1; step <= state.seatCount; step++) {
|
|
const index = this.nextSeatIndex(state, state.dealerSeatIndex, step);
|
|
if (state.seats[index].inHand) {
|
|
state.dealerSeatIndex = index;
|
|
return;
|
|
}
|
|
}
|
|
// Nobody is dealt in; move it along anyway so it doesn't look stuck.
|
|
state.dealerSeatIndex = this.nextSeatIndex(state, state.dealerSeatIndex, 1);
|
|
},
|
|
|
|
// Seats dealt into this hand, in blind order starting from the seat after the
|
|
// button. Blinds follow the players, not the chairs — the seats immediately left
|
|
// of the button are empty often enough that counting chairs posts them to nobody.
|
|
liveOrderFromButton(state) {
|
|
const order = [];
|
|
for (let step = 1; step <= state.seatCount; step++) {
|
|
const index = this.nextSeatIndex(state, state.dealerSeatIndex, step);
|
|
if (state.seats[index].inHand) order.push(index);
|
|
}
|
|
return order;
|
|
},
|
|
|
|
// A button sitting on an empty chair is only cosmetic until you try to read
|
|
// position off it. On the very first hand of a fresh table it starts on seat 1
|
|
// whether or not anybody is sitting there, and a table that empties out around it
|
|
// leaves it stranded. Move it to a real player before a hand is dealt.
|
|
// Where the button will be when the hand is dealt — without moving it.
|
|
//
|
|
// Kept separate from ensureButtonOnPlayer because the deal preview has to be able
|
|
// to ask the question without answering it. It couldn't, and so the preview lied:
|
|
// with the button parked on an empty chair it promised one pair of blinds and
|
|
// dealing produced another, because beginHand relocates the button first and then
|
|
// works out who posts.
|
|
buttonSeatForDeal(state) {
|
|
const seated = state.seats[state.dealerSeatIndex];
|
|
if (seated && seated.inHand) return state.dealerSeatIndex;
|
|
for (let step = 0; step < state.seatCount; step++) {
|
|
const index = this.nextSeatIndex(state, state.dealerSeatIndex, step);
|
|
if (state.seats[index].inHand) return index;
|
|
}
|
|
return state.dealerSeatIndex;
|
|
},
|
|
|
|
ensureButtonOnPlayer(state) {
|
|
state.dealerSeatIndex = this.buttonSeatForDeal(state);
|
|
},
|
|
|
|
// A hand-picked blind is only usable if that seat is actually in the hand — a
|
|
// dealer who names a blind and then deals that player out shouldn't have the
|
|
// table quietly post to nobody.
|
|
validBlindOverride(state) {
|
|
const o = state.blindOverride;
|
|
if (!o) return null;
|
|
const ok = i => Number.isInteger(i) && i >= 0 && i < state.seatCount && state.seats[i] && state.seats[i].inHand;
|
|
if (!ok(o.sbIndex) || !ok(o.bbIndex) || o.sbIndex === o.bbIndex) return null;
|
|
return o;
|
|
},
|
|
|
|
// Who posts what. Heads-up is its own rule: with two players the button posts the
|
|
// small blind, which is the opposite of the usual order.
|
|
getBlindSeats(state) {
|
|
const live = this.liveOrderFromButton(state);
|
|
const dealerIndex = state.dealerSeatIndex;
|
|
|
|
// A blind the dealer named by hand wins over anything positional. Heads-up is
|
|
// still reported honestly so the betting order stays right — who posts what
|
|
// is the dealer's call, but two-handed play acting in reverse is a rule.
|
|
const override = this.validBlindOverride(state);
|
|
if (override) {
|
|
return {
|
|
dealerIndex,
|
|
sbIndex: override.sbIndex,
|
|
bbIndex: override.bbIndex,
|
|
headsUp: live.length === 2,
|
|
manual: true,
|
|
};
|
|
}
|
|
|
|
if (live.length < 2) {
|
|
return { dealerIndex, sbIndex: null, bbIndex: null, headsUp: false };
|
|
}
|
|
|
|
if (live.length === 2) {
|
|
const buttonIsPlaying = state.seats[dealerIndex].inHand;
|
|
const sbIndex = buttonIsPlaying ? dealerIndex : live[0];
|
|
const bbIndex = live.find(i => i !== sbIndex);
|
|
return { dealerIndex, sbIndex, bbIndex, headsUp: true };
|
|
}
|
|
|
|
return { dealerIndex, sbIndex: live[0], bbIndex: live[1], headsUp: false };
|
|
},
|
|
|
|
startNextHand(state) {
|
|
this.advanceButton(state);
|
|
// Cards aren't out and nothing is posted until the dealer says so.
|
|
state.bettingOpen = false;
|
|
state.actingSeatIndex = null;
|
|
state.street = "preflop";
|
|
state.currentBet = 0;
|
|
state.lastRaiseSize = 0;
|
|
// A hand-picked blind applies to the hand it was picked for. Carrying it into
|
|
// the next one would freeze the blinds on two players for the rest of the
|
|
// night, which is the opposite of what naming them once is for.
|
|
state.blindOverride = null;
|
|
state.seats.forEach(p => {
|
|
p.folded = false;
|
|
p.cards = ["", ""];
|
|
p.chipCounts = {};
|
|
p.amountOverride = null;
|
|
p.committed = 0;
|
|
p.hasActed = false;
|
|
});
|
|
// Anyone who joined mid-hand is a full player again from here.
|
|
this.ensureButtonOnPlayer(state);
|
|
},
|
|
|
|
// Appends a resolved-pot record to the running session log a dealer can point to
|
|
// for chip-count disputes, then advances the hand counter. `payouts` is
|
|
// [{ id, label, amount }, ...] — who got paid what out of this hand's pot(s).
|
|
//
|
|
// Must be called before startNextHand() clears the seats: the per-player `ledger`
|
|
// it stores alongside the payouts is what session standings are summed from later,
|
|
// and it needs this hand's contributions while they're still on the table.
|
|
recordHand(state, payouts) {
|
|
if (typeof state.handNumber !== "number") state.handNumber = 1;
|
|
if (!Array.isArray(state.handHistory)) state.handHistory = [];
|
|
|
|
const pot = payouts.reduce((sum, p) => sum + p.amount, 0);
|
|
const wonById = new Map(payouts.map(p => [p.id, p.amount]));
|
|
const ledger = state.seats
|
|
.map((player, index) => ({
|
|
id: player.id,
|
|
name: this.displayName(player, index),
|
|
in: this.computeContribution(player, state.chipLegend, state.basePotAmount),
|
|
won: wonById.get(player.id) || 0,
|
|
}))
|
|
.filter(row => row.in > 0 || row.won > 0);
|
|
|
|
state.handHistory.unshift({
|
|
handNumber: state.handNumber,
|
|
timestamp: Date.now(),
|
|
pot,
|
|
payouts,
|
|
ledger,
|
|
});
|
|
if (state.handHistory.length > this.MAX_HISTORY_ENTRIES) {
|
|
state.handHistory.length = this.MAX_HISTORY_ENTRIES;
|
|
}
|
|
state.handNumber += 1;
|
|
},
|
|
|
|
clearHistory(state) {
|
|
state.handHistory = [];
|
|
state.handNumber = 1;
|
|
},
|
|
|
|
// Running per-player totals for the whole session, so a dealer settling up at the
|
|
// end of the night reads one number per person instead of adding up every hand.
|
|
// Keyed by seat id, which survives renames and reseating; hands recorded before
|
|
// the ledger existed simply don't contribute to the totals.
|
|
sessionStandings(state) {
|
|
const totals = new Map();
|
|
|
|
(state.handHistory || []).forEach(entry => {
|
|
(entry.ledger || []).forEach(row => {
|
|
// History is newest-first, so the first name seen for an id is the
|
|
// most recent one the dealer typed.
|
|
const running = totals.get(row.id) || { id: row.id, name: row.name, in: 0, won: 0 };
|
|
running.in += row.in;
|
|
running.won += row.won;
|
|
totals.set(row.id, running);
|
|
});
|
|
});
|
|
|
|
// Cash bought in and chips currently in front don't come from the hand log at
|
|
// all, so they're read off the seat rather than summed — a rebuy isn't a hand.
|
|
const seatById = new Map(state.seats.map(p => [p.id, p]));
|
|
|
|
// Anyone who has money in the game belongs in the settle-up, whether or not
|
|
// they've been in a logged hand yet. Building the list from the hand log alone
|
|
// left a player who bought in and then sat through a few hands folding —
|
|
// or who just sat down — missing entirely, so the banker's own record didn't
|
|
// mention the cash they'd taken. They turn up at cash-out holding chips the
|
|
// list never accounted for.
|
|
state.seats.forEach((player, index) => {
|
|
if (totals.has(player.id)) return;
|
|
const hasMoneyInPlay =
|
|
(Number(player.buyIns) || 0) > 0 ||
|
|
(Number(player.stack) || 0) > 0 ||
|
|
(Number(player.amountOverride) || 0) > 0 ||
|
|
(Number(player.committed) || 0) > 0;
|
|
if (!hasMoneyInPlay) return;
|
|
totals.set(player.id, { id: player.id, name: this.displayName(player, index), in: 0, won: 0 });
|
|
});
|
|
|
|
return Array.from(totals.values())
|
|
.map(t => {
|
|
const seat = seatById.get(t.id);
|
|
const boughtIn = seat ? Money.round(Number(seat.buyIns) || 0) : 0;
|
|
const chipsNow = seat ? Money.round(Number(seat.stack) || 0) : 0;
|
|
// Chips already pushed into the hand in progress are still this
|
|
// player's money until it's won. Leaving them out makes the cash
|
|
// position disagree with the hand log for as long as a hand is open —
|
|
// and blinds are posted the instant the previous one settles, so that
|
|
// window is almost always.
|
|
const inFrontOfThem = seat ? (Number(seat.amountOverride) || 0) + (Number(seat.committed) || 0) : 0;
|
|
|
|
return {
|
|
...t,
|
|
in: Money.round(t.in),
|
|
won: Money.round(t.won),
|
|
net: Money.round(t.won - t.in),
|
|
boughtIn,
|
|
chipsNow,
|
|
cashPosition: Money.round(chipsNow + inFrontOfThem - boughtIn),
|
|
};
|
|
})
|
|
.sort((a, b) => b.net - a.net);
|
|
},
|
|
|
|
// The one check a banker actually needs, and the one nothing here did.
|
|
//
|
|
// Every chip on the table is a promise to pay cash for it later. If the chips in
|
|
// play add up to more than the cash that came in, that promise can't be kept —
|
|
// and the way you find out is the last two players cashing out and the box being
|
|
// empty. It's always someone taking chips without paying, a rebuy that never got
|
|
// recorded, or a miscount, and it's silent until it's too late to work out which.
|
|
//
|
|
// Chips in play counts everything a player could hand back: what's in front of
|
|
// them and what's in the middle waiting to be won.
|
|
bankReconciliation(state) {
|
|
const cashIn = state.seats.reduce((sum, p) => sum + Math.max(0, Number(p.buyIns) || 0), 0);
|
|
const chipsInPlay = state.seats.reduce(
|
|
(sum, p) =>
|
|
sum +
|
|
Math.max(0, Number(p.stack) || 0) +
|
|
Math.max(0, Number(p.committed) || 0) +
|
|
Math.max(0, Number(p.amountOverride) || 0),
|
|
0
|
|
);
|
|
|
|
const difference = Money.round(chipsInPlay - cashIn);
|
|
return {
|
|
cashIn: Money.round(cashIn),
|
|
chipsInPlay: Money.round(chipsInPlay),
|
|
difference,
|
|
// Half a cent of float dust isn't a discrepancy worth alarming anyone over.
|
|
balanced: Math.abs(difference) <= Money.EPSILON,
|
|
};
|
|
},
|
|
};
|
|
|
|
if (typeof module !== "undefined" && module.exports) {
|
|
module.exports = HoldemState;
|
|
}
|