diff options
| author | historia <[not public]> | 2026-09-15 14:18:14 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-15 14:18:14 -0400 |
| commit | 396c3facb8287b9a34076cae8f3d7b300d366d99 (patch) | |
| tree | 941f280d051fac474fa68ca08318a8d16adc8d0f /internal/casino/config_validate_test.go | |
| parent | b3568f03e4103dd46db2ccc680fbab0a6454d604 (diff) | |
| download | thehouseoficarus-396c3facb8287b9a34076cae8f3d7b300d366d99.tar.gz | |
fix: atomic saves, casino RNG/config isolation, tick lifecycle
Diffstat (limited to 'internal/casino/config_validate_test.go')
| -rw-r--r-- | internal/casino/config_validate_test.go | 124 |
1 files changed, 124 insertions, 0 deletions
diff --git a/internal/casino/config_validate_test.go b/internal/casino/config_validate_test.go new file mode 100644 index 0000000..f0f87f5 --- /dev/null +++ b/internal/casino/config_validate_test.go @@ -0,0 +1,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") + } +} |
