Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d6e7e0b5a | ||
|
|
aeb02d023d |
@@ -104,15 +104,17 @@
|
||||
<summary>Session</summary>
|
||||
<div class="panelBody">
|
||||
<h3 class="panelSubhead">Standings</h3>
|
||||
<div id="standingsTable">
|
||||
<div class="standingsRow standingsHeader">
|
||||
<span>Player</span>
|
||||
<span>In</span>
|
||||
<span>Won</span>
|
||||
<span>Net</span>
|
||||
<span title="Chips in front of them, less the cash they handed over">Cash out</span>
|
||||
<div class="standingsTableWrap">
|
||||
<div id="standingsTable">
|
||||
<div class="standingsRow standingsHeader">
|
||||
<span>Player</span>
|
||||
<span class="standingsInHead">In</span>
|
||||
<span class="standingsWonHead">Won</span>
|
||||
<span>Net</span>
|
||||
<span title="Chips in front of them, less the cash they handed over">Cash out</span>
|
||||
</div>
|
||||
<div id="standingsList"></div>
|
||||
</div>
|
||||
<div id="standingsList"></div>
|
||||
</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>
|
||||
|
||||
@@ -313,6 +315,41 @@
|
||||
</p>
|
||||
</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">
|
||||
Follows the Tournament Directors Association rules, which most rooms and
|
||||
home games take their lead from.
|
||||
|
||||
@@ -1465,9 +1465,22 @@ button,
|
||||
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 {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto auto auto;
|
||||
grid-template-columns: minmax(4rem, 1fr) auto auto;
|
||||
gap: 0.5rem;
|
||||
align-items: baseline;
|
||||
padding: 0.3rem 0.1rem;
|
||||
@@ -1478,7 +1491,36 @@ button,
|
||||
.standingsRow > span:not(:first-child) {
|
||||
text-align: right;
|
||||
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
|
||||
|
||||
@@ -153,14 +153,33 @@ const Betting = {
|
||||
const delta = this.round(target - this.committed(player));
|
||||
const moved = this.putIn(state, index, delta);
|
||||
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;
|
||||
// A raise reopens the action: everyone else owes a response again.
|
||||
state.seats.forEach((p, i) => {
|
||||
if (i !== index && p.inHand && !p.folded) p.hasActed = false;
|
||||
});
|
||||
|
||||
if (isFullRaise) {
|
||||
state.lastRaiseSize = increase;
|
||||
// A real raise reopens the action: everyone else owes a response again.
|
||||
state.seats.forEach((p, i) => {
|
||||
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;
|
||||
|
||||
@@ -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