aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/config_validate_test.go
blob: f0f87f5190ba547f4ca71f3585848f3b021769ef (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
package casino

import (
	"fmt"
	"math"
	"reflect"
	"sync"
	"testing"
)

func TestMachineConfigValidation(t *testing.T) {
	valid := MachineConfig{ID: "slots", Game: GameSlots}
	if err := valid.Validate(); err != nil {
		t.Fatal(err)
	}
	for name, change := range map[string]func(*MachineConfig){
		"missing ID":      func(c *MachineConfig) { c.ID = "" },
		"ID whitespace":   func(c *MachineConfig) { c.ID = " slots " },
		"unknown game":    func(c *MachineConfig) { c.Game = "roulette" },
		"unknown variant": func(c *MachineConfig) { c.Variant = "typo" },
		"negative bet":    func(c *MachineConfig) { c.MinBet = -1 },
		"negative max":    func(c *MachineConfig) { c.MaxBet = -1 },
		"inverted bets":   func(c *MachineConfig) { c.MinBet, c.MaxBet = 10, 5 },
		"negative timer":  func(c *MachineConfig) { c.SpinTicks = -1 },
		"negative payout": func(c *MachineConfig) { c.Payout = -1 },
		"excess payout":   func(c *MachineConfig) { c.Payout = 1.01 },
		"NaN payout":      func(c *MachineConfig) { c.Payout = math.NaN() },
		"infinite payout": func(c *MachineConfig) { c.Payout = math.Inf(1) },
	} {
		t.Run(name, func(t *testing.T) {
			c := valid
			change(&c)
			if err := c.Validate(); err == nil {
				t.Fatalf("accepted invalid config: %+v", c)
			}
			m := NewManager(1)
			if m.EnsureMachine(1, c) != nil {
				t.Fatal("invalid machine instantiated")
			}
			if events := m.JoinMachine(1, c, "alice"); len(events) != 1 || !contains(events[0].Message, "not available") {
				t.Fatalf("invalid machine join: %+v", events)
			}
		})
	}
}

func TestTableConfigValidation(t *testing.T) {
	for _, game := range []GameName{GameBlackjack, GameBaccarat} {
		if err := (TableConfig{ID: "table", Game: game}).Validate(); err != nil {
			t.Fatal(err)
		}
	}
	for name, c := range map[string]TableConfig{
		"unknown game":          {ID: "table", Game: "roulette"},
		"missing ID":            {Game: GameBlackjack},
		"negative timer":        {ID: "table", Game: GameBlackjack, BettingTicks: -1},
		"negative seats":        {ID: "table", Game: GameBlackjack, MaxPlayers: -1},
		"inverted bets":         {ID: "table", Game: GameBlackjack, MinBet: 10, MaxBet: 5},
		"negative decks":        {ID: "table", Game: GameBlackjack, Blackjack: BlackjackConfig{Decks: -1}},
		"huge shoe":             {ID: "table", Game: GameBaccarat, Baccarat: BaccaratConfig{Decks: 1000000000}},
		"NaN shuffle":           {ID: "table", Game: GameBlackjack, Blackjack: BlackjackConfig{ShuffleAt: math.NaN()}},
		"shuffle endpoint":      {ID: "table", Game: GameBaccarat, Baccarat: BaccaratConfig{ShuffleAt: 1}},
		"infinite payout":       {ID: "table", Game: GameBlackjack, Blackjack: BlackjackConfig{BlackjackPayout: math.Inf(1)}},
		"negative insurance":    {ID: "table", Game: GameBlackjack, Blackjack: BlackjackConfig{InsurancePayout: -1}},
		"NaN tie":               {ID: "table", Game: GameBaccarat, Baccarat: BaccaratConfig{TiePayout: math.NaN()}},
		"negative banker":       {ID: "table", Game: GameBaccarat, Baccarat: BaccaratConfig{BankerPayout: -1}},
		"negative splits":       {ID: "table", Game: GameBlackjack, Blackjack: BlackjackConfig{MaxSplitHands: -1}},
		"negative action timer": {ID: "table", Game: GameBlackjack, Blackjack: BlackjackConfig{ActionTicks: -1}},
	} {
		t.Run(name, func(t *testing.T) {
			if c.Validate() == nil || NewManager(1).EnsureTable(1, c) != nil {
				t.Fatalf("accepted invalid table: %+v", c)
			}
		})
	}
}

func TestRoomCasinoIDsAndLegacySlots(t *testing.T) {
	tables := []TableConfig{{ID: "slots", Game: GameSlots}}
	machines := []MachineConfig{{ID: "slots", Game: GameSlots}}
	if err := ValidateRoomConfigs(tables, nil); err != nil {
		t.Fatalf("legacy slots rejected: %v", err)
	}
	if ValidateRoomConfigs(tables, machines) == nil || ValidateRoomConfigs(nil, append(machines, machines...)) == nil {
		t.Fatal("duplicate casino IDs accepted")
	}
}

func TestManagerGamesHaveIndependentRandomStreams(t *testing.T) {
	m := NewManager(123)
	var wg sync.WaitGroup
	for i := 0; i < 8; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			// Creation/shuffling can overlap spins on already-created games.
			id := fmt.Sprintf("slots%d", i)
			machine := m.EnsureMachine(1, MachineConfig{ID: id, Game: GameSlots})
			m.EnsureTable(1, TableConfig{ID: fmt.Sprintf("cards%d", i), Game: GameBlackjack})
			machine.join(id)
			wallet := &testWallet{credits: 100}
			for j := 0; j < 50; j++ {
				machine.setBet(id, 1, wallet)
				for machine.isBusy(id) {
					machine.tick()
				}
			}
		}(i)
	}
	wg.Wait()
}

func TestScaleToRTPDoesNotChangeSourceProfile(t *testing.T) {
	source := DefaultHuffAndPuffProfile()
	want := DefaultHuffAndPuffProfile()
	first := ScaleToRTP(source, .9)
	second := ScaleToRTP(source, .97)
	if !reflect.DeepEqual(source, want) {
		t.Fatal("scaling mutated the source profile")
	}
	if math.Abs(CalculateRTP(first)-.9) > 1e-10 || math.Abs(CalculateRTP(second)-.97) > 1e-10 {
		t.Fatal("profiles share mutable paytables")
	}
}