// Who turns their hand over first. // // A call the dealer makes at every showdown that isn't a fold, and one nobody at the // table volunteers for — everyone would rather see a hand than show one. The tool knew // who last raised and who sits left of the button and said neither. const test = require("node:test"); const { describe } = require("node:test"); const assert = require("node:assert"); const { HoldemState, Betting, makeTable } = require("./load"); const NAMES = ["Ann", "Ben", "Cal", "Dee"]; const table = (dealerSeatIndex = 0, stack = 200) => { const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], stack, dealerSeatIndex }); NAMES.forEach((name, i) => { state.seats[i].name = name; }); Betting.beginHand(state); return state; }; const call = state => { const answer = Betting.showdownOrder(state); return answer && { who: state.seats[answer.index].name, reason: answer.reason }; }; // Everybody left just calls whatever is in front of them, then the street closes. const checkRound = state => { let guard = 0; while (state.actingSeatIndex !== null && guard++ < 12) { const index = state.actingSeatIndex; if (Betting.toCall(state, state.seats[index]) > 0) Betting.call(state, index); else Betting.check(state, index); } Betting.closeStreet(state); }; describe("a bet on the river", () => { test("the last player to raise shows first", () => { const state = table(0); // blinds Ben and Cal checkRound(state); // pre-flop checkRound(state); // flop checkRound(state); // turn Betting.raiseTo(state, state.actingSeatIndex, 20); const raiser = state.seats.findIndex(p => Betting.committed(p) === 20); while (state.actingSeatIndex !== null) Betting.call(state, state.actingSeatIndex); assert.deepEqual(call(state), { who: state.seats[raiser].name, reason: "aggressor" }); }); test("a re-raise moves it to the last one, not the first", () => { const state = table(0); checkRound(state); checkRound(state); checkRound(state); const first = state.actingSeatIndex; Betting.raiseTo(state, first, 20); const second = state.actingSeatIndex; Betting.raiseTo(state, second, 60); while (state.actingSeatIndex !== null) { const i = state.actingSeatIndex; if (Betting.toCall(state, state.seats[i]) > 0) Betting.call(state, i); else Betting.check(state, i); } assert.equal(call(state).who, state.seats[second].name); }); test("a short all-in still shows first, even though it doesn't reopen the betting", () => { // The rule about reopening is about who may raise. It says nothing about who // shows, and the player who shoved is still the last one who put money in. const state = table(0); checkRound(state); checkRound(state); checkRound(state); const bettor = state.actingSeatIndex; Betting.raiseTo(state, bettor, 40); const shortStack = state.actingSeatIndex; // Over the $40 but only by $15, where a full raise would need $40 more. Going // *under* the bet would be a short call, not a short raise, and wouldn't make // them the aggressor at all. state.seats[shortStack].stack = 55; Betting.allIn(state, shortStack); assert.equal(Betting.committed(state.seats[shortStack]), 55, "raised, if not by enough"); while (state.actingSeatIndex !== null) { const i = state.actingSeatIndex; if (Betting.toCall(state, state.seats[i]) > 0) Betting.call(state, i); else Betting.check(state, i); } assert.equal(call(state).who, state.seats[shortStack].name); }); }); describe("a river checked through", () => { test("the first live player left of the button shows first", () => { const state = table(0); // button Ann checkRound(state); checkRound(state); checkRound(state); checkRound(state); assert.deepEqual(call(state), { who: "Ben", reason: "leftOfButton" }); }); test("an earlier street's raiser doesn't carry over to the river", () => { // The rule is about the *final* round. Leading the flop and then checking the // river doesn't oblige anyone. const state = table(0); checkRound(state); // pre-flop Betting.raiseTo(state, state.actingSeatIndex, 20); // leads the flop while (state.actingSeatIndex !== null) Betting.call(state, state.actingSeatIndex); Betting.closeStreet(state); checkRound(state); // turn checkRound(state); // river assert.equal(call(state).reason, "leftOfButton"); }); test("it skips a folded seat rather than naming somebody who isn't in the hand", () => { const state = table(0); // button Ann, so Ben would be first checkRound(state); checkRound(state); checkRound(state); Betting.fold(state, 1); // Ben gets out on the river while (state.actingSeatIndex !== null) { const i = state.actingSeatIndex; if (Betting.toCall(state, state.seats[i]) > 0) Betting.call(state, i); else Betting.check(state, i); } Betting.closeStreet(state); assert.equal(call(state).who, "Cal", "the next one round who is still in"); }); }); describe("everyone all-in before the river", () => { test("the last player who could put money in shows first", () => { // There was no final round to lead, so "checked round" isn't a choice anybody // made and the seat left of the button hasn't earned the obligation. const state = table(0, 30); checkRound(state); // pre-flop const shover = state.actingSeatIndex; Betting.allIn(state, shover); while (state.actingSeatIndex !== null) Betting.call(state, state.actingSeatIndex); Betting.closeStreet(state); assert.equal(Betting.canStillBet(state), false, "nobody has chips left to bet"); assert.deepEqual(call(state), { who: state.seats[shover].name, reason: "allInAggressor" }); }); test("a hand where everyone got in for the blind and nobody raised falls to the button", () => { // Two-dollar stacks against a two-dollar blind: everyone is all-in by calling, // and calling is not aggression. Nobody chose to lead, so nobody is on the hook // and it goes back to the seat left of the button. const state = table(0, 2); checkRound(state); assert.equal(Betting.canStillBet(state), false, "nobody has a chip behind"); assert.equal(state.handAggressorIndex, null, "and nobody ever raised"); const answer = call(state); assert.ok(answer, "there is still a showdown to order"); assert.equal(answer.reason, "leftOfButton"); }); }); describe("when there is nothing to call", () => { test("one player left isn't a showdown", () => { const state = table(0); [1, 2, 3].forEach(i => Betting.fold(state, i)); assert.equal(Betting.showdownOrder(state), null); }); test("an aggressor who has since folded doesn't get named", () => { // Can't happen in play — you don't fold after raising unopposed — but the // index is a number on the state and a reset table could hold a stale one. const state = table(0); checkRound(state); checkRound(state); checkRound(state); checkRound(state); state.streetAggressorIndex = 1; state.seats[1].folded = true; const answer = call(state); assert.notEqual(answer.who, "Ben"); assert.equal(answer.reason, "leftOfButton"); }); test("an index pointing at no seat at all is ignored", () => { const state = table(0); checkRound(state); checkRound(state); checkRound(state); checkRound(state); state.streetAggressorIndex = 99; assert.equal(call(state).reason, "leftOfButton"); }); }); describe("the tracking resets when it should", () => { test("a new hand starts with nobody on the hook", () => { const state = table(0); checkRound(state); checkRound(state); checkRound(state); Betting.raiseTo(state, state.actingSeatIndex, 20); assert.notEqual(state.handAggressorIndex, null); HoldemState.startNextHand(state); Betting.beginHand(state); assert.equal(state.streetAggressorIndex, null); assert.equal(state.handAggressorIndex, null, "last night's raise isn't this hand's business"); }); test("posting a blind is not aggression", () => { const state = table(0); assert.equal(state.streetAggressorIndex, null); assert.equal(state.handAggressorIndex, null); }); });