package casino import ( "fmt" "math/rand" "sort" "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) } func (m *Manager) EnsureMachine(roomID int, cfg MachineConfig) *Machine { cfg = cfg.Normalize() key := tableKey(roomID, cfg.ID) m.mu.Lock() defer m.mu.Unlock() if machine := m.machines[key]; machine != nil { return machine } machine := newMachine(roomID, cfg, m.rng) m.machines[key] = machine return machine } // EnsureTable returns the table for cfg, or nil when cfg.Game has no // multiplayer table implementation. func (m *Manager) EnsureTable(roomID int, cfg TableConfig) *Table { cfg = cfg.Normalize() key := tableKey(roomID, cfg.ID) m.mu.Lock() defer m.mu.Unlock() if table := m.tables[key]; table != nil { return table } table := newTable(roomID, cfg, m.rng) 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 { return m.EnsureMachine(roomID, cfg).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 }