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
}
|