package casino import ( "fmt" "math/rand" "sort" "strings" "sync" ) type Manager struct { mu sync.Mutex tables map[string]*Table machines map[string]*Machine rng *rand.Rand } func NewManager(seed int64) *Manager { return &Manager{ tables: make(map[string]*Table), machines: make(map[string]*Machine), rng: rand.New(rand.NewSource(seed)), } } func tableKey(roomID int, tableID string) string { return fmt.Sprintf("%d:%s", roomID, tableID) } // EnsureMachine returns nil for an invalid or unsupported configuration. // When a machine already exists for the room/ID, its live rules are // replaced by the new configuration ONLY while it is idle (no seated // player, so no wager can be discarded); an occupied machine keeps its // live rules until it becomes idle again. func (m *Manager) EnsureMachine(roomID int, cfg MachineConfig) *Machine { if cfg.Validate() != nil { return nil } cfg = cfg.Normalize() key := tableKey(roomID, cfg.ID) m.mu.Lock() defer m.mu.Unlock() if machine := m.machines[key]; machine != nil { if machine.Config == cfg { return machine } if machine.idle() { if next := newMachine(roomID, cfg, rand.New(rand.NewSource(m.rng.Int63()))); next != nil { m.machines[key] = next return next } // Fall through to keep the live machine rather than drop a // functioning game for an invalid replacement. } return machine } // Only the manager consumes its RNG, under m.mu. Each game owns an // independent stream protected by that game's lock. machine := newMachine(roomID, cfg, rand.New(rand.NewSource(m.rng.Int63()))) m.machines[key] = machine return machine } // EnsureTable returns nil for an invalid or unsupported configuration. // When a table already exists for the room/ID, its live rules are replaced // only while the table is idle (no seated players, so no wagers can be // silently discarded); an occupied table keeps its live rules until idle. func (m *Manager) EnsureTable(roomID int, cfg TableConfig) *Table { if cfg.Validate() != nil { return nil } cfg = cfg.Normalize() key := tableKey(roomID, cfg.ID) m.mu.Lock() defer m.mu.Unlock() if table := m.tables[key]; table != nil { if table.Config == cfg { return table } if table.idle() { next := newTable(roomID, cfg, rand.New(rand.NewSource(m.rng.Int63()))) if next != nil { m.tables[key] = next return next } // Fall through to keep the live table rather than drop a // functioning game for an invalid replacement. } return table } table := newTable(roomID, cfg, rand.New(rand.NewSource(m.rng.Int63()))) if table == nil { return nil } m.tables[key] = table return table } func (m *Manager) Table(roomID int, tableID string) *Table { m.mu.Lock() defer m.mu.Unlock() return m.tables[tableKey(roomID, tableID)] } // snapshotTables copies the live table list under the lock so callers can // iterate without holding it (table methods take their own locks). func (m *Manager) snapshotTables() []*Table { m.mu.Lock() tables := make([]*Table, 0, len(m.tables)) for _, table := range m.tables { tables = append(tables, table) } m.mu.Unlock() return tables } func (m *Manager) snapshotMachines() []*Machine { m.mu.Lock() machines := make([]*Machine, 0, len(m.machines)) for _, machine := range m.machines { machines = append(machines, machine) } m.mu.Unlock() return machines } func (m *Manager) Tick() []Event { tables := m.snapshotTables() sort.Slice(tables, func(i, j int) bool { if tables[i].RoomID != tables[j].RoomID { return tables[i].RoomID < tables[j].RoomID } return tables[i].Config.ID < tables[j].Config.ID }) var events []Event for _, table := range tables { events = append(events, table.tick()...) } machines := m.snapshotMachines() sort.Slice(machines, func(i, j int) bool { if machines[i].RoomID != machines[j].RoomID { return machines[i].RoomID < machines[j].RoomID } return machines[i].Config.ID < machines[j].Config.ID }) for _, machine := range machines { events = append(events, machine.tick()...) } return events } // withMachine runs fn against the named machine when it exists. func (m *Manager) withMachine(roomID int, machineID string, fn func(*Machine) []Event) []Event { m.mu.Lock() machine := m.machines[tableKey(roomID, machineID)] m.mu.Unlock() if machine == nil { return nil } return fn(machine) } func (m *Manager) JoinMachine(roomID int, cfg MachineConfig, name string) []Event { machine := m.EnsureMachine(roomID, cfg) if machine == nil { return []Event{{RoomID: roomID, TableID: cfg.ID, Type: EventResult, PrivateTo: name, Message: "That game is not available here."}} } return machine.join(name) } func (m *Manager) SetMachineBet(roomID int, machineID, name string, amount int, wallet Wallet) []Event { return m.withMachine(roomID, machineID, func(machine *Machine) []Event { return machine.setBet(name, amount, wallet) }) } func (m *Manager) StartMachine(roomID int, machineID, name string, wallet Wallet) []Event { return m.withMachine(roomID, machineID, func(machine *Machine) []Event { return machine.start(name, wallet) }) } func (m *Manager) EnableMachineAutoBet(roomID int, machineID, name string, wallet Wallet) []Event { return m.withMachine(roomID, machineID, func(machine *Machine) []Event { return machine.enableAutoBet(name, wallet) }) } func (m *Manager) StopMachineAutoBet(roomID int, machineID, name string) ([]Event, bool) { stopped := false events := m.withMachine(roomID, machineID, func(machine *Machine) []Event { events, ok := machine.stopAutoBet(name) stopped = ok return events }) return events, stopped } func (m *Manager) LeaveMachine(roomID int, machineID, name string) []Event { return m.withMachine(roomID, machineID, func(machine *Machine) []Event { return machine.leave(name) }) } func (m *Manager) Join(roomID int, cfg TableConfig, name string) []Event { table := m.EnsureTable(roomID, cfg) if table == nil { return []Event{{RoomID: roomID, TableID: cfg.ID, Type: EventResult, PrivateTo: name, Message: "That game is not available here."}} } return table.join(name) } func (m *Manager) Leave(roomID int, tableID, name string) []Event { if table := m.Table(roomID, tableID); table != nil { return table.leave(name) } return nil } func (m *Manager) Bet(roomID int, tableID, name, spot string, amount int, wallet Wallet) []Event { if table := m.Table(roomID, tableID); table != nil { return table.bet(name, spot, amount, wallet) } return []Event{{RoomID: roomID, TableID: tableID, Type: EventResult, Player: name, PrivateTo: name, Message: "That game is not available here."}} } func (m *Manager) Action(roomID int, tableID, name, action string) []Event { if table := m.Table(roomID, tableID); table != nil { return table.action(name, action) } return []Event{{RoomID: roomID, TableID: tableID, Type: EventResult, PrivateTo: name, Message: "That game is not available here."}} } func (m *Manager) Rebet(roomID int, tableID, name string) (string, int) { if table := m.Table(roomID, tableID); table != nil { return table.rebet(name) } return "", 0 } // MarkDisconnected removes the player from every table and machine, // forfeiting any wager they had committed, so seats free up immediately. func (m *Manager) MarkDisconnected(name string) []Event { var events []Event for _, table := range m.snapshotTables() { events = append(events, table.markDisconnected(name)...) } for _, machine := range m.snapshotMachines() { events = append(events, machine.markDisconnected(name)...) } return events } func (m *Manager) Participation(name string) (*Table, bool) { m.mu.Lock() defer m.mu.Unlock() for _, table := range m.tables { if table.hasParticipant(name) { return table, true } } for _, machine := range m.machines { if machine.hasParticipant(name) { return nil, true } } return nil, false } func (m *Manager) MachineFor(name string) (*Machine, bool) { m.mu.Lock() defer m.mu.Unlock() for _, machine := range m.machines { if machine.hasParticipant(name) { return machine, true } } return nil, false } func (m *Manager) MachineBusy(name string) bool { m.mu.Lock() defer m.mu.Unlock() for _, machine := range m.machines { if machine.isBusy(name) { return true } } return false } // RefundAll resolves every outstanding wager by refunding committed stakes // to their players. Graceful shutdown uses this: rounds cannot settle, so // no player may lose a stake to the shutdown itself. Refunds go through the // wallet (durable, journaled) and return the refund events for delivery. func (m *Manager) RefundAll() []Event { var events []Event for _, table := range m.snapshotTables() { events = append(events, table.refundAll()...) } for _, machine := range m.snapshotMachines() { events = append(events, machine.refundAll()...) } return events } // RekeyRooms remaps every room-keyed table and machine per idMap // (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. func (m *Manager) RekeyRooms(idMap map[int]int) { if len(idMap) == 0 { return } m.mu.Lock() defer m.mu.Unlock() rekey := func(key string) (string, bool) { var roomID int var tableID string if _, err := fmt.Sscanf(key, "%d:%s", &roomID, &tableID); err != nil { return "", false } newID, ok := idMap[roomID] if !ok || newID == roomID { return "", false } return fmt.Sprintf("%d:%s", newID, tableID), true } movedTables := make(map[string]*Table, len(m.tables)) for key, table := range m.tables { if newKey, ok := rekey(key); ok { var roomID int var tableID string fmt.Sscanf(key, "%d:%s", &roomID, &tableID) table.RoomID = idMap[roomID] table.game.setRoom(table.RoomID) movedTables[newKey] = table delete(m.tables, key) } } for newKey, table := range movedTables { m.tables[newKey] = table } movedMachines := make(map[string]*Machine, len(m.machines)) for key, machine := range m.machines { if newKey, ok := rekey(key); ok { var roomID int var tableID string fmt.Sscanf(key, "%d:%s", &roomID, &tableID) machine.RoomID = idMap[roomID] movedMachines[newKey] = machine delete(m.machines, key) } } for newKey, machine := range movedMachines { m.machines[newKey] = machine } } // EvictRoom removes every cached table and machine in roomID (graceful // shutdown semantics for deleted/renumbered rooms): outstanding wagers are // refunded through each game's refundAll before the entries drop. func (m *Manager) EvictRoom(roomID int) []Event { prefix := fmt.Sprintf("%d:", roomID) var events []Event m.mu.Lock() for key, table := range m.tables { if strings.HasPrefix(key, prefix) { events = append(events, table.refundAll()...) delete(m.tables, key) } } for key, machine := range m.machines { if strings.HasPrefix(key, prefix) { events = append(events, machine.refundAll()...) delete(m.machines, key) } } m.mu.Unlock() return events }