aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/roulette.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/casino/roulette.go')
-rw-r--r--internal/casino/roulette.go148
1 files changed, 124 insertions, 24 deletions
diff --git a/internal/casino/roulette.go b/internal/casino/roulette.go
index 71a0916..254a472 100644
--- a/internal/casino/roulette.go
+++ b/internal/casino/roulette.go
@@ -459,8 +459,15 @@ func rouletteBasket(nums []string, double bool) ([]string, int, bool) {
return nil, 0, false
}
+type roulettePlacedBet struct {
+ spot string
+ wager Wager
+}
+
type roulettePlayer struct {
Participant
+ bets []roulettePlacedBet
+ lastBets []rebetWager
lastSpot string
lastBet int
}
@@ -519,8 +526,8 @@ func (t *rouletteTable) menuEvent(name string, board bool) Event {
p := t.players.get(name)
if t.phase != phasePlaying && p != nil && !p.HasBet {
commands = append(commands, MenuCommand{Command: "bet N on BET", Desc: "Place one wager"})
- if p.lastBet > 0 {
- commands = append(commands, MenuCommand{Command: "bet / rebet", Desc: "Repeat your previous wager"})
+ if len(p.lastBets) > 0 {
+ commands = append(commands, MenuCommand{Command: "bet / rebet", Desc: "Repeat all your last wagers"})
}
}
if p != nil {
@@ -564,15 +571,20 @@ func (t *rouletteTable) leave(name string) []Event {
func (t *rouletteTable) bet(name, spot string, amount int, w Wallet) []Event {
t.mu.Lock()
defer t.mu.Unlock()
+ events, _ := t.betLocked(name, spot, amount, w)
+ return events
+}
+
+// betLocked is the locked implementation of bet, shared with the multi-wager
+// replay so a rebet holds the table lock across every stake. It reports false
+// when no wager was placed.
+func (t *rouletteTable) betLocked(name, spot string, amount int, w Wallet) ([]Event, bool) {
p := t.players.get(name)
if p == nil {
- return []Event{t.private(name, "You aren't playing roulette.")}
+ return []Event{t.private(name, "You aren't playing roulette.")}, false
}
if t.phase == phasePlaying {
- return []Event{t.private(name, "No more bets! The wheel is spinning.")}
- }
- if p.HasBet {
- return []Event{t.private(name, "You have already bet this spin.")}
+ return []Event{t.private(name, "No more bets! The wheel is spinning.")}, false
}
if spot == "" {
spot = p.lastSpot
@@ -580,21 +592,24 @@ func (t *rouletteTable) bet(name, spot string, amount int, w Wallet) []Event {
spot = normalizeRouletteSpot(spot)
resolved, ok := rouletteWagerFor(spot, t.Config.Roulette.DoubleZero, t.Config.Roulette.Racetrack)
if !ok {
- return []Event{t.private(name, "Unknown roulette target. Use table to see all available bets.")}
+ return []Event{t.private(name, "Unknown roulette target. Use table to see all available bets.")}, false
}
if amount < t.Config.MinBet || amount > t.Config.MaxBet {
- return []Event{t.private(name, "Roulette bets must be "+betRangeText(t.Config.MinBet, t.Config.MaxBet)+".")}
+ return []Event{t.private(name, "Roulette bets must be "+betRangeText(t.Config.MinBet, t.Config.MaxBet)+".")}, false
}
// Check every possible winning return before committing even an unlimited
// wager: a call bet's components pay different multiples.
if err := resolved.validate(amount); err != nil {
- return []Event{t.private(name, "That wager's potential payout is too large.")}
+ return []Event{t.private(name, "That wager's potential payout is too large.")}, false
}
wager, err := w.Wager(amount)
if err != nil {
- return []Event{t.private(name, walletErrorMessage(err))}
+ return []Event{t.private(name, walletErrorMessage(err))}, false
}
- p.HasBet, p.Wager, p.Wallet, p.lastSpot, p.lastBet, p.SittingOut = true, wager, w, spot, amount, false
+ p.bets = append(p.bets, roulettePlacedBet{spot: spot, wager: wager})
+ p.HasBet, p.Wallet, p.lastSpot, p.lastBet, p.SittingOut = true, w, spot, amount, false
+ p.Wager.Total += amount
+ p.Wager.ID = wager.ID
events := []Event{t.playerAction(name, fmt.Sprintf("%s bets %d on %s.", name, amount, spot), fmt.Sprintf("You bet %d on %s.", amount, spot))}
// With two or more players a timed betting window lets the table bet
// together. A lone player has no time limit: once their wager lands,
@@ -611,7 +626,7 @@ func (t *rouletteTable) bet(name, spot string, amount int, w Wallet) []Event {
events = append(events, t.startSpin())
}
}
- return events
+ return events, true
}
func (t *rouletteTable) tick() []Event {
t.mu.Lock()
@@ -729,23 +744,39 @@ func (t *rouletteTable) settle() []Event {
if !p.HasBet {
continue
}
- wager, resolved := rouletteWagerFor(p.lastSpot, t.Config.Roulette.DoubleZero, t.Config.Roulette.Racetrack)
- payout := 0
+ totalPayout := 0
result := "loss"
+ var ids []string
+ var lines []SettlementLine
var err error
- if resolved {
- if share, ok := wager.covers(pocket); ok {
+ for _, b := range p.bets {
+ ids = append(ids, b.wager.ID)
+ wager, resolved := rouletteWagerFor(b.spot, t.Config.Roulette.DoubleZero, t.Config.Roulette.Racetrack)
+ payout := 0
+ res := "loss"
+ if resolved {
+ if share, ok := wager.covers(pocket); ok {
+ res = "win"
+ payout, err = share.payout(b.wager.Total)
+ }
+ }
+ if err != nil {
+ break
+ }
+ totalPayout += payout
+ if res == "win" {
result = "win"
- payout, err = share.payout(p.Wager.Total)
}
+ lines = append(lines, SettlementLine{Label: b.spot, Odds: rouletteSettlementOdds(wager, pocket), Stake: b.wager.Total, Returned: payout, Result: res})
}
- // PayCredits supports a zero return and atomically consumes a losing
- // stake too, surfacing persistence errors instead of announcing success.
+ // PayCredits supports a zero return and atomically consumes every
+ // losing stake too, surfacing persistence errors instead of
+ // announcing success.
failed := err != nil
if !failed {
- failed = p.Wallet.PayCredits(payout, p.Wager.ID) != nil
+ failed = p.Wallet.PayCredits(totalPayout, ids...) != nil
}
- text := resultTextLabel(p.lastSpot, result, p.Wager.Total, payout)
+ text := settlementText(lines)
public := p.Name + " " + result + "."
if failed {
text = payoutFailedMessage()
@@ -756,7 +787,7 @@ func (t *rouletteTable) settle() []Event {
// departures/sit-outs need a balance attached to the settlement.
event.ShowBalance = p.SettlementBalanceNeeded(failed)
if !failed {
- event.Settlement = []SettlementLine{{Label: p.lastSpot, Odds: rouletteSettlementOdds(wager, pocket), Stake: p.Wager.Total, Returned: payout, Result: result}}
+ event.Settlement = lines
if result == "win" {
event.Color = "casino_result_win"
} else {
@@ -778,6 +809,13 @@ func (t *rouletteTable) resetRound() {
t.phase = phaseWaiting
t.window.close()
t.spinRemaining = 0
+ for _, p := range t.players.ordered() {
+ p.lastBets = p.lastBets[:0]
+ for _, b := range p.bets {
+ p.lastBets = append(p.lastBets, rebetWager{spot: b.spot, amount: b.wager.Total})
+ }
+ p.bets = nil
+ }
clearBets(t.players.participants())
}
func (t *rouletteTable) action(name, action string) []Event {
@@ -795,6 +833,38 @@ func (t *rouletteTable) action(name, action string) []Event {
}
return []Event{t.private(name, "Roulette has no player actions after betting.")}
}
+
+// unbetLocked returns every stake the seat committed this round. Roulette
+// allows several wagers in one spin, so it shadows the shared single-stake
+// helper.
+func (t *rouletteTable) unbetLocked(name string) ([]Event, bool) {
+ p := t.players.get(name)
+ if p == nil {
+ return []Event{t.private(name, "You aren't seated at this table.")}, true
+ }
+ if t.phase != phaseWaiting && t.phase != phaseBetting {
+ return []Event{t.private(name, "You can only take back bets before the round starts.")}, true
+ }
+ if !p.HasBet || len(p.bets) == 0 {
+ return []Event{t.private(name, "Nothing to take back.")}, true
+ }
+ total := 0
+ var ids []string
+ for _, b := range p.bets {
+ total += b.wager.Total
+ if b.wager.ID != "" {
+ ids = append(ids, b.wager.ID)
+ }
+ }
+ if p.Wallet != nil {
+ if err := p.Wallet.RefundStakes(ids...); err != nil {
+ return []Event{t.private(name, payoutFailedMessage())}, true
+ }
+ }
+ p.HasBet, p.Wager, p.bets = false, Wager{}, nil
+ return []Event{t.private(name, fmt.Sprintf("You take back %d credits.", total))}, true
+}
+
func (t *rouletteTable) rebet(name string) (string, int) {
t.mu.Lock()
defer t.mu.Unlock()
@@ -803,6 +873,32 @@ func (t *rouletteTable) rebet(name string) (string, int) {
}
return "", t.Config.MinBet
}
+
+// rebetAll repeats every remembered wager from the previous spin in one
+// command, so a bare bet/rebet restores a whole spread.
+func (t *rouletteTable) rebetAll(name string, w Wallet) []Event {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+ p := t.players.get(name)
+ if p == nil {
+ return []Event{t.private(name, "You aren't playing roulette.")}
+ }
+ if t.phase == phasePlaying {
+ return []Event{t.private(name, "No more bets! The wheel is spinning.")}
+ }
+ if len(p.lastBets) == 0 {
+ return []Event{t.private(name, "Bet on a number, color, or combination. Use table to see all available bets.")}
+ }
+ var events []Event
+ for _, b := range p.lastBets {
+ ev, ok := t.betLocked(name, b.spot, b.amount, w)
+ events = append(events, ev...)
+ if !ok {
+ break
+ }
+ }
+ return events
+}
func (t *rouletteTable) status(name string) []Event {
t.mu.Lock()
defer t.mu.Unlock()
@@ -830,7 +926,11 @@ func (t *rouletteTable) markDisconnected(name string) []Event {
if p.Wallet != nil {
var err error
if p.HasBet && t.phase == phasePlaying {
- err = p.Wallet.PayCredits(0, p.Wager.ID)
+ var ids []string
+ for _, b := range p.bets {
+ ids = append(ids, b.wager.ID)
+ }
+ err = p.Wallet.PayCredits(0, ids...)
} else {
_, _, err = p.Wallet.RefundOutstanding()
}