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 TestRoomCasinoIDsRejectDuplicates(t *testing.T) { machines := []MachineConfig{{ID: "slots", Game: GameSlots}} if ValidateRoomConfigs(nil, append(machines, machines...)) == nil { t.Fatal("duplicate casino IDs accepted") } // Slots are machines only: declaring them under casino_tables is invalid. if err := ValidateRoomConfigs([]TableConfig{{ID: "slots", Game: GameSlots}}, nil); err == nil { t.Fatal("slots table accepted; slots must be declared under casino_machines") } } 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") } } func TestSlotProfileEmptySymbolsRejected(t *testing.T) { // A new slotVariants entry that forgets its symbol table must not validate // and silently play the built-in cabinet. if err := (SlotProfile{}).Validate(); err == nil { t.Fatal("empty slot profile validated; expected an error") } } func TestTableConfigRejectsForeignRuleBlock(t *testing.T) { c := TableConfig{ID: "t", Game: GameBaccarat, Blackjack: BlackjackConfig{BlackjackPayout: 15}} if err := c.Validate(); err == nil { t.Fatal("baccarat table with a blackjack rule block validated; expected an error") } ok := TableConfig{ID: "t", Game: GameBlackjack} if err := ok.Validate(); err != nil { t.Fatalf("valid blackjack table rejected: %v", err) } } func TestTableConfigTimerCaps(t *testing.T) { cases := map[string]TableConfig{ "huge betting window": {ID: "t", Game: GameBaccarat, BettingTicks: maxBettingTicks + 1}, "huge action window": {ID: "t", Game: GameBlackjack, Blackjack: BlackjackConfig{ActionTicks: maxActionTicks + 1}}, } for name, c := range cases { t.Run(name, func(t *testing.T) { if err := c.Validate(); err == nil { t.Fatal("expected an error") } }) } m := MachineConfig{ID: "m", Game: GameSlots, SpinTicks: maxSpinTicks + 1} if err := m.Validate(); err == nil { t.Fatal("expected an error for an oversized spin window") } }