diff options
| author | historia <[not public]> | 2026-07-31 17:44:10 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-07-31 17:44:10 -0400 |
| commit | f51424c90dbce7191a31b6e675818c985f0f4539 (patch) | |
| tree | a7845a2e09bd6e83d2d034f5e702a4940f38126a /internal/casino/table.go | |
| parent | 58758d41488a777d2bc0e787be655363bdd9eb60 (diff) | |
| download | thehouseoficarus-f51424c90dbce7191a31b6e675818c985f0f4539.tar.gz | |
feat: casino framework and basic slot machine
Diffstat (limited to 'internal/casino/table.go')
| -rw-r--r-- | internal/casino/table.go | 217 |
1 files changed, 217 insertions, 0 deletions
diff --git a/internal/casino/table.go b/internal/casino/table.go new file mode 100644 index 0000000..1b9d0b5 --- /dev/null +++ b/internal/casino/table.go @@ -0,0 +1,217 @@ +package casino + +import ( + "fmt" + "math/rand" + "sync" +) + +type tablePhase string + +const ( + phaseWaiting tablePhase = "waiting" + phaseBetting tablePhase = "betting" +) + +type Table struct { + mu sync.Mutex + RoomID int + Config TableConfig + + participants map[string]*Participant + order []string + phase tablePhase + deadline int + clock int + rng *rand.Rand +} + +func newTable(roomID int, cfg TableConfig, rng *rand.Rand) *Table { + return &Table{ + RoomID: roomID, + Config: cfg, + phase: phaseWaiting, + participants: make(map[string]*Participant), + rng: rng, + } +} + +func (t *Table) join(name string) []Event { + t.mu.Lock() + defer t.mu.Unlock() + if p := t.participants[name]; p != nil { + p.Connected = true + return []Event{{RoomID: t.RoomID, TableID: t.Config.ID, Type: EventJoined, Player: name, PrivateTo: name, Message: fmt.Sprintf("You resume your place at the %s table.", t.Config.ID)}} + } + if len(t.participants) >= t.Config.MaxPlayers { + return []Event{{RoomID: t.RoomID, TableID: t.Config.ID, Type: EventResult, Player: name, PrivateTo: name, Message: "That table is full."}} + } + t.participants[name] = &Participant{Name: name, Connected: true} + t.order = append(t.order, name) + return []Event{{RoomID: t.RoomID, TableID: t.Config.ID, Type: EventJoined, Player: name, Message: fmt.Sprintf("%s joins the %s table.", name, t.Config.ID), Public: true}} +} + +func (t *Table) leave(name string) []Event { + t.mu.Lock() + defer t.mu.Unlock() + p := t.participants[name] + if p == nil { + return nil + } + if t.phase != phaseWaiting { + return []Event{{RoomID: t.RoomID, TableID: t.Config.ID, Type: EventResult, Player: name, PrivateTo: name, Message: "You're in the middle of a game!"}} + } + delete(t.participants, name) + for i, n := range t.order { + if n == name { + t.order = append(t.order[:i], t.order[i+1:]...) + break + } + } + events := []Event{{RoomID: t.RoomID, TableID: t.Config.ID, Type: EventLeft, Player: name, Message: fmt.Sprintf("%s stops playing at the %s table.", name, t.Config.ID), Public: true}} + if len(t.participants) == 0 { + events = append(events, Event{RoomID: t.RoomID, TableID: t.Config.ID, Type: EventClosed, Message: fmt.Sprintf("The %s table closes.", t.Config.ID), Public: true}) + } + return events +} + +func (t *Table) bet(name string, amount int, wallet Wallet) []Event { + t.mu.Lock() + defer t.mu.Unlock() + p := t.participants[name] + if p == nil { + return []Event{{RoomID: t.RoomID, TableID: t.Config.ID, Type: EventResult, Player: name, PrivateTo: name, Message: "You aren't playing at that table."}} + } + if t.phase != phaseWaiting && t.phase != phaseBetting { + return []Event{{RoomID: t.RoomID, TableID: t.Config.ID, Type: EventResult, Player: name, PrivateTo: name, Message: "That table isn't accepting bets."}} + } + if amount < t.Config.MinBet || amount > t.Config.MaxBet { + return []Event{{RoomID: t.RoomID, TableID: t.Config.ID, Type: EventResult, Player: name, PrivateTo: name, Message: fmt.Sprintf("Bets must be between %d and %d.", t.Config.MinBet, t.Config.MaxBet)}} + } + if p.HasBet { + return []Event{{RoomID: t.RoomID, TableID: t.Config.ID, Type: EventResult, Player: name, PrivateTo: name, Message: "You have already bet this round."}} + } + wager, ok := wallet.Wager(amount) + if !ok { + return []Event{{RoomID: t.RoomID, TableID: t.Config.ID, Type: EventResult, Player: name, PrivateTo: name, Message: "You don't have enough chips and credits for that bet."}} + } + p.HasBet = true + p.Wager = wager + p.Wallet = wallet + if t.phase == phaseWaiting && len(t.participants) > 1 { + t.phase = phaseBetting + t.clock = 0 + t.deadline = t.Config.BettingTicks + } + events := []Event{{RoomID: t.RoomID, TableID: t.Config.ID, Type: EventBetPlaced, Player: name, Message: fmt.Sprintf("%s bets %d credits at the %s table.", name, amount, t.Config.ID), Public: true}} + if t.phase == phaseBetting && t.clock == 0 { + events = append(events, Event{RoomID: t.RoomID, TableID: t.Config.ID, Type: EventBetting, Message: fmt.Sprintf("Betting is open at the %s table for %d ticks.", t.Config.ID, t.deadline), Public: true}) + } + if len(t.participants) == 1 || t.allBet() { + events = append(events, t.resolveRound()...) + } + return events +} + +func (t *Table) tick() []Event { + t.mu.Lock() + defer t.mu.Unlock() + if t.phase != phaseBetting { + return nil + } + t.clock++ + if t.allBet() { + return t.resolveRound() + } + if t.clock < t.deadline { + return nil + } + var events []Event + for _, name := range t.order { + p := t.participants[name] + if p != nil && !p.HasBet { + events = append(events, Event{RoomID: t.RoomID, TableID: t.Config.ID, Type: EventTimedOut, Player: name, Message: fmt.Sprintf("%s sits out this round.", name), Public: true}) + } + } + if t.anyBet() { + events = append(events, t.resolveRound()...) + } else { + t.resetRound() + } + return events +} + +func (t *Table) resolveRound() []Event { + var events []Event + for _, name := range t.order { + p := t.participants[name] + if p == nil || !p.HasBet { + continue + } + result := slotResult(t.rng, p.Wager.Total) + message := fmt.Sprintf("%s spins: %s", name, result.Display) + events = append(events, Event{RoomID: t.RoomID, TableID: t.Config.ID, Type: EventRoundStarted, Player: name, Message: message, Public: true}) + if result.Payout > 0 { + if p.Wallet != nil { + p.Wallet.PayCredits(result.Payout) + } + events = append(events, Event{RoomID: t.RoomID, TableID: t.Config.ID, Type: EventPayout, Player: name, PrivateTo: name, Message: fmt.Sprintf("You win %d credits!", result.Payout)}) + } + } + t.resetRound() + return events +} + +func (t *Table) resetRound() { + t.phase = phaseWaiting + t.clock = 0 + t.deadline = 0 + for _, p := range t.participants { + p.HasBet = false + p.Wager = Wager{} + } +} + +func (t *Table) allBet() bool { + if len(t.participants) == 0 { + return false + } + for _, p := range t.participants { + if !p.HasBet { + return false + } + } + return true +} + +func (t *Table) anyBet() bool { + for _, p := range t.participants { + if p.HasBet { + return true + } + } + return false +} + +func (t *Table) markDisconnected(name string) []Event { + t.mu.Lock() + defer t.mu.Unlock() + if p := t.participants[name]; p != nil { + p.Connected = false + } + return nil +} + +func (t *Table) markConnected(name string) { + t.mu.Lock() + defer t.mu.Unlock() + if p := t.participants[name]; p != nil { + p.Connected = true + } +} + +func (t *Table) hasParticipant(name string) bool { + t.mu.Lock() + defer t.mu.Unlock() + return t.participants[name] != nil +} |
