aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/house_edge_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_test.go
parent8eea0e4e06a4aaa6a18edabd9897d3bea1ea2ca0 (diff)
downloadthehouseoficarus-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.go127
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
+}