Went looking at dealer procedure again and ended up checking the rule home games argue about longest. The tool had it wrong. 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 would need eight. Under TDA rule 44 that does not reopen the betting to anyone who has already acted; they owe the extra three or they fold. Seat 1 and seat 4 were both being handed another go. Two things were wrong in raiseTo. Any increase at all reset hasActed for every live player, so a short shove gave the whole table another chance to raise. And lastRaiseSize was overwritten with the short amount, so the next legal raise was computed off 3 instead of 8 and came out too small — a second, quieter error that only shows up when somebody then tries to raise. Both now key off whether the increase was actually a full raise. A player who had not yet acted keeps full rights, which falls out naturally because their hasActed is already false — that distinction is the part of the rule people get wrong in the other direction. Everyone still owes the difference: isStreetComplete already asks anyone with chips left to call, so the short all-in is answered without being re-raiseable. The rulings sheet now covers what the tool does for you It had three entries. A dealer challenged on a payout couldn't look up why the tool did what it did, even for the rules it enforces automatically. Added the short all-in rule above, uncalled chips coming back rather than being announced as a side pot, and the odd chip in a chopped pot going to the first player left of the button — all three are things this tool already decides silently, and all three are worth being able to point at when somebody objects. 10 tests (137 total): already-acted players aren't reopened but still owe the difference, a player yet to act keeps full rights, the minimum raise isn't shrunk, calling it ends the street, no chips are created, and — the other half of the rule — a full-sized all-in, an ordinary raise, a raise exactly matching the last one, and a full-sized opening bet all still reopen everyone. Validated through the real action bar: after the short shove, lastRaiseSize stays 8 and the player who had already called stays settled, while the two who hadn't acted keep their turn. The longer rulings sheet still fits a phone, scrolls, traps focus and closes on Escape. Sources: TDA rules via thelodgepokerclub.com/tda-rules/, pokernews.com/strategy/deal-with-it-tips-for-mistake-free-dealing-in-home-games-30231.htm Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
144 lines
6.0 KiB
JavaScript
144 lines
6.0 KiB
JavaScript
// 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");
|
|
});
|
|
});
|