// The house's cut. // // A game that rakes has chips leaving the table every hand, and the bank check used to // call them missing: buy-ins in, chips on the felt, and a dealer told somebody had been // paid for chips they never got. The cut is the one thing here that is supposed to go // and not come back. const test = require("node:test"); const { describe } = require("node:test"); const assert = require("node:assert"); const { Money, HoldemState, Betting, makeTable, chipsOnTable } = require("./load"); const raking = (rake, extra = {}) => { const state = makeTable({ seatCount: 6, stack: 200, ...extra }); state.rake = { percent: 0, cap: null, noFlopNoDrop: true, ...rake }; return state; }; describe("what the house takes", () => { test("nothing, unless somebody says otherwise", () => { const state = makeTable({ seatCount: 6 }); assert.equal(state.rake.percent, 0); assert.equal(HoldemState.rakeOn(state, 100), 0); }); test("a percentage of the pot", () => { assert.equal(HoldemState.rakeOn(raking({ percent: 5 }), 100), 5); assert.equal(HoldemState.rakeOn(raking({ percent: 10 }), 60), 6); }); test("no more than the cap", () => { const state = raking({ percent: 5, cap: 5 }); assert.equal(HoldemState.rakeOn(state, 100), 5); assert.equal(HoldemState.rakeOn(state, 400), 5, "still five on a pot four times the size"); assert.equal(HoldemState.rakeOn(state, 40), 2, "under the cap it is just the percentage"); }); test("no flop, no drop", () => { const state = raking({ percent: 5 }); assert.equal(HoldemState.rakeOn(state, 100, false), 0, "hand ended before the flop"); assert.equal(HoldemState.rakeOn(state, 100, true), 5); }); test("unless the house rakes every pot", () => { const state = raking({ percent: 5, noFlopNoDrop: false }); assert.equal(HoldemState.rakeOn(state, 100, false), 5); }); test("nothing it can't make out of the table's chips, rounded the players' way", () => { // 5% of $30 is $1.50, and this table's smallest chip is a dollar. const state = raking({ percent: 5 }); assert.equal(HoldemState.rakeOn(state, 30), 1); // 5% of $10 is 50c — under a chip, so the house takes nothing at all. assert.equal(HoldemState.rakeOn(state, 10), 0); }); test("never more than the pot, whatever gets typed in", () => { assert.equal(HoldemState.rakeOn(raking({ percent: 100 }), 20), 20); assert.equal(HoldemState.rakeOn(raking({ percent: 5, cap: 500 }), 20), 1); }); test("nothing out of an empty pot", () => { assert.equal(HoldemState.rakeOn(raking({ percent: 5 }), 0), 0); }); }); describe("taking it out of the pot", () => { const winner = amount => [{ id: "a", label: "Ann", amount }]; test("one winner just gets the pot less the cut", () => { const { payouts, rake } = HoldemState.applyRake(winner(100), 5); assert.equal(rake, 5); assert.deepEqual(payouts.map(p => p.amount), [95]); }); test("a split pot shares what's left, in proportion", () => { const { payouts } = HoldemState.applyRake( [{ id: "a", label: "Ann", amount: 60 }, { id: "b", label: "Ben", amount: 40 }], 10); assert.deepEqual(payouts.map(p => p.amount), [54, 36]); }); test("the rounding leftovers go back to the players, not to the house", () => { // $100 split three ways less $7: 93 doesn't divide evenly, and every cent of // it has to land on somebody. const { payouts, rake } = HoldemState.applyRake([ { id: "a", label: "Ann", amount: 34 }, { id: "b", label: "Ben", amount: 33 }, { id: "c", label: "Cal", amount: 33 }, ], 7); assert.equal(rake, 7); assert.equal(Money.round(payouts.reduce((sum, p) => sum + p.amount, 0)), 93); }); test("what goes out plus what the house keeps is what was in the middle", () => { [[100, 5], [37.5, 1.25], [1000, 0], [9, 9]].forEach(([pot, rake]) => { const result = HoldemState.applyRake(winner(pot), rake); assert.equal(Money.round(result.payouts[0].amount + result.rake), pot, `${pot} less ${rake}`); }); }); test("asking for more than the pot takes the pot, not more", () => { const { payouts, rake } = HoldemState.applyRake(winner(20), 50); assert.equal(rake, 20); assert.deepEqual(payouts.map(p => p.amount), [0]); }); test("no rake, no change — the same payouts come back", () => { const { payouts, rake } = HoldemState.applyRake(winner(80), 0); assert.equal(rake, 0); assert.deepEqual(payouts.map(p => p.amount), [80]); }); }); describe("what the box is checked against", () => { test("raked chips are gone but not missing", () => { const state = raking({ percent: 5 }); state.seats.forEach(p => { p.buyIns = 200; }); const before = HoldemState.bankReconciliation(state); assert.equal(before.balanced, true); // The house takes $10 out of the middle: somebody's stack is $10 lighter. state.seats[0].stack = 190; state.rakeTaken = 10; const after = HoldemState.bankReconciliation(state); assert.equal(after.raked, 10); assert.equal(after.balanced, true, "the check still balances"); }); test("and are told apart from the ones that went astray", () => { const state = raking({ percent: 5 }); state.seats.forEach(p => { p.buyIns = 200; }); state.seats[0].stack = 180; state.rakeTaken = 10; const check = HoldemState.bankReconciliation(state); assert.equal(check.raked, 10); assert.equal(check.difference, -10, "ten dollars really are missing"); assert.equal(check.balanced, false); }); test("a table that has never raked reads exactly as it did before", () => { const state = makeTable({ seatCount: 6 }); state.seats.forEach(p => { p.buyIns = 100; p.stack = 100; }); const check = HoldemState.bankReconciliation(state); assert.equal(check.raked, 0); assert.equal(check.balanced, true); }); }); describe("the record of it", () => { test("the hand log keeps the pot as the table saw it, and the cut beside it", () => { const state = raking({ percent: 5 }); HoldemState.recordHand(state, [{ id: state.seats[0].id, label: "P1", amount: 95 }], 5); const [entry] = state.handHistory; assert.equal(entry.pot, 100, "the pot in the middle"); assert.equal(entry.rake, 5); assert.equal(entry.payouts[0].amount, 95, "what the winner actually took"); }); test("a hand with no rake records a zero rather than nothing", () => { const state = makeTable({ seatCount: 6 }); HoldemState.recordHand(state, [{ id: state.seats[0].id, label: "P1", amount: 40 }]); assert.equal(state.handHistory[0].rake, 0); assert.equal(state.handHistory[0].pot, 40); }); test("a table saved before the house took anything takes nothing", () => { const parsed = JSON.parse(JSON.stringify(makeTable({ seatCount: 6 }))); delete parsed.rake; delete parsed.rakeTaken; const migrated = HoldemState.migrate(parsed); assert.equal(migrated.rake.percent, 0); assert.equal(migrated.rake.noFlopNoDrop, true); assert.equal(migrated.rakeTaken, 0); }); test("nonsense in a saved rake is cleaned up rather than trusted", () => { const parsed = JSON.parse(JSON.stringify(makeTable({ seatCount: 6 }))); parsed.rake = { percent: -5, cap: -1, noFlopNoDrop: false }; parsed.rakeTaken = -20; const migrated = HoldemState.migrate(parsed); assert.equal(migrated.rake.percent, 0); assert.equal(migrated.rake.cap, null); assert.equal(migrated.rake.noFlopNoDrop, false, "that one is a real choice"); assert.equal(migrated.rakeTaken, 0); }); }); describe("the file the dealer hands over", () => { const { HandHistoryExport } = require("./load"); test("has a column for what the house took", () => { const state = raking({ percent: 5 }); HoldemState.recordHand(state, [{ id: "a", label: "Ann", amount: 95 }], 5); HoldemState.recordHand(state, [{ id: "b", label: "Ben", amount: 40 }], 0); // Oldest hand first, and money as bare numbers so a spreadsheet can total it. const csv = HandHistoryExport.toCsv(state.handHistory); const [header, raked, unraked] = csv.split("\n"); assert.match(header, /Pot,House take,Payouts/); assert.match(raked, /,100\.00,5\.00,/, "the raked hand says how much was taken"); assert.match(unraked, /,40\.00,0\.00,/, "and the unraked one says nothing was"); }); }); describe("a raked hand, end to end", () => { test("the chips add up: stacks plus the house's cut equal what was bought in", () => { // 5/10 blinds, so six callers make a pot big enough for 5% to be a whole chip. // At 1/2 the same hand makes $12, 5% of which is 60c and rounds away to // nothing — which is the rule working, not a hand worth testing the bank on. const state = raking({ percent: 5, cap: 5 }, { smallBlind: 5, bigBlind: 10 }); state.seats.forEach(p => { p.buyIns = 200; }); Betting.beginHand(state); // Everyone calls the big blind, then it is checked down. state.seats.forEach((p, i) => { if (p.inHand) Betting.putIn(state, i, Betting.toCall(state, p)); }); Betting.sweepToHandTotals(state); const pot = state.seats.reduce((sum, p) => sum + (Number(p.amountOverride) || 0), 0); const rake = HoldemState.rakeOn(state, pot, true); assert.equal(pot, 60); assert.equal(rake, 3); const { payouts } = HoldemState.applyRake([{ id: state.seats[0].id, label: "P1", amount: pot }], rake); state.rakeTaken = rake; HoldemState.recordHand(state, payouts, rake); Betting.creditWinnings(state, payouts); HoldemState.startNextHand(state); assert.equal(Money.round(chipsOnTable(state) + state.rakeTaken), 1200); assert.equal(HoldemState.bankReconciliation(state).balanced, true); }); });