aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/craps_clock.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/casino/craps_clock.go')
-rw-r--r--internal/casino/craps_clock.go120
1 files changed, 120 insertions, 0 deletions
diff --git a/internal/casino/craps_clock.go b/internal/casino/craps_clock.go
new file mode 100644
index 0000000..cb43731
--- /dev/null
+++ b/internal/casino/craps_clock.go
@@ -0,0 +1,120 @@
+package casino
+
+import (
+ "fmt"
+ "time"
+)
+
+const (
+ crapsBetDelay = 10 * time.Second
+ crapsDealerDelay = 60 * time.Second
+ crapsAutoDelay = 3 * time.Second
+)
+
+func crapsSeconds(d time.Duration) int {
+ if d <= 0 {
+ return 0
+ }
+ return int((d + time.Second - 1) / time.Second)
+}
+func (t *crapsTable) shooter() *crapsPlayer {
+ if len(t.queue) == 0 {
+ return nil
+ }
+ return t.players.get(t.queue[0])
+}
+func (t *crapsTable) hasLiveBets() bool {
+ for _, p := range t.players.ordered() {
+ if p.HasBet {
+ return true
+ }
+ }
+ return false
+}
+func (t *crapsTable) activePlayers() int {
+ n := 0
+ for _, p := range t.players.ordered() {
+ if p.Connected && !p.SittingOut {
+ n++
+ }
+ }
+ return n
+}
+
+// Called under the table lock whenever seats or wagers change. Only entering
+// multiplayer or starting a new round opens a window; adding bets never
+// extends it. A solo player has no forced deadline at all.
+func (t *crapsTable) syncRoundClock() []Event {
+ multi := t.activePlayers() > 1
+ if !t.hasLiveBets() {
+ t.phase = phaseWaiting
+ t.roundElapsed = 0
+ t.lastCall = false
+ t.multiplayer = multi
+ return nil
+ }
+ fresh := t.phase == phaseWaiting || (multi && !t.multiplayer)
+ t.multiplayer = multi
+ if fresh {
+ t.roundElapsed = 0
+ t.lastCall = false
+ }
+ t.phase = phasePlaying
+ if multi && t.roundElapsed < crapsBetDelay {
+ t.phase = phaseBetting
+ }
+ if fresh && multi {
+ return []Event{t.event("{casino_emphasis}Bets open{/} — 10 seconds before the shooter can roll. Betting stays open until the dice roll.", true)}
+ }
+ return nil
+}
+
+func (t *crapsTable) tickElapsed(elapsed time.Duration) []Event {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+ events := t.syncRoundClock()
+ if elapsed <= 0 || !t.hasLiveBets() {
+ return events
+ }
+ t.roundElapsed += elapsed
+ p := t.shooter()
+ if p == nil {
+ return events
+ }
+ if p.autoRoll && p.HasBet {
+ p.autoElapsed += elapsed
+ }
+ if t.multiplayer && !t.lastCall && t.roundElapsed >= crapsBetDelay-3*time.Second {
+ t.lastCall = true
+ events = append(events, t.event(fmt.Sprintf("{casino_warning}Last call for bets! 3 seconds until %s can shoot!{/}", p.Name), true))
+ }
+ if t.multiplayer && t.roundElapsed < crapsBetDelay {
+ return events
+ }
+ if t.phase == phaseBetting {
+ events = append(events, t.event(fmt.Sprintf("{casino_emphasis}%s can shoot!{/} Bets stay open until the roll. Dealer rolls in 60 seconds if needed.", p.Name), true))
+ }
+ t.phase = phasePlaying
+ if (p.autoRoll && p.HasBet && p.autoElapsed >= crapsAutoDelay) || (t.multiplayer && t.roundElapsed >= crapsBetDelay+crapsDealerDelay) {
+ return append(events, t.roll()...)
+ }
+ return events
+}
+
+// StopAutoRoll is atomic with ticking and does not queue a departure or touch
+// any wager. A second stop, in manual mode, retains normal casino leave behavior.
+func (t *Table) StopAutoRoll(name string) ([]Event, bool) {
+ game, ok := t.game.(*crapsTable)
+ if !ok {
+ return nil, false
+ }
+ game.mu.Lock()
+ defer game.mu.Unlock()
+ p := game.players.get(name)
+ if p == nil || !p.autoRoll {
+ return nil, false
+ }
+ p.autoRoll = false
+ p.autoElapsed = 0
+ return []Event{game.private(name, "{casino_emphasis}Autoroll OFF{/} — manual play resumed. Your bets stay on the table.")}, true
+}