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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
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")
}
}
|