package casino import ( "math" "math/rand" "strings" "testing" ) func TestBlackjackEdgeRules(t *testing.T) { no, yes := false, true base := BlackjackConfig{DoubleSplitBlackjack: &no}.Normalize() want := blackjackMainEdge(base) if math.Abs(want-.0043) > 1e-10 { t.Fatalf("standard eight decks: %g", want) } cases := []struct { name string change func(*BlackjackConfig) higher bool }{ {"one deck", func(r *BlackjackConfig) { r.Decks = 1 }, false}, {"two decks", func(r *BlackjackConfig) { r.Decks = 2 }, false}, {"H17", func(r *BlackjackConfig) { r.DealerHitSoft17 = &yes }, true}, {"6:5", func(r *BlackjackConfig) { r.BlackjackPayout = 1.2 }, true}, {"no DAS", func(r *BlackjackConfig) { r.DoubleAfterSplit = &no }, true}, {"surrender", func(r *BlackjackConfig) { r.Surrender = &yes }, false}, {"no resplit", func(r *BlackjackConfig) { r.MaxSplitHands = 2 }, true}, {"no split", func(r *BlackjackConfig) { r.MaxSplitHands = 1 }, true}, {"resplit aces", func(r *BlackjackConfig) { r.ResplitAces = &yes }, false}, {"double split blackjack", func(r *BlackjackConfig) { r.DoubleSplitBlackjack = &yes }, false}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { r := base tc.change(&r) got := blackjackMainEdge(r) if (got > want) != tc.higher || math.Abs(got-want) < 1e-8 { t.Fatalf("edge %g base %g", got, want) } }) } h17 := base h17.DealerHitSoft17 = &yes if math.Abs(blackjackMainEdge(h17)-want-.0022) > .0003 { t.Fatal("H17 adjustment departed from published reference") } for _, action := range []string{"hit", "stand", "split", "double", "decline", "surrender"} { if got := (TableConfig{Game: GameBlackjack}).HouseEdge(action).Text(2); got != "" { t.Errorf("action %s has standalone edge: %s", action, got) } } } func TestVideoPokerEdgeEveryCabinetSchedule(t *testing.T) { for cabinet, games := range cabinetGames { for _, variant := range games { for _, tier := range []string{"low", "normal", "full"} { c := (VideoPokerConfig{Cabinet: cabinet, Payout: tier, Variant: variant}).Normalize() for _, hands := range CabinetHandCounts(cabinet, variant) { for _, feature := range []bool{false, true} { for coins := 1; coins <= 5; coins++ { e := pokerHouseEdge(c, "main", coins, feature, hands) if len(e.Parts) != 1 || math.IsNaN(e.Parts[0].Loss) { t.Fatalf("missing edge %s %s %s hands=%d feature=%v coins=%d: %+v", cabinet, variant, tier, hands, feature, coins, e) } } } } } } } } func TestVideoPokerEdgeReferences(t *testing.T) { for _, tc := range []struct { c VideoPokerConfig coins int feature bool hands int want float64 }{ {VideoPokerConfig{}, 5, false, 1, .00456096}, {VideoPokerConfig{Payout: "low"}, 5, false, 1, .038528}, {VideoPokerConfig{}, 1, false, 1, .00456096 + 550*.00002476}, {VideoPokerConfig{Cabinet: CabinetUXSingle}, 5, true, 1, .0028}, {VideoPokerConfig{Cabinet: CabinetUXMulti}, 5, true, 3, .007022}, {VideoPokerConfig{Cabinet: CabinetUXMulti}, 5, true, 10, .005780}, {VideoPokerConfig{Cabinet: CabinetSuperTimesPay}, 5, true, 1, 1 - .99543904*(18.05/15)*5/6}, {VideoPokerConfig{Cabinet: CabinetHotRoll}, 5, true, 3, .00456096}, {VideoPokerConfig{Variant: "deuces_wild"}, 5, false, 1, -.007620}, } { e := pokerHouseEdge(tc.c, "main", tc.coins, tc.feature, tc.hands) if len(e.Parts) != 1 || math.Abs(e.Parts[0].Loss-tc.want) > 1e-9 { t.Fatalf("%+v: %+v want %g", tc, e, tc.want) } } } func TestHouseEdgeUsesVideoPokerSeat(t *testing.T) { cfg := (TableConfig{Game: GameVideoPoker}).Normalize() game := newVideoPokerTable(1, cfg, rand.New(rand.NewSource(1))).(*videoPokerTable) game.players["alice"] = &pokerPlayer{config: (VideoPokerConfig{Variant: "deuces_wild"}).Normalize(), loaded: 5, hands: 1} table := &Table{game: game} if got := table.HouseEdge("alice", "main").Text(2); got != "House edge: -0.76%" { t.Fatal(got) } game.players["alice"].loaded = 1 if got := table.HouseEdge("alice", "main").Text(2); got == "House edge: -0.76%" { t.Fatal("coin selection ignored") } } func TestPaiGowEdgeReferences(t *testing.T) { for _, tc := range []struct { r PaiGowConfig bet string want float64 }{ {PaiGowConfig{}, "fortune", .034318}, {PaiGowConfig{Envy: true}, "fortune", .044292}, {PaiGowConfig{Variant: PaiGowFaceUp, PushAceHigh: "5_15_40"}, "pushace", .0929}, {PaiGowConfig{Variant: PaiGowFaceUp, PushAceHigh: "5_12_40"}, "pushace", .1052}, } { e := paiGowHouseEdge(TableConfig{PaiGow: tc.r}, tc.bet) if math.Abs(e.Parts[0].Loss-tc.want) > .000001 { t.Fatalf("%+v: %+v", tc, e) } } for _, variant := range []string{PaiGowClassic, PaiGowEZ, PaiGowNoPush, PaiGowFaceUp} { if e := paiGowHouseEdge(TableConfig{PaiGow: PaiGowConfig{Variant: variant}}, "main"); len(e.Parts) != 1 { t.Fatal(variant, e) } } if fortuneEnvyReturn(2, 5) <= fortuneEnvyReturn(1, 5) || fortuneEnvyReturn(2, 4) != 0 { t.Fatal("Envy eligibility/scaling") } } func TestHouseEdgeTextOnlyPercentage(t *testing.T) { e := HouseEdge{Parts: []EdgePart{{Label: "something", Loss: .027027}}, Note: "explanation"} if e.Text(0) != "House edge: 3%" || e.Text(2) != "House edge: 2.70%" { t.Fatal(e.Text(2)) } if strings.Contains(e.Text(2), "explanation") || unknownEdge("unknown").Text(2) != "" { t.Fatal("extra prose") } }