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