aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/settlement_test.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-10-01 22:47:41 -0400
committerhistoria <[not public]>2026-10-01 22:47:41 -0400
commit8eea0e4e06a4aaa6a18edabd9897d3bea1ea2ca0 (patch)
tree63698c0618f382a553d4558a33e3d2087039c378 /internal/casino/settlement_test.go
parent6d9908b29c8187196d23b126b1165c7e0bd0fc76 (diff)
downloadthehouseoficarus-8eea0e4e06a4aaa6a18edabd9897d3bea1ea2ca0.tar.gz
feat: consistent wager outcome table across casino games
Diffstat (limited to 'internal/casino/settlement_test.go')
-rw-r--r--internal/casino/settlement_test.go252
1 files changed, 252 insertions, 0 deletions
diff --git a/internal/casino/settlement_test.go b/internal/casino/settlement_test.go
new file mode 100644
index 0000000..9c110d6
--- /dev/null
+++ b/internal/casino/settlement_test.go
@@ -0,0 +1,252 @@
+package casino
+
+import (
+ "strings"
+ "testing"
+)
+
+type recoveringSettlementWallet struct {
+ testWallet
+ fail bool
+}
+
+func (w *recoveringSettlementWallet) PayCredits(n int, ids ...string) error {
+ if w.fail {
+ return ErrWalletPersistence
+ }
+ return w.testWallet.PayCredits(n, ids...)
+}
+
+func receiptLines(events []Event, name string) []SettlementLine {
+ var lines []SettlementLine
+ for _, e := range events {
+ if e.PrivateTo == name {
+ lines = append(lines, e.Settlement...)
+ }
+ }
+ return lines
+}
+func receiptTotal(lines []SettlementLine) (stake, returned int) {
+ for _, line := range lines {
+ if !line.Unresolved {
+ stake += line.Stake
+ returned += line.Returned
+ }
+ }
+ return
+}
+
+func TestCasinoReceiptBlackjackSplitsInsuranceAndNatural(t *testing.T) {
+ tb := uxBlackjack()
+ p := tb.players.get("alice")
+ p.Wallet = &testWallet{}
+ p.hands = []*blackjackHand{
+ {bet: 100, cards: []card{{rank: "A"}, {rank: "K"}}},
+ {bet: 200, cards: []card{{rank: "10"}, {rank: "9"}}},
+ {bet: 100, cards: []card{{rank: "10"}, {rank: "7"}}},
+ }
+ p.insurance = 50
+ lines := receiptLines(tb.settle(), "alice")
+ if len(lines) != 4 {
+ t.Fatalf("%+v", lines)
+ }
+ if lines[0].Odds != "3:2" || lines[0].Returned != 250 || lines[1].Returned != 400 || lines[2].Result != "push" || lines[3].Odds != "2:1" {
+ t.Fatalf("%+v", lines)
+ }
+ stake, paid := receiptTotal(lines)
+ if stake != 450 || paid != 750 {
+ t.Fatalf("stake=%d paid=%d", stake, paid)
+ }
+}
+
+func TestCasinoReceiptRouletteAndBaccarat(t *testing.T) {
+ t.Run("roulette", func(t *testing.T) {
+ tb := newRouletteTable(1, TableConfig{ID: "r", Game: GameRoulette}, newTestRand())
+ w := &testWallet{credits: 100}
+ tb.join("alice")
+ tb.bet("alice", "17", 10, w)
+ lines := receiptLines(tb.settle(), "alice")
+ if len(lines) != 1 || lines[0].Odds != "35:1" || lines[0].Returned != w.paid {
+ t.Fatalf("%+v paid=%d", lines, w.paid)
+ }
+ })
+ t.Run("baccarat", func(t *testing.T) {
+ tb := newBaccaratTestTable(TableConfig{ID: "b", Game: GameBaccarat})
+ w := &testWallet{credits: 100}
+ tb.join("alice")
+ events := tb.bet("alice", "banker", 10, w)
+ for i := 0; i < 30 && len(receiptLines(events, "alice")) == 0; i++ {
+ events = append(events, tb.tick()...)
+ }
+ lines := receiptLines(events, "alice")
+ if len(lines) != 1 || lines[0].Odds != "0.95:1" || lines[0].Returned != w.paid {
+ t.Fatalf("%+v paid=%d", lines, w.paid)
+ }
+ })
+}
+
+func TestCasinoReceiptCrapsActiveAndOddsRefund(t *testing.T) {
+ tb := crapsFixture()
+ w := &testWallet{credits: 1000}
+ tb.join("alice")
+ tb.bet("alice", "pass", 100, w)
+ tb.bet("alice", "field", 25, w)
+ lines := receiptLines(tb.resolveRoll(3, 3), "alice")
+ if len(lines) != 2 {
+ t.Fatalf("%+v", lines)
+ }
+ stake, paid := receiptTotal(lines)
+ if stake != 25 || paid != 0 {
+ t.Fatalf("%+v", lines)
+ }
+ active := 0
+ for _, line := range lines {
+ if line.Unresolved {
+ active += line.Stake
+ }
+ }
+ if active != 100 {
+ t.Fatal(lines)
+ }
+ tb.bet("alice", "odds pass", 120, w)
+ lines = receiptLines(tb.resolveRoll(3, 3), "alice")
+ if len(lines) != 2 {
+ t.Fatalf("%+v", lines)
+ }
+ if lines[0].Odds != "1:1" || lines[1].Odds != "6:5" {
+ t.Fatal(lines)
+ }
+ stake, paid = receiptTotal(lines)
+ if stake != 220 || paid != 464 {
+ t.Fatalf("%+v", lines)
+ }
+ b := &crapsBet{kind: "come", number: 6, stake: Wager{Total: 10}, odds: Wager{Total: 60}}
+ lines = tb.crapsSettlementLines(b, -1, 1, true, false)
+ if lines[0].Result != "loss" || lines[1].Result != "push" || lines[1].Returned != 60 {
+ t.Fatal(lines)
+ }
+}
+
+func TestCasinoReceiptCrapsPartialFailure(t *testing.T) {
+ tb := crapsFixture()
+ tb.join("alice")
+ w := &recoveringSettlementWallet{testWallet: testWallet{credits: 1000}}
+ tb.bet("alice", "pass", 100, w)
+ tb.bet("alice", "field", 25, w)
+ w.fail = true
+ events := tb.resolveRoll(3, 3)
+ lines := receiptLines(events, "alice")
+ if len(lines) != 1 || !lines[0].Unresolved {
+ t.Fatal(lines)
+ }
+ found := false
+ for _, e := range events {
+ found = found || e.SettlementNote != ""
+ }
+ if !found {
+ t.Fatal("failed wager warning lost")
+ }
+}
+
+func TestCasinoReceiptSlotFeatureAndDurability(t *testing.T) {
+ m := newMachine(1, MachineConfig{ID: "s", Game: GameSlots}, newTestRand())
+ w := &recoveringSettlementWallet{testWallet: testWallet{credits: 100}}
+ m.join("alice")
+ m.beginSpinLocked(10, w, "", "")
+ m.spin = &SlotSpin{ScatterCount: 3, Payout: 20}
+ lines := receiptLines(m.finishSpin(), "alice")
+ if len(lines) != 1 || !lines[0].Unresolved || w.paid != 0 {
+ t.Fatal(lines, w.paid)
+ }
+ m.freeSpins = 0
+ m.spin = &SlotSpin{Payout: 5}
+ lines = receiptLines(m.finishSpin(), "alice")
+ if len(lines) != 1 || lines[0].Unresolved || lines[0].Stake != 10 || lines[0].Returned != 25 || w.paid != 25 {
+ t.Fatal(lines, w.paid)
+ }
+ // A failed zero-return save must not announce a settled loss.
+ m.beginSpinLocked(10, w, "", "")
+ m.spin = &SlotSpin{}
+ w.fail = true
+ events := m.finishSpin()
+ if len(receiptLines(events, "alice")) != 0 || len(w.open) != 1 || !hasMessage(events, payoutFailedMessage()) {
+ t.Fatalf("events=%+v open=%v", events, w.open)
+ }
+}
+
+func TestCasinoReceiptVideoPokerAndPaiGow(t *testing.T) {
+ t.Run("poker", func(t *testing.T) {
+ tb, w, _ := pokerTestCabinet(t, VideoPokerConfig{Cabinet: CabinetClassic})
+ pokerDeal(t, tb, w)
+ p := tb.players["alice"]
+ p.cards = pokerHand("Ah Kh Qh Jh 10h")
+ p.held = [5]bool{true, true, true, true, true}
+ stake := p.wager.Total
+ lines := receiptLines(tb.drawLocked("alice", p), "alice")
+ got, paid := receiptTotal(lines)
+ if len(lines) != p.hands || got != stake || paid != w.paid || paid == 0 || lines[0].Odds != "799:1" {
+ t.Fatalf("%+v stake=%d paid=%d", lines, stake, w.paid)
+ }
+ })
+ t.Run("pai gow", func(t *testing.T) {
+ tb := pgTestTable(true)
+ w := &testWallet{credits: 1000}
+ tb.join("alice")
+ tb.bet("alice", "fortune", 5, w)
+ tb.bet("alice", "main", 100, w)
+ p := tb.player("alice")
+ p.split = pgHouse(p.cards)
+ p.ready = true
+ lines := receiptLines(tb.prepareSettlementLocked(), "alice")
+ stake, paid := receiptTotal(lines)
+ if len(lines) != 2 || stake != 105 || paid != w.paid || lines[0].Odds != "19:20" {
+ t.Fatalf("%+v paid=%d", lines, w.paid)
+ }
+ })
+}
+
+func TestCasinoReceiptPaiGowRetryAnnouncesExactlyOnce(t *testing.T) {
+ tb := pgTestTable(false)
+ w := &recoveringSettlementWallet{testWallet: testWallet{credits: 1000}}
+ tb.join("alice")
+ tb.bet("alice", "fortune", 5, w)
+ tb.bet("alice", "main", 100, w)
+ p := tb.player("alice")
+ p.split, p.ready = pgHouse(p.cards), true
+ w.fail = true
+ if lines := receiptLines(tb.prepareSettlementLocked(), "alice"); len(lines) != 0 {
+ t.Fatal(lines)
+ }
+ if !p.showdown.Pending {
+ t.Fatal("missing payment-pending status")
+ }
+ w.fail = false
+ lines := receiptLines(tb.payLocked(), "alice")
+ if len(lines) != 2 || p.showdown.Pending || !p.showdown.Paid {
+ t.Fatal(lines, p.showdown)
+ }
+ paid := w.paid
+ if len(receiptLines(tb.payLocked(), "alice")) != 0 || w.paid != paid {
+ t.Fatal("duplicate settlement on retry")
+ }
+}
+
+func TestCasinoReceiptOddsAreProfitNotReturn(t *testing.T) {
+ for _, tc := range []struct {
+ num, den int
+ want string
+ }{{35, 1, "35:1"}, {3, 2, "3:2"}, {144, 120, "6:5"}, {0, 5, "0:1"}} {
+ if got := profitOdds(tc.num, tc.den); got != tc.want {
+ t.Fatalf("%s != %s", got, tc.want)
+ }
+ }
+ w, _ := rouletteWagerFor("orphelins", false, true)
+ if got := rouletteSettlementOdds(w, "0"); got != "Varies" {
+ t.Fatal(got)
+ }
+ for _, line := range crapsFixture().crapsSettlementLines(&crapsBet{kind: "buy", number: 4, stake: Wager{Total: 20}}, 2, 1, false, false) {
+ if !strings.Contains(line.Odds, "fee") || line.Returned != 59 {
+ t.Fatal(line)
+ }
+ }
+}