1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
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
}
|