Files
JoshHeaps.Net/tests/holdem/reopeningBetting.test.js
jheapsandClaude Opus 5 aeb02d023d A short all-in doesn't reopen the betting
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
2026-09-01 07:52:08 -06:00

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");
});
});