aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/house_edge_rules_test.go
blob: b79888f04deeb657b21db8d7ee7d214e670a6d59 (plain)
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")
	}
}