diff options
| author | historia <[not public]> | 2026-10-02 01:54:37 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-10-02 01:54:37 -0400 |
| commit | d3a305d1d7962f82af2397760cf8008758bddd1d (patch) | |
| tree | f91f0deb342c20f2993e5366f772c3432648f426 /internal/casino/house_edge_test.go | |
| parent | 8eea0e4e06a4aaa6a18edabd9897d3bea1ea2ca0 (diff) | |
| download | thehouseoficarus-d3a305d1d7962f82af2397760cf8008758bddd1d.tar.gz | |
feat: better 'list' bet table, multiple bets in baccarat
Diffstat (limited to 'internal/casino/house_edge_test.go')
| -rw-r--r-- | internal/casino/house_edge_test.go | 127 |
1 files changed, 127 insertions, 0 deletions
diff --git a/internal/casino/house_edge_test.go b/internal/casino/house_edge_test.go new file mode 100644 index 0000000..5f75e44 --- /dev/null +++ b/internal/casino/house_edge_test.go @@ -0,0 +1,127 @@ +package casino + +import ( + "strings" + "testing" +) + +// TestKnownHouseEdges pins the exact expected-loss figures against +// independently derived values (the engine's own resolution rules for +// variant-specific bets, standard published mathematics for the classics). +// A changed payout or rule must break this test, never silently reprice the +// reference. +func TestKnownHouseEdges(t *testing.T) { + cases := []struct { + name string + cfg TableConfig + bet string + want float64 // expected loss per unit stake (unrounded fraction) + }{ + {"roulette straight", TableConfig{Game: GameRoulette}, "<number>", 1.0 / 37}, + {"roulette even money", TableConfig{Game: GameRoulette}, "red / black", 1.0 / 37}, + {"roulette DZ straight", TableConfig{Game: GameRoulette, Roulette: RouletteConfig{DoubleZero: true}}, "<number>", 2.0 / 38}, + {"roulette DZ basket", TableConfig{Game: GameRoulette, Roulette: RouletteConfig{DoubleZero: true}}, "basket", 3.0 / 38}, + {"craps pass", TableConfig{Game: GameCraps}, "pass", 0.014141414141414}, + {"craps dontpass", TableConfig{Game: GameCraps}, "dontpass", 0.013636363636364}, + {"craps come", TableConfig{Game: GameCraps}, "come", 0.014141414141414}, + {"craps dontcome", TableConfig{Game: GameCraps}, "dontcome", 0.013636363636364}, + {"craps odds", TableConfig{Game: GameCraps}, "odds pass", 0}, + {"craps field 2", TableConfig{Game: GameCraps}, "field", 0.055555555555556}, + {"craps field 3", TableConfig{Game: GameCraps, Craps: CrapsConfig{Field12: 3}}, "field", 0.027777777777778}, + {"craps seven", TableConfig{Game: GameCraps}, "seven", 1.0 / 6}, + {"craps craps", TableConfig{Game: GameCraps}, "craps", 1.0 / 9}, + {"craps eleven", TableConfig{Game: GameCraps}, "eleven / ace-deuce", 1.0 / 9}, + {"craps aces", TableConfig{Game: GameCraps}, "aces / boxcars", 5.0 / 36}, + {"craps horn", TableConfig{Game: GameCraps}, "horn", 0.125}, + {"craps place 4", TableConfig{Game: GameCraps}, "place 4", 0.066666666666667}, + {"craps place 5", TableConfig{Game: GameCraps}, "place 5", 0.04}, + {"craps place 6", TableConfig{Game: GameCraps}, "place 6", 0.015151515151515}, + {"craps hard 4", TableConfig{Game: GameCraps}, "hard 4 / hard 10", 1.0 / 9}, + {"craps hard 6", TableConfig{Game: GameCraps}, "hard 6 / hard 8", 1.0 / 11}, + {"craps small", TableConfig{Game: GameCraps}, "small / tall", 0.077613176297387}, + {"craps all", TableConfig{Game: GameCraps}, "all", 0.074644079069427}, + {"craps fire", TableConfig{Game: GameCraps}, "fire", 0.207627504982314}, + {"craps simplified", TableConfig{Game: GameCraps, Craps: CrapsConfig{Variant: "simplified"}}, "pass", 1.0 / 36}, + {"craps high point", TableConfig{Game: GameCraps, Craps: CrapsConfig{Variant: "high_point"}}, "pass", 0.023569023569024}, + {"craps die rich", TableConfig{Game: GameCraps, Craps: CrapsConfig{Variant: "die_rich"}}, "pass", 0}, + {"craps crapless", TableConfig{Game: GameCraps, Craps: CrapsConfig{Variant: "crapless"}}, "pass", 0.053823953823954}, + {"craps new york", TableConfig{Game: GameCraps, Craps: CrapsConfig{Variant: "new_york"}}, "pass", 0.014141414141414}, + {"craps new york dontpass", TableConfig{Game: GameCraps, Craps: CrapsConfig{Variant: "new_york"}}, "dontpass", 0.013636363636364}, + {"baccarat player", TableConfig{Game: GameBaccarat}, "player", 0.012350813289149}, + {"baccarat banker", TableConfig{Game: GameBaccarat}, "banker", 0.010579057842466}, + {"baccarat tie", TableConfig{Game: GameBaccarat}, "tie", 0.143596287787302}, + {"baccarat pair", TableConfig{Game: GameBaccarat}, "player_pair", 0.103614457831325}, + {"baccarat dragon", TableConfig{Game: GameBaccarat, Baccarat: BaccaratConfig{Dragon7: true}}, "dragon_7", 0.076113344724498}, + {"baccarat panda", TableConfig{Game: GameBaccarat, Baccarat: BaccaratConfig{Panda8: true}}, "panda_8", 0.101876321693189}, + {"baccarat super6 bet", TableConfig{Game: GameBaccarat, Baccarat: BaccaratConfig{Super6Bet: true}}, "super_6", 0.299771693848976}, + {"baccarat super6 banker", TableConfig{Game: GameBaccarat, Baccarat: BaccaratConfig{Variant: BaccaratSuper6}}, "banker", 0.014581044639736}, + {"baccarat ez banker", TableConfig{Game: GameBaccarat, Baccarat: BaccaratConfig{Variant: BaccaratEZ}}, "banker", 0.010183007571229}, + {"baccarat 6 decks player", TableConfig{Game: GameBaccarat, Baccarat: BaccaratConfig{Decks: 6}}, "player", 0.012374148986428}, + } + for _, tc := range cases { + if tc.bet == "" { + continue + } + got := tc.cfg.HouseEdge(tc.bet) + if strings.Contains(got.Note, "not verified") { + t.Fatalf("%s: expected a verified edge, got %q", tc.name, got.Text(2)) + } + if len(got.Parts) != 1 || abs64(got.Parts[0].Loss-tc.want) > 5e-13 { + t.Fatalf("%s: loss = %.18f, want %.18f", tc.name, got.Parts[0].Loss, tc.want) + } + } +} + +// Strategy games have reference estimates rather than missing readouts. +func TestStrategyHouseEdges(t *testing.T) { + for _, tc := range []struct { + name string + cfg TableConfig + bet string + }{ + {"blackjack main", TableConfig{Game: GameBlackjack}, "main"}, + {"paigow main", TableConfig{Game: GamePaiGow}, "main"}, + {"paigow fortune", TableConfig{Game: GamePaiGow}, "fortune"}, + {"video poker", TableConfig{Game: GameVideoPoker}, "main"}, + } { + got := tc.cfg.HouseEdge(tc.bet) + if len(got.Parts) != 1 || got.Text(2) == "" { + t.Fatalf("%s: missing reference edge: %+v", tc.name, got) + } + } +} + +// TestUnrecognizedTargetNeverInherits: a target the analysis does not know +// must be flagged, not silently given another bet's edge. +func TestUnrecognizedTargetNeverInherits(t *testing.T) { + for _, cfg := range []TableConfig{ + {Game: GameRoulette}, + {Game: GameCraps}, + {Game: GameBaccarat}, + {Game: GameBlackjack}, + {Game: GamePaiGow}, + {Game: GameVideoPoker}, + } { + if got := cfg.HouseEdge("not-a-bet"); got.Note == "" { + t.Fatalf("%s: unrecognized target inherited an edge: %s", cfg.Game, got.Text(2)) + } + } +} + +// TestHopBetsTwoParts pins the split hop analysis (easy vs doubles). +func TestHopBetsTwoParts(t *testing.T) { + got := (TableConfig{Game: GameCraps}).HouseEdge("hop <a> <b>") + if len(got.Parts) != 2 { + t.Fatalf("hop analysis: %s", got.Text(2)) + } + if abs64(got.Parts[0].Loss-1.0/9) > 5e-13 || abs64(got.Parts[1].Loss-5.0/36) > 5e-13 { + t.Fatalf("hop losses = %s", got.Text(2)) + } +} + +func abs64(x float64) float64 { + if x < 0 { + return -x + } + return x +} |
