diff options
Diffstat (limited to 'internal/casino')
| -rw-r--r-- | internal/casino/config_validate.go | 3 | ||||
| -rw-r--r-- | internal/casino/machine.go | 8 | ||||
| -rw-r--r-- | internal/casino/manager.go | 12 | ||||
| -rw-r--r-- | internal/casino/minbet_regression_test.go | 25 | ||||
| -rw-r--r-- | internal/casino/types.go | 9 |
5 files changed, 53 insertions, 4 deletions
diff --git a/internal/casino/config_validate.go b/internal/casino/config_validate.go index 13229bc..8e5e7b5 100644 --- a/internal/casino/config_validate.go +++ b/internal/casino/config_validate.go @@ -68,6 +68,9 @@ func validateIdentityAndBets(id string, minBet, maxBet int) error { if minBet < 0 || maxBet < 0 { return fmt.Errorf("min_bet and max_bet must not be negative") } + if minBet > 0 && maxBet == 0 && minBet > DefaultMaxBet { + return fmt.Errorf("min_bet (%d) exceeds the default maximum bet (%d); set a larger max_bet", minBet, DefaultMaxBet) + } if maxBet > 0 && maxBet < max(1, minBet) { return fmt.Errorf("max_bet must be at least min_bet") } diff --git a/internal/casino/machine.go b/internal/casino/machine.go index 0f137ec..844c5e9 100644 --- a/internal/casino/machine.go +++ b/internal/casino/machine.go @@ -228,8 +228,12 @@ func (m *Machine) finishSpin() []Event { events := []Event{} result := m.spin.Payout + m.payoutRemainder // Checked conversion: a runaway profile or overflow must clamp at the - // economy cap instead of wrapping through a machine-sized int. - if result > MaxCredits || result != result || result < 0 { + // economy cap instead of wrapping through a machine-sized int. A NaN or + // negative result is a corrupt computation, not a jackpot: clamp it to + // zero (fail closed) rather than up to the economy ceiling (fail open). + if math.IsNaN(result) || result < 0 { + result = 0 + } else if result > MaxCredits { result = MaxCredits } credits := int(math.Floor(result)) diff --git a/internal/casino/manager.go b/internal/casino/manager.go index 78e26b0..4798217 100644 --- a/internal/casino/manager.go +++ b/internal/casino/manager.go @@ -3,6 +3,7 @@ package casino import ( "fmt" "math/rand" + "reflect" "sort" "strings" "sync" @@ -41,7 +42,10 @@ func (m *Manager) EnsureMachine(roomID int, cfg MachineConfig) *Machine { m.mu.Lock() defer m.mu.Unlock() if machine := m.machines[key]; machine != nil { - if machine.Config == cfg { + // DeepEqual (not ==) so the *bool rule fields compare by value; + // == would treat every freshly parsed config as different and + // discard an idle machine on every room load. + if reflect.DeepEqual(machine.Config, cfg) { return machine } if machine.idle() { @@ -74,7 +78,7 @@ func (m *Manager) EnsureTable(roomID int, cfg TableConfig) *Table { m.mu.Lock() defer m.mu.Unlock() if table := m.tables[key]; table != nil { - if table.Config == cfg { + if reflect.DeepEqual(table.Config, cfg) { return table } if table.idle() { @@ -308,6 +312,10 @@ func (m *Manager) RefundAll() []Event { // (oldID -> newID): both the manager's map keys and each game's RoomID. // Game state (wagers, rounds) is preserved — the games continue in the // renamed room. Uses delete-all-then-set-all so a swap cannot clobber. +// +// Callers MUST run on the owner goroutine: each game's RoomID is mutated +// without taking that game's own lock, which is safe only because Tick and +// every command are owner-serialized too. func (m *Manager) RekeyRooms(idMap map[int]int) { if len(idMap) == 0 { return diff --git a/internal/casino/minbet_regression_test.go b/internal/casino/minbet_regression_test.go new file mode 100644 index 0000000..578c251 --- /dev/null +++ b/internal/casino/minbet_regression_test.go @@ -0,0 +1,25 @@ +package casino + +import "testing" + +// TestMinBetAboveDefaultMaxRejected guards the config/validate + Normalize +// interaction: an unset max_bet defaults to DefaultMaxBet, so a larger min_bet +// must be rejected (and Normalize must never leave MaxBet below MinBet), or the +// game would accept a table every wager is rejected from. +func TestMinBetAboveDefaultMaxRejected(t *testing.T) { + if err := (TableConfig{ID: "bj", Game: GameBlackjack, MinBet: DefaultMaxBet + 1}).Validate(); err == nil { + t.Fatal("table accepted min_bet above the default max") + } + if err := (MachineConfig{ID: "slots", Game: GameSlots, MinBet: DefaultMaxBet + 1}).Validate(); err == nil { + t.Fatal("machine accepted min_bet above the default max") + } + + m := MachineConfig{ID: "slots", Game: GameSlots, MinBet: DefaultMaxBet + 5}.Normalize() + if m.MaxBet < m.MinBet { + t.Fatalf("normalized machine left max %d below min %d", m.MaxBet, m.MinBet) + } + tc := TableConfig{ID: "bj", Game: GameBlackjack, MinBet: DefaultMaxBet + 5}.Normalize() + if tc.MaxBet < tc.MinBet { + t.Fatalf("normalized table left max %d below min %d", tc.MaxBet, tc.MinBet) + } +} diff --git a/internal/casino/types.go b/internal/casino/types.go index 283aa43..49e2cc4 100644 --- a/internal/casino/types.go +++ b/internal/casino/types.go @@ -128,6 +128,12 @@ func (c MachineConfig) Normalize() MachineConfig { if c.MaxBet <= 0 { c.MaxBet = DefaultMaxBet } + // Never leave a normalized config self-contradictory: a min_bet above + // the default cap must widen the effective maximum rather than produce + // a game every wager is rejected from. + if c.MaxBet < c.MinBet { + c.MaxBet = c.MinBet + } if c.SpinTicks <= 0 { c.SpinTicks = 2 } @@ -153,6 +159,9 @@ func (c TableConfig) Normalize() TableConfig { if c.MaxBet <= 0 { c.MaxBet = DefaultMaxBet } + if c.MaxBet < c.MinBet { + c.MaxBet = c.MinBet + } return c } |
