aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/table.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/casino/table.go')
-rw-r--r--internal/casino/table.go217
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
+}