aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/config_validate_test.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-15 14:18:14 -0400
committerhistoria <[not public]>2026-09-15 14:18:14 -0400
commit396c3facb8287b9a34076cae8f3d7b300d366d99 (patch)
tree941f280d051fac474fa68ca08318a8d16adc8d0f /internal/casino/config_validate_test.go
parentb3568f03e4103dd46db2ccc680fbab0a6454d604 (diff)
downloadthehouseoficarus-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.go124
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")
+ }
+}