diff options
| author | historia <[not public]> | 2026-10-10 04:08:09 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-10-10 04:08:09 -0400 |
| commit | 9fb1f9010526100e12dd1ab1b0ca98401bb6ed75 (patch) | |
| tree | 4a128211bfb0dd429bbb559a591d6c4be25c7757 /internal/casino/roulette.go | |
| parent | ced77d7c89129da6ba4001ab8e3dad265aa41396 (diff) | |
| download | thehouseoficarus-main.tar.gz | |
Diffstat (limited to 'internal/casino/roulette.go')
| -rw-r--r-- | internal/casino/roulette.go | 148 |
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() } |
