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 }