diff options
| author | historia <[not public]> | 2026-09-15 15:35:52 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-15 15:35:52 -0400 |
| commit | 0b0b25222b3131ada79ff78c28eb8bf07d977deb (patch) | |
| tree | 2ceaef5203595044f66a8f00064cd8e017f8f48d /internal/casino/manager.go | |
| parent | 396c3facb8287b9a34076cae8f3d7b300d366d99 (diff) | |
| download | thehouseoficarus-0b0b25222b3131ada79ff78c28eb8bf07d977deb.tar.gz | |
feat: single-owner game state, transactional lifecycle, durable money on server stop
Diffstat (limited to 'internal/casino/manager.go')
| -rw-r--r-- | internal/casino/manager.go | 122 |
1 files changed, 122 insertions, 0 deletions
diff --git a/internal/casino/manager.go b/internal/casino/manager.go index 38b754f..78e26b0 100644 --- a/internal/casino/manager.go +++ b/internal/casino/manager.go @@ -4,6 +4,7 @@ import ( "fmt" "math/rand" "sort" + "strings" "sync" ) @@ -27,6 +28,10 @@ func tableKey(roomID int, tableID string) string { } // 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 @@ -36,6 +41,17 @@ 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 { + 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 @@ -46,6 +62,9 @@ func (m *Manager) EnsureMachine(roomID int, cfg MachineConfig) *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 @@ -55,6 +74,18 @@ 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 { + 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()))) @@ -257,3 +288,94 @@ func (m *Manager) MachineBusy(name string) bool { } 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 +} |
