// Who hands cash to whom at the end of the night. // // The standings panel already works out what everyone is up or down. It then stopped, // which leaves the dealer with six signed numbers at 2am and the actual job still to // do: turning them into payments. That's the step people get wrong — not the poker. // // Everything is done in cents. A settlement that has to come out exactly right is the // last place to let a float land on 41.99999999. const SettleUp = { // Largest debt against largest win, repeatedly. It's greedy rather than optimal, // and for this problem that's the right trade: finding the true minimum number of // transfers is NP-hard, while this is guaranteed to need at most one payment fewer // than there are people, and each payment it does emit clears somebody out of the // list entirely. A dealer wants a short list they can read down, not a proof. // // Payers are the outer loop so that everything one person owes is printed together // — the list is read out loud, one player at a time. plan(standings) { const owed = []; const owing = []; (standings || []).forEach(row => { const cents = Money.toCents(row.cashPosition); if (cents > 0) owed.push({ name: row.name, cents }); else if (cents < 0) owing.push({ name: row.name, cents: -cents }); }); owed.sort((a, b) => b.cents - a.cents || a.name.localeCompare(b.name)); owing.sort((a, b) => b.cents - a.cents || a.name.localeCompare(b.name)); const payments = []; let creditor = 0; owing.forEach(payer => { while (payer.cents > 0 && creditor < owed.length) { const taker = owed[creditor]; const amount = Math.min(payer.cents, taker.cents); payments.push({ from: payer.name, to: taker.name, amount: Money.fromCents(amount) }); payer.cents -= amount; taker.cents -= amount; if (taker.cents === 0) creditor++; } }); // What's left over when the chips on the table don't match the cash that came // in. It is never a rounding artifact — the arithmetic is integer cents — so it // is always the thing the bank check is already warning about: a rebuy nobody // wrote down, or a miscount. Reported rather than quietly spread across the // payments, because inventing a transfer to make the columns agree is how a // real person ends up short. const unpaid = Money.fromCents(owed.reduce((sum, row) => sum + row.cents, 0)); const unspent = Money.fromCents(owing.reduce((sum, row) => sum + row.cents, 0)); return { payments, // Only one of these can be non-zero: either there are more chips than cash // or less, never both. shortBy: unpaid, overBy: unspent, balanced: unpaid === 0 && unspent === 0, }; }, }; if (typeof module !== "undefined" && module.exports) { module.exports = SettleUp; }