Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d6e7e0b5a | ||
|
|
aeb02d023d | ||
|
|
60d192a9ec | ||
|
|
cc6a1902bc |
@@ -104,16 +104,31 @@
|
|||||||
<summary>Session</summary>
|
<summary>Session</summary>
|
||||||
<div class="panelBody">
|
<div class="panelBody">
|
||||||
<h3 class="panelSubhead">Standings</h3>
|
<h3 class="panelSubhead">Standings</h3>
|
||||||
|
<div class="standingsTableWrap">
|
||||||
<div id="standingsTable">
|
<div id="standingsTable">
|
||||||
<div class="standingsRow standingsHeader">
|
<div class="standingsRow standingsHeader">
|
||||||
<span>Player</span>
|
<span>Player</span>
|
||||||
<span>In</span>
|
<span class="standingsInHead">In</span>
|
||||||
<span>Won</span>
|
<span class="standingsWonHead">Won</span>
|
||||||
<span>Net</span>
|
<span>Net</span>
|
||||||
|
<span title="Chips in front of them, less the cash they handed over">Cash out</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="standingsList"></div>
|
<div id="standingsList"></div>
|
||||||
</div>
|
</div>
|
||||||
<p id="standingsEmptyHint" class="fieldHint">Once you resolve a pot, everyone's running total for the night shows up here.</p>
|
</div>
|
||||||
|
<p id="standingsEmptyHint" class="fieldHint">Everyone who has bought in shows up here, with what they'd be owed if the night ended now.</p>
|
||||||
|
|
||||||
|
<div id="bankReconcile" class="bankReconcile" hidden>
|
||||||
|
<div class="bankReconcileRow">
|
||||||
|
<span>Cash taken</span>
|
||||||
|
<span id="bankCashIn"></span>
|
||||||
|
</div>
|
||||||
|
<div class="bankReconcileRow">
|
||||||
|
<span>Chips in play</span>
|
||||||
|
<span id="bankChipsInPlay"></span>
|
||||||
|
</div>
|
||||||
|
<p id="bankReconcileVerdict" class="bankReconcileVerdict"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3 class="panelSubhead">Hands</h3>
|
<h3 class="panelSubhead">Hands</h3>
|
||||||
<div id="handHistoryList"></div>
|
<div id="handHistoryList"></div>
|
||||||
@@ -225,6 +240,7 @@
|
|||||||
<span class="standingsIn"></span>
|
<span class="standingsIn"></span>
|
||||||
<span class="standingsWon"></span>
|
<span class="standingsWon"></span>
|
||||||
<span class="standingsNet"></span>
|
<span class="standingsNet"></span>
|
||||||
|
<span class="standingsCashOut"></span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -299,6 +315,41 @@
|
|||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="ruling">
|
||||||
|
<h3>A short all-in doesn't reopen the betting</h3>
|
||||||
|
<p>
|
||||||
|
Somebody shoving in less than a full raise — say $3 more when the last
|
||||||
|
raise was $8 — does not give the players behind them another go. Anyone
|
||||||
|
who has already acted owes the difference or folds; they don't get to
|
||||||
|
raise again because a short stack happened to land between them.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
A player who hadn't acted yet keeps full rights and may still raise, and
|
||||||
|
the minimum raise stays measured off the last <em>full</em> raise, not
|
||||||
|
off the short one. The action bar follows this for you.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="ruling">
|
||||||
|
<h3>Chips nobody called come back</h3>
|
||||||
|
<p>
|
||||||
|
Bet more than anyone was able to call and the excess is returned to you
|
||||||
|
before the showdown — it was never contested, so it isn't part of the
|
||||||
|
pot. The pot breakdown shows it as "uncalled" rather than pretending it's
|
||||||
|
a side pot you could win.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="ruling">
|
||||||
|
<h3>The odd chip in a split pot</h3>
|
||||||
|
<p>
|
||||||
|
When a chopped pot won't divide evenly, the odd chip goes to the first
|
||||||
|
player left of the button, not to whoever the dealer happened to tick
|
||||||
|
first. Pot payouts here order the winners that way, so the leftover lands
|
||||||
|
on the right seat every time.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
<p class="rulingsSource">
|
<p class="rulingsSource">
|
||||||
Follows the Tournament Directors Association rules, which most rooms and
|
Follows the Tournament Directors Association rules, which most rooms and
|
||||||
home games take their lead from.
|
home games take their lead from.
|
||||||
|
|||||||
@@ -1465,9 +1465,22 @@ button,
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The sidebar is about 290px wide, and four numeric columns at a 4.5rem minimum
|
||||||
|
plus gaps come to 320px — so the name column, being the only flexible one, was
|
||||||
|
collapsing to *zero* and player names disappeared entirely. Names were already
|
||||||
|
down to 50px with four columns; adding the cash-out column finished them off.
|
||||||
|
Measured at 360, 390 and 1280 alike, since the sidebar is narrow at all of them.
|
||||||
|
|
||||||
|
So the table earns its columns instead of assuming them. Narrow: the player and
|
||||||
|
the two numbers a banker settles up from. Wide enough: the per-hand detail as
|
||||||
|
well. Everything is in the CSV export either way. */
|
||||||
|
.standingsTableWrap {
|
||||||
|
container-type: inline-size;
|
||||||
|
}
|
||||||
|
|
||||||
.standingsRow {
|
.standingsRow {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) auto auto auto;
|
grid-template-columns: minmax(4rem, 1fr) auto auto;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
padding: 0.3rem 0.1rem;
|
padding: 0.3rem 0.1rem;
|
||||||
@@ -1478,7 +1491,78 @@ button,
|
|||||||
.standingsRow > span:not(:first-child) {
|
.standingsRow > span:not(:first-child) {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
min-width: 4.5rem;
|
min-width: 3.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.standingsRow > .standingsName {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The per-hand columns are detail: how the night's swings happened. They're the
|
||||||
|
first thing to go when there isn't room, because Net and Cash out are what the
|
||||||
|
banker is actually reading. */
|
||||||
|
.standingsIn,
|
||||||
|
.standingsWon,
|
||||||
|
.standingsInHead,
|
||||||
|
.standingsWonHead {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container (min-width: 30rem) {
|
||||||
|
.standingsRow {
|
||||||
|
grid-template-columns: minmax(5rem, 1fr) auto auto auto auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.standingsIn,
|
||||||
|
.standingsWon,
|
||||||
|
.standingsInHead,
|
||||||
|
.standingsWonHead {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The settle-up column is the one a banker reads while counting out cash, so it
|
||||||
|
carries the weight the per-hand columns don't. */
|
||||||
|
.standingsCashOut {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Bank reconciliation ----------
|
||||||
|
Cash taken versus chips on the table. Every chip is a promise to pay cash for it
|
||||||
|
later, and if the chips outrun the box that promise can't be kept — which a dealer
|
||||||
|
otherwise discovers when the last two players cash out and there's nothing left. */
|
||||||
|
.bankReconcile {
|
||||||
|
margin-top: 0.6rem;
|
||||||
|
padding: 0.55rem 0.6rem;
|
||||||
|
border: 1px solid #2f2f2f;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #151515;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bankReconcileRow {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.75rem;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: #b8b8b8;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
padding: 0.12rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bankReconcileVerdict {
|
||||||
|
margin: 0.4rem 0 0;
|
||||||
|
font-size: 0.74rem;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bankReconcileVerdict.bankBalanced {
|
||||||
|
color: #7fc99a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bankReconcileVerdict.bankOff {
|
||||||
|
color: #f0b46a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.standingsHeader {
|
.standingsHeader {
|
||||||
|
|||||||
@@ -153,15 +153,34 @@ const Betting = {
|
|||||||
const delta = this.round(target - this.committed(player));
|
const delta = this.round(target - this.committed(player));
|
||||||
const moved = this.putIn(state, index, delta);
|
const moved = this.putIn(state, index, delta);
|
||||||
const newBet = this.committed(player);
|
const newBet = this.committed(player);
|
||||||
|
const previousBet = state.currentBet || 0;
|
||||||
|
|
||||||
|
if (newBet > previousBet) {
|
||||||
|
const increase = this.round(newBet - previousBet);
|
||||||
|
// What a full raise would have had to be: at least as much again as the
|
||||||
|
// last one, or the big blind if there hasn't been a raise yet.
|
||||||
|
const fullRaise = this.round(state.lastRaiseSize || state.bigBlind || 0);
|
||||||
|
const isFullRaise = increase >= fullRaise - this.EPSILON_CHIPS;
|
||||||
|
|
||||||
if (newBet > (state.currentBet || 0)) {
|
|
||||||
state.lastRaiseSize = this.round(newBet - (state.currentBet || 0));
|
|
||||||
state.currentBet = newBet;
|
state.currentBet = newBet;
|
||||||
// A raise reopens the action: everyone else owes a response again.
|
|
||||||
|
if (isFullRaise) {
|
||||||
|
state.lastRaiseSize = increase;
|
||||||
|
// A real raise reopens the action: everyone else owes a response again.
|
||||||
state.seats.forEach((p, i) => {
|
state.seats.forEach((p, i) => {
|
||||||
if (i !== index && p.inHand && !p.folded) p.hasActed = false;
|
if (i !== index && p.inHand && !p.folded) p.hasActed = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// Otherwise this is a short all-in — somebody with less than a full raise
|
||||||
|
// left shoving it in. The most misread rule in poker: it does *not* reopen
|
||||||
|
// the betting to anyone who has already acted. They owe the difference or
|
||||||
|
// they fold; they don't get to raise again because a short stack happened
|
||||||
|
// to be between them. Players yet to act keep their full rights, which
|
||||||
|
// they do here because their hasActed is already false.
|
||||||
|
//
|
||||||
|
// lastRaiseSize deliberately isn't lowered either, or the next legal raise
|
||||||
|
// would be computed off the short amount and come out too small.
|
||||||
|
}
|
||||||
|
|
||||||
player.hasActed = true;
|
player.hasActed = true;
|
||||||
this.passAction(state, index);
|
this.passAction(state, index);
|
||||||
|
|||||||
@@ -318,15 +318,34 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
Render.renderAll();
|
Render.renderAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("nextHandBtn").addEventListener("click", () => {
|
document.getElementById("nextHandBtn").addEventListener("click", async () => {
|
||||||
|
// Moving on wipes every seat's contribution, so an unresolved pot is simply
|
||||||
|
// destroyed — measured at a four-handed table, pressing this with $8 in the
|
||||||
|
// middle took the table from 800 chips to 792, and the toast said nothing
|
||||||
|
// about it. Chips only ever leave this tool because a dealer meant them to.
|
||||||
|
const unresolved = HoldemState.unresolvedContributions(state);
|
||||||
|
|
||||||
|
if (unresolved > Betting.EPSILON_CHIPS) {
|
||||||
|
const ok = await Dialog.confirm({
|
||||||
|
title: `Throw away ${Money.format(unresolved)}?`,
|
||||||
|
message: "There's still money in the middle from this hand. Starting the next one takes it off the table without paying anybody — use Resolve Pot if somebody won it.",
|
||||||
|
confirmLabel: "Throw it away",
|
||||||
|
danger: true,
|
||||||
|
});
|
||||||
|
if (!ok) return;
|
||||||
|
}
|
||||||
|
|
||||||
// startNextHand moves the button on by one; say where it landed so the
|
// startNextHand moves the button on by one; say where it landed so the
|
||||||
// dealer can confirm the rotation without hunting for the badge.
|
// dealer can confirm the rotation without hunting for the badge.
|
||||||
const nextDealer = HoldemState.nextSeatIndex(state, state.dealerSeatIndex, 1);
|
const nextDealer = HoldemState.nextSeatIndex(state, state.dealerSeatIndex, 1);
|
||||||
const where = state.dedicatedDealer
|
const where = state.dedicatedDealer
|
||||||
? `small blind on seat ${HoldemState.nextSeatIndex(state, nextDealer, 1) + 1}`
|
? `small blind on seat ${HoldemState.nextSeatIndex(state, nextDealer, 1) + 1}`
|
||||||
: `button on seat ${nextDealer + 1}`;
|
: `button on seat ${nextDealer + 1}`;
|
||||||
|
const discarded = unresolved > Betting.EPSILON_CHIPS
|
||||||
|
? ` ${Money.format(unresolved)} taken off the table.`
|
||||||
|
: "";
|
||||||
|
|
||||||
runUndoable(`Next hand — ${where}. Blinds posted.`, () => {
|
runUndoable(`Next hand — ${where}. Blinds posted.${discarded}`, () => {
|
||||||
HoldemState.startNextHand(state);
|
HoldemState.startNextHand(state);
|
||||||
Betting.beginHand(state);
|
Betting.beginHand(state);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -272,11 +272,25 @@ const PotWizard = {
|
|||||||
});
|
});
|
||||||
actions.appendChild(applyBtn);
|
actions.appendChild(applyBtn);
|
||||||
|
|
||||||
const closeBtn = document.createElement("button");
|
// This button cancels — it does not pay anybody. Under a heading reading
|
||||||
closeBtn.className = "holdemBtn holdemBtnGhost";
|
// "Final Payouts" with an amount beside a name, "Close" reads like dismissing
|
||||||
closeBtn.textContent = "Close";
|
// something already done, and a dealer who taps it walks away believing the
|
||||||
closeBtn.addEventListener("click", () => this.close());
|
// pot has been pushed. It hasn't: the chips are still in the middle and the
|
||||||
actions.appendChild(closeBtn);
|
// hand isn't logged. So it says what it does.
|
||||||
|
const cancelBtn = document.createElement("button");
|
||||||
|
cancelBtn.className = "holdemBtn holdemBtnGhost";
|
||||||
|
cancelBtn.textContent = "Cancel — don't pay yet";
|
||||||
|
cancelBtn.addEventListener("click", () => this.close());
|
||||||
|
actions.appendChild(cancelBtn);
|
||||||
|
|
||||||
|
if (this.totals.size > 0) {
|
||||||
|
const note = document.createElement("p");
|
||||||
|
note.className = "fieldHint potWizardPendingNote";
|
||||||
|
note.textContent = "Nothing has been paid out yet. Start Next Hand pushes the pot and logs the hand.";
|
||||||
|
wrap.appendChild(actions);
|
||||||
|
wrap.appendChild(note);
|
||||||
|
return wrap;
|
||||||
|
}
|
||||||
|
|
||||||
wrap.appendChild(actions);
|
wrap.appendChild(actions);
|
||||||
return wrap;
|
return wrap;
|
||||||
|
|||||||
@@ -701,8 +701,46 @@ const Render = {
|
|||||||
netEl.classList.toggle("netUp", row.net > 0);
|
netEl.classList.toggle("netUp", row.net > 0);
|
||||||
netEl.classList.toggle("netDown", row.net < 0);
|
netEl.classList.toggle("netDown", row.net < 0);
|
||||||
|
|
||||||
|
// What they'd actually be handed if the night ended now: chips in front of
|
||||||
|
// them less the cash they gave the banker. This was computed and written to
|
||||||
|
// the CSV all along, but never shown to the person doing the paying out.
|
||||||
|
const cashEl = node.querySelector(".standingsCashOut");
|
||||||
|
cashEl.textContent = Money.formatSigned(row.cashPosition);
|
||||||
|
cashEl.classList.toggle("netUp", row.cashPosition > 0);
|
||||||
|
cashEl.classList.toggle("netDown", row.cashPosition < 0);
|
||||||
|
|
||||||
list.appendChild(node);
|
list.appendChild(node);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.renderBankReconciliation();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Does the cash box cover the chips on the table? Nothing asked this before, and
|
||||||
|
// it's the question that decides whether the last two players can be paid.
|
||||||
|
renderBankReconciliation() {
|
||||||
|
const host = document.getElementById("bankReconcile");
|
||||||
|
if (!host) return;
|
||||||
|
|
||||||
|
const bank = HoldemState.bankReconciliation(this.state);
|
||||||
|
// Nothing to reconcile before anybody has handed over money or taken a chip.
|
||||||
|
host.hidden = bank.cashIn === 0 && bank.chipsInPlay === 0;
|
||||||
|
if (host.hidden) return;
|
||||||
|
|
||||||
|
document.getElementById("bankCashIn").textContent = Money.format(bank.cashIn);
|
||||||
|
document.getElementById("bankChipsInPlay").textContent = Money.format(bank.chipsInPlay);
|
||||||
|
|
||||||
|
const verdict = document.getElementById("bankReconcileVerdict");
|
||||||
|
if (bank.balanced) {
|
||||||
|
verdict.textContent = "Balanced — the cash covers every chip on the table.";
|
||||||
|
} else if (bank.difference > 0) {
|
||||||
|
verdict.textContent =
|
||||||
|
`${Money.format(bank.difference)} more in chips than cash taken. Someone's rebuy probably wasn't recorded — the box will be short at cash-out.`;
|
||||||
|
} else {
|
||||||
|
verdict.textContent =
|
||||||
|
`${Money.format(Math.abs(bank.difference))} more cash than chips in play. Someone has paid for chips they haven't been given.`;
|
||||||
|
}
|
||||||
|
verdict.classList.toggle("bankBalanced", bank.balanced);
|
||||||
|
verdict.classList.toggle("bankOff", !bank.balanced);
|
||||||
},
|
},
|
||||||
|
|
||||||
// ---- Hand counter + history ----
|
// ---- Hand counter + history ----
|
||||||
|
|||||||
@@ -167,6 +167,38 @@ const HoldemState = {
|
|||||||
return !player.inHand;
|
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) {
|
totalPot(state) {
|
||||||
return state.seats.reduce(
|
return state.seats.reduce(
|
||||||
(sum, p) => sum + this.computeContribution(p, state.chipLegend, state.basePotAmount),
|
(sum, p) => sum + this.computeContribution(p, state.chipLegend, state.basePotAmount),
|
||||||
@@ -352,6 +384,23 @@ const HoldemState = {
|
|||||||
// all, so they're read off the seat rather than summed — a rebuy isn't a hand.
|
// 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]));
|
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())
|
return Array.from(totals.values())
|
||||||
.map(t => {
|
.map(t => {
|
||||||
const seat = seatById.get(t.id);
|
const seat = seatById.get(t.id);
|
||||||
@@ -376,6 +425,37 @@ const HoldemState = {
|
|||||||
})
|
})
|
||||||
.sort((a, b) => b.net - a.net);
|
.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) {
|
if (typeof module !== "undefined" && module.exports) {
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
// The banker's job, which is a different job from the dealer's: every chip on the
|
||||||
|
// table is a promise to pay cash for it later, and somebody has to be able to check
|
||||||
|
// that the promise can be kept before the last two players try to cash out.
|
||||||
|
const { test, describe } = require("node:test");
|
||||||
|
const assert = require("node:assert/strict");
|
||||||
|
const { Betting, HoldemState, Money, makeTable } = require("./load");
|
||||||
|
|
||||||
|
const seat = (state, index, name, cash, chips) => {
|
||||||
|
const p = state.seats[index];
|
||||||
|
p.name = name; p.inHand = true; p.buyIns = cash; p.stack = chips;
|
||||||
|
return p;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("everyone with money in the game is accounted for", () => {
|
||||||
|
test("a player who has bought in but played no logged hand still appears", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2], stack: 0 });
|
||||||
|
seat(state, 0, "Ann", 100, 100);
|
||||||
|
seat(state, 1, "Ben", 100, 100);
|
||||||
|
seat(state, 2, "Cal", 200, 200); // just sat down
|
||||||
|
|
||||||
|
state.seats[0].amountOverride = 10;
|
||||||
|
state.seats[1].amountOverride = 10;
|
||||||
|
HoldemState.recordHand(state, [{ id: state.seats[0].id, label: "Ann", amount: 20 }]);
|
||||||
|
state.seats[0].amountOverride = null;
|
||||||
|
state.seats[1].amountOverride = null;
|
||||||
|
|
||||||
|
const names = HoldemState.sessionStandings(state).map(r => r.name);
|
||||||
|
assert.ok(names.includes("Cal"), `Cal handed over $200 and is missing: ${names.join(", ")}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("their row reads as no hands played, not as a loss", () => {
|
||||||
|
const state = makeTable({ seatCount: 4, seated: [0, 1], stack: 0 });
|
||||||
|
seat(state, 0, "Ann", 100, 100);
|
||||||
|
seat(state, 1, "Ben", 200, 200);
|
||||||
|
|
||||||
|
const cal = HoldemState.sessionStandings(state).find(r => r.name === "Ben");
|
||||||
|
assert.equal(cal.in, 0);
|
||||||
|
assert.equal(cal.won, 0);
|
||||||
|
assert.equal(cal.net, 0);
|
||||||
|
assert.equal(cal.boughtIn, 200);
|
||||||
|
assert.equal(cal.cashPosition, 0, "level: they've bought chips and still hold them");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("an empty chair is not dragged into the settle-up", () => {
|
||||||
|
const state = makeTable({ seatCount: 9, seated: [0], stack: 0 });
|
||||||
|
seat(state, 0, "Ann", 100, 100);
|
||||||
|
const rows = HoldemState.sessionStandings(state);
|
||||||
|
assert.equal(rows.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("somebody who has cashed out completely still shows what they took", () => {
|
||||||
|
const state = makeTable({ seatCount: 4, seated: [0, 1], stack: 0 });
|
||||||
|
seat(state, 0, "Ann", 100, 0); // handed every chip back
|
||||||
|
seat(state, 1, "Ben", 100, 200);
|
||||||
|
const ann = HoldemState.sessionStandings(state).find(r => r.name === "Ann");
|
||||||
|
assert.equal(ann.cashPosition, -100, "down the whole buy-in");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("does the cash box cover the chips on the table", () => {
|
||||||
|
test("a straight game balances", () => {
|
||||||
|
const state = makeTable({ seatCount: 4, seated: [0, 1, 2], stack: 0 });
|
||||||
|
seat(state, 0, "Ann", 100, 60);
|
||||||
|
seat(state, 1, "Ben", 100, 140);
|
||||||
|
seat(state, 2, "Cal", 100, 100);
|
||||||
|
|
||||||
|
const bank = HoldemState.bankReconciliation(state);
|
||||||
|
assert.equal(bank.cashIn, 300);
|
||||||
|
assert.equal(bank.chipsInPlay, 300);
|
||||||
|
assert.equal(bank.difference, 0);
|
||||||
|
assert.equal(bank.balanced, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("chips outrunning the cash box is caught before cash-out", () => {
|
||||||
|
// The 2am failure: the last players out can't be paid.
|
||||||
|
const state = makeTable({ seatCount: 4, seated: [0, 1, 2], stack: 0 });
|
||||||
|
seat(state, 0, "Ann", 100, 100);
|
||||||
|
seat(state, 1, "Ben", 100, 100);
|
||||||
|
seat(state, 2, "Cal", 100, 260); // took chips for a rebuy nobody wrote down
|
||||||
|
|
||||||
|
const bank = HoldemState.bankReconciliation(state);
|
||||||
|
assert.equal(bank.cashIn, 300);
|
||||||
|
assert.equal(bank.chipsInPlay, 460);
|
||||||
|
assert.equal(bank.difference, 160);
|
||||||
|
assert.equal(bank.balanced, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("cash taken for chips never handed over is caught too", () => {
|
||||||
|
const state = makeTable({ seatCount: 4, seated: [0, 1], stack: 0 });
|
||||||
|
seat(state, 0, "Ann", 200, 100); // paid 200, only got 100 in chips
|
||||||
|
seat(state, 1, "Ben", 100, 100);
|
||||||
|
|
||||||
|
const bank = HoldemState.bankReconciliation(state);
|
||||||
|
assert.equal(bank.difference, -100);
|
||||||
|
assert.equal(bank.balanced, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("chips in the middle still count as chips in play", () => {
|
||||||
|
// Money in the pot is somebody's; it just hasn't been won yet. Leaving it out
|
||||||
|
// would report a shortfall every time a hand was open — which is nearly always.
|
||||||
|
const state = makeTable({ seatCount: 4, seated: [0, 1, 2], dealerSeatIndex: 0 });
|
||||||
|
state.seats.forEach(p => { if (p.inHand) p.buyIns = 100; });
|
||||||
|
|
||||||
|
const before = HoldemState.bankReconciliation(state);
|
||||||
|
assert.equal(before.balanced, true);
|
||||||
|
|
||||||
|
Betting.beginHand(state); // blinds go in
|
||||||
|
Betting.raiseTo(state, 0, 25); // and a raise
|
||||||
|
Betting.sweepToHandTotals(state); // banked into the pot
|
||||||
|
|
||||||
|
const during = HoldemState.bankReconciliation(state);
|
||||||
|
assert.equal(during.balanced, true, "an open pot is not a discrepancy");
|
||||||
|
assert.equal(during.chipsInPlay, before.chipsInPlay);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a whole session of play never knocks the books out", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], dealerSeatIndex: 0 });
|
||||||
|
state.seats.forEach(p => { if (p.inHand) p.buyIns = 100; });
|
||||||
|
|
||||||
|
for (let hand = 0; hand < 6; hand++) {
|
||||||
|
Betting.beginHand(state);
|
||||||
|
Betting.liveSeats(state).forEach(s => {
|
||||||
|
if (Betting.toCall(state, s.player) > 0) Betting.call(state, s.index);
|
||||||
|
});
|
||||||
|
Betting.sweepToHandTotals(state);
|
||||||
|
|
||||||
|
const pot = HoldemState.totalPot(state);
|
||||||
|
const winner = Betting.liveSeats(state)[0];
|
||||||
|
const payouts = [{ id: winner.player.id, label: "w", amount: pot }];
|
||||||
|
HoldemState.recordHand(state, payouts);
|
||||||
|
Betting.creditWinnings(state, payouts);
|
||||||
|
HoldemState.startNextHand(state);
|
||||||
|
|
||||||
|
assert.equal(HoldemState.bankReconciliation(state).balanced, true, `books off after hand ${hand + 1}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a rebuy keeps the books straight, a chip grab doesn't", () => {
|
||||||
|
const state = makeTable({ seatCount: 4, seated: [0, 1], stack: 0 });
|
||||||
|
seat(state, 0, "Ann", 100, 100);
|
||||||
|
seat(state, 1, "Ben", 100, 100);
|
||||||
|
|
||||||
|
// A proper rebuy: cash to the banker, chips to the player.
|
||||||
|
state.seats[0].buyIns += 100;
|
||||||
|
state.seats[0].stack += 100;
|
||||||
|
assert.equal(HoldemState.bankReconciliation(state).balanced, true);
|
||||||
|
|
||||||
|
// Chips taken without paying.
|
||||||
|
state.seats[1].stack += 50;
|
||||||
|
const bank = HoldemState.bankReconciliation(state);
|
||||||
|
assert.equal(bank.balanced, false);
|
||||||
|
assert.equal(bank.difference, 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("nothing to reconcile before anybody has bought in", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [], stack: 0 });
|
||||||
|
const bank = HoldemState.bankReconciliation(state);
|
||||||
|
assert.equal(bank.cashIn, 0);
|
||||||
|
assert.equal(bank.chipsInPlay, 0);
|
||||||
|
assert.equal(bank.balanced, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("float dust is not a discrepancy", () => {
|
||||||
|
const state = makeTable({ seatCount: 4, seated: [0, 1], stack: 0 });
|
||||||
|
seat(state, 0, "Ann", 0.1 + 0.2, 0.3);
|
||||||
|
seat(state, 1, "Ben", 10, 10);
|
||||||
|
assert.equal(HoldemState.bankReconciliation(state).balanced, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
// Chips only ever leave this tool because a dealer meant them to. Starting the next
|
||||||
|
// hand clears every seat's contribution, so an unresolved pot is simply destroyed —
|
||||||
|
// the UI now asks first, and the number it quotes has to be exactly what goes.
|
||||||
|
const { test, describe } = require("node:test");
|
||||||
|
const assert = require("node:assert/strict");
|
||||||
|
const { Betting, HoldemState, Money, makeTable, chipsOnTable } = require("./load");
|
||||||
|
|
||||||
|
// The same measure the confirmation uses.
|
||||||
|
const unresolvedPot = state => HoldemState.unresolvedContributions(state);
|
||||||
|
|
||||||
|
describe("what starting the next hand destroys", () => {
|
||||||
|
test("the quoted amount is exactly what leaves the table", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], dealerSeatIndex: 0 });
|
||||||
|
Betting.beginHand(state);
|
||||||
|
Betting.call(state, 3); Betting.call(state, 0); Betting.call(state, 1); Betting.check(state, 2);
|
||||||
|
Betting.sweepToHandTotals(state);
|
||||||
|
|
||||||
|
const before = chipsOnTable(state);
|
||||||
|
const quoted = unresolvedPot(state);
|
||||||
|
assert.ok(quoted > 0, "there is a pot to warn about");
|
||||||
|
|
||||||
|
HoldemState.startNextHand(state);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
Money.toCents(chipsOnTable(state)),
|
||||||
|
Money.toCents(before - quoted),
|
||||||
|
"the dealer must be told the real number, not an approximation"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("chips still in front of players on the current street are counted too", () => {
|
||||||
|
const state = makeTable({ seatCount: 4, seated: [0, 1, 2], dealerSeatIndex: 0 });
|
||||||
|
Betting.beginHand(state); // blinds are `committed`, not yet banked
|
||||||
|
Betting.raiseTo(state, 0, 25); // and so is a raise mid-street
|
||||||
|
|
||||||
|
const before = chipsOnTable(state);
|
||||||
|
const quoted = unresolvedPot(state);
|
||||||
|
HoldemState.startNextHand(state);
|
||||||
|
|
||||||
|
assert.equal(Money.toCents(chipsOnTable(state)), Money.toCents(before - quoted));
|
||||||
|
assert.ok(quoted >= 25, `a live raise has to be included; quoted ${quoted}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a hand that was paid out properly leaves nothing to warn about", () => {
|
||||||
|
const state = makeTable({ seatCount: 4, seated: [0, 1, 2], dealerSeatIndex: 0 });
|
||||||
|
Betting.beginHand(state);
|
||||||
|
Betting.call(state, 0); Betting.call(state, 1); Betting.check(state, 2);
|
||||||
|
Betting.sweepToHandTotals(state);
|
||||||
|
|
||||||
|
const pot = HoldemState.totalPot(state);
|
||||||
|
const payouts = [{ id: state.seats[0].id, label: "Ann", amount: pot }];
|
||||||
|
HoldemState.recordHand(state, payouts);
|
||||||
|
Betting.creditWinnings(state, payouts);
|
||||||
|
HoldemState.startNextHand(state);
|
||||||
|
|
||||||
|
assert.equal(unresolvedPot(state), 0, "nothing left over after a settled hand");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("settling then dealing on conserves every chip", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], dealerSeatIndex: 0 });
|
||||||
|
const start = chipsOnTable(state);
|
||||||
|
|
||||||
|
for (let hand = 0; hand < 5; hand++) {
|
||||||
|
Betting.beginHand(state);
|
||||||
|
const live = Betting.liveSeats(state);
|
||||||
|
live.forEach(s => { if (Betting.toCall(state, s.player) > 0) Betting.call(state, s.index); });
|
||||||
|
Betting.sweepToHandTotals(state);
|
||||||
|
|
||||||
|
const pot = HoldemState.totalPot(state);
|
||||||
|
const winner = Betting.liveSeats(state)[0];
|
||||||
|
const payouts = [{ id: winner.player.id, label: "w", amount: pot }];
|
||||||
|
HoldemState.recordHand(state, payouts);
|
||||||
|
Betting.creditWinnings(state, payouts);
|
||||||
|
HoldemState.startNextHand(state);
|
||||||
|
|
||||||
|
assert.equal(chipsOnTable(state), start, `chips drifted on hand ${hand + 1}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("an empty table has no phantom pot to warn about", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [] });
|
||||||
|
assert.equal(unresolvedPot(state), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a standing ante is never warned about, because it isn't lost", () => {
|
||||||
|
// The table default is a stand-in for "everyone puts the same in every hand",
|
||||||
|
// used by a dealer not tracking stacks at all. It re-applies to the next hand
|
||||||
|
// rather than going anywhere, so counting it would nag this dealer before
|
||||||
|
// every single hand — which is why the warning measures the fields
|
||||||
|
// startNextHand actually clears rather than the pot badge.
|
||||||
|
const state = makeTable({ seatCount: 9, seated: [0, 1] });
|
||||||
|
state.basePotAmount = 25;
|
||||||
|
state.seats.forEach(p => { p.amountOverride = null; });
|
||||||
|
|
||||||
|
// The base pot carries into the next hand, so nothing is being thrown away
|
||||||
|
// and the dealer must not be asked to confirm anything.
|
||||||
|
assert.equal(HoldemState.totalPot(state), 50, "the badge still shows a pot");
|
||||||
|
assert.equal(unresolvedPot(state), 0, "but none of it is at risk, so no warning");
|
||||||
|
|
||||||
|
HoldemState.startNextHand(state);
|
||||||
|
assert.equal(HoldemState.totalPot(state), 50, "the standing ante applies to the new hand too");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
// When does a bet reopen the action? The most misread rule in poker, and the one a
|
||||||
|
// home game argues about longest: a player shoving in less than a full raise does not
|
||||||
|
// give everybody behind them another go.
|
||||||
|
//
|
||||||
|
// TDA Rule 44 — an all-in wager of less than a full raise does not reopen the betting
|
||||||
|
// to a player who has already acted and is not facing a full raise.
|
||||||
|
const { test, describe } = require("node:test");
|
||||||
|
const assert = require("node:assert/strict");
|
||||||
|
const { Betting, makeTable, chipsOnTable } = require("./load");
|
||||||
|
|
||||||
|
const actedFlags = state => state.seats
|
||||||
|
.map((p, i) => ({ seat: i, acted: p.hasActed, live: p.inHand && !p.folded }))
|
||||||
|
.filter(s => s.live);
|
||||||
|
|
||||||
|
describe("a short all-in does not reopen the betting", () => {
|
||||||
|
// Blinds 1/2. Seat 4 raises to 10 (a full raise of 8). Seat 1 calls.
|
||||||
|
// Seat 2 is all-in for 13 — three more, where a full raise needs eight.
|
||||||
|
const shortAllInTable = () => {
|
||||||
|
const state = makeTable({ seatCount: 4, seated: [0, 1, 2, 3], stack: 100, dealerSeatIndex: 0 });
|
||||||
|
state.seats[1].stack = 13;
|
||||||
|
Betting.beginHand(state);
|
||||||
|
Betting.raiseTo(state, 3, 10);
|
||||||
|
Betting.call(state, 0);
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
test("players who already acted are not asked to act again", () => {
|
||||||
|
const state = shortAllInTable();
|
||||||
|
assert.equal(state.seats[0].hasActed, true, "seat 1 has called");
|
||||||
|
assert.equal(state.seats[3].hasActed, true, "seat 4 has raised");
|
||||||
|
|
||||||
|
Betting.allIn(state, 1);
|
||||||
|
|
||||||
|
assert.equal(state.seats[0].hasActed, true, "seat 1 must not be reopened");
|
||||||
|
assert.equal(state.seats[3].hasActed, true, "seat 4 must not be reopened");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("but they do still owe the difference", () => {
|
||||||
|
const state = shortAllInTable();
|
||||||
|
Betting.allIn(state, 1);
|
||||||
|
|
||||||
|
assert.equal(Betting.toCall(state, state.seats[0]), 3, "seat 1 owes the extra 3");
|
||||||
|
assert.equal(Betting.isStreetComplete(state), false, "so the street isn't over");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a player who had not acted yet keeps full rights", () => {
|
||||||
|
const state = shortAllInTable();
|
||||||
|
assert.equal(state.seats[2].hasActed, false, "seat 3 hasn't acted");
|
||||||
|
Betting.allIn(state, 1);
|
||||||
|
assert.equal(state.seats[2].hasActed, false, "and still hasn't — they may still raise");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the minimum raise is not shrunk by the short all-in", () => {
|
||||||
|
const state = shortAllInTable();
|
||||||
|
assert.equal(state.lastRaiseSize, 8);
|
||||||
|
|
||||||
|
Betting.allIn(state, 1);
|
||||||
|
|
||||||
|
assert.equal(state.lastRaiseSize, 8, "still the last *full* raise");
|
||||||
|
assert.equal(Betting.minRaiseTo(state), 21, "13 to call plus a full raise of 8");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("calling the short all-in ends the street", () => {
|
||||||
|
const state = shortAllInTable();
|
||||||
|
Betting.allIn(state, 1);
|
||||||
|
Betting.call(state, 2); // seat 3, who had full rights, just calls
|
||||||
|
Betting.call(state, 3);
|
||||||
|
Betting.call(state, 0);
|
||||||
|
assert.equal(Betting.isStreetComplete(state), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("no chips are created or lost by any of it", () => {
|
||||||
|
const state = makeTable({ seatCount: 4, seated: [0, 1, 2, 3], stack: 100, dealerSeatIndex: 0 });
|
||||||
|
state.seats[1].stack = 13;
|
||||||
|
const start = chipsOnTable(state);
|
||||||
|
Betting.beginHand(state);
|
||||||
|
Betting.raiseTo(state, 3, 10);
|
||||||
|
Betting.call(state, 0);
|
||||||
|
Betting.allIn(state, 1);
|
||||||
|
Betting.call(state, 2);
|
||||||
|
Betting.call(state, 3);
|
||||||
|
Betting.call(state, 0);
|
||||||
|
assert.equal(chipsOnTable(state), start);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("a full raise still reopens the betting", () => {
|
||||||
|
test("an all-in that is a full raise or more reopens everyone", () => {
|
||||||
|
const state = makeTable({ seatCount: 4, seated: [0, 1, 2, 3], stack: 100, dealerSeatIndex: 0 });
|
||||||
|
state.seats[1].stack = 40;
|
||||||
|
Betting.beginHand(state);
|
||||||
|
Betting.raiseTo(state, 3, 10);
|
||||||
|
Betting.call(state, 0);
|
||||||
|
|
||||||
|
Betting.allIn(state, 1); // to 40: an increase of 30, well over the 8 needed
|
||||||
|
|
||||||
|
assert.equal(state.seats[0].hasActed, false, "seat 1 gets another go");
|
||||||
|
assert.equal(state.seats[3].hasActed, false, "and so does seat 4");
|
||||||
|
assert.equal(state.lastRaiseSize, 30);
|
||||||
|
assert.equal(Betting.minRaiseTo(state), 70);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("an ordinary raise reopens everyone", () => {
|
||||||
|
const state = makeTable({ seatCount: 4, seated: [0, 1, 2, 3], dealerSeatIndex: 0 });
|
||||||
|
Betting.beginHand(state);
|
||||||
|
Betting.call(state, 3);
|
||||||
|
Betting.call(state, 0);
|
||||||
|
assert.equal(state.seats[3].hasActed, true);
|
||||||
|
|
||||||
|
Betting.raiseTo(state, 1, 10);
|
||||||
|
|
||||||
|
assert.equal(state.seats[3].hasActed, false);
|
||||||
|
assert.equal(state.seats[0].hasActed, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a raise exactly the size of the last one counts as full", () => {
|
||||||
|
const state = makeTable({ seatCount: 4, seated: [0, 1, 2, 3], stack: 100, dealerSeatIndex: 0 });
|
||||||
|
Betting.beginHand(state);
|
||||||
|
Betting.raiseTo(state, 3, 10); // raise of 8
|
||||||
|
Betting.call(state, 0);
|
||||||
|
|
||||||
|
Betting.raiseTo(state, 1, 18); // exactly 8 more — a legal minimum raise
|
||||||
|
|
||||||
|
assert.equal(state.seats[0].hasActed, false, "a minimum raise still reopens");
|
||||||
|
assert.equal(state.lastRaiseSize, 8);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the first raise of a street is measured against the big blind", () => {
|
||||||
|
const state = makeTable({ seatCount: 4, seated: [0, 1, 2, 3], stack: 100, dealerSeatIndex: 0 });
|
||||||
|
Betting.beginHand(state);
|
||||||
|
Betting.call(state, 3);
|
||||||
|
Betting.call(state, 0);
|
||||||
|
Betting.call(state, 1);
|
||||||
|
Betting.check(state, 2);
|
||||||
|
Betting.closeStreet(state); // to the flop, no bet yet
|
||||||
|
|
||||||
|
const first = Betting.nextToAct(state, state.dealerSeatIndex);
|
||||||
|
Betting.raiseTo(state, first, 2); // a bet of exactly one big blind
|
||||||
|
|
||||||
|
const others = state.seats.filter((p, i) => p.inHand && !p.folded && i !== first);
|
||||||
|
assert.ok(others.every(p => p.hasActed === false), "a full-sized opening bet reopens everyone");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user