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)) } } } // TestCrapsGroupedTargetsHaveRTP: the list names grouped rows (4/10, buy N, // odds pass / come). Each must resolve to a real edge, not the unknown dash. func TestCrapsGroupedTargetsHaveRTP(t *testing.T) { for _, bet := range []string{ "place 4/10", "place 5/9", "place 6/8", "hard 4/10", "hard 6/8", "odds pass / come", "odds dont N", "buy N", "lay N", } { got := (TableConfig{Game: GameCraps}).HouseEdge(bet) if len(got.Parts) == 0 { t.Fatalf("%s has no RTP: %s", bet, got.Note) } } crapless := TableConfig{Game: GameCraps, Craps: CrapsConfig{Variant: "crapless"}} for _, bet := range []string{"place 2/12", "place 3/11"} { got := crapless.HouseEdge(bet) if len(got.Parts) == 0 { t.Fatalf("crapless %s has no RTP: %s", bet, got.Note) } } // The rows expand to every legal number, including the crapless extras. for _, want := range []string{"2", "3", "11", "12"} { found := false for _, part := range crapless.HouseEdge("lay N").Parts { if part.Label == want { found = true } } if !found { t.Fatalf("crapless lay N missing number %s", want) } } } // 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 }