aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/craps_clock.go
blob: cb43731d0c851d25e7bd9b57145f4c06084de869 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
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
}