aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/house_edge_rules_test.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-10-02 01:54:37 -0400
committerhistoria <[not public]>2026-10-02 01:54:37 -0400
commitd3a305d1d7962f82af2397760cf8008758bddd1d (patch)
treef91f0deb342c20f2993e5366f772c3432648f426 /internal/casino/house_edge_rules_test.go
parent8eea0e4e06a4aaa6a18edabd9897d3bea1ea2ca0 (diff)
downloadthehouseoficarus-d3a305d1d7962f82af2397760cf8008758bddd1d.tar.gz
feat: better 'list' bet table, multiple bets in baccarat
Diffstat (limited to 'internal/casino/house_edge_rules_test.go')
-rw-r--r--internal/casino/house_edge_rules_test.go148
1 files changed, 148 insertions, 0 deletions
diff --git a/internal/casino/house_edge_rules_test.go b/internal/casino/house_edge_rules_test.go
new file mode 100644
index 0000000..b79888f
--- /dev/null
+++ b/internal/casino/house_edge_rules_test.go
@@ -0,0 +1,148 @@
+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")
+ }
+}