package casino import ( "fmt" "math" "math/rand" "sync" ) type baccaratPhase string const ( baccaratWaiting baccaratPhase = "waiting" baccaratBetting baccaratPhase = "betting" ) // baccaratSpots are the outcomes a participant may wager on. const ( baccaratSpotPlayer = "player" baccaratSpotBanker = "banker" baccaratSpotTie = "tie" ) func baccaratSpotValid(spot string) bool { return spot == baccaratSpotPlayer || spot == baccaratSpotBanker || spot == baccaratSpotTie } type baccaratPlayer struct { Participant lastBet int lastSpot string } func (p *baccaratPlayer) participantPtr() *Participant { return &p.Participant } type baccaratTable struct { mu sync.Mutex tableBase Rules BaccaratConfig players seatSet[*baccaratPlayer] phase baccaratPhase window bettingWindow shoe *shoe } func newBaccaratTable(roomID int, cfg TableConfig, rng *rand.Rand) *baccaratTable { cfg = cfg.Normalize() rules := cfg.Baccarat.Normalize() return &baccaratTable{ tableBase: tableBase{RoomID: roomID, Config: cfg}, Rules: rules, players: newSeatSet[*baccaratPlayer](), phase: baccaratWaiting, shoe: newShoe(rules.Decks, rules.ShuffleAt, rng), } } func (t *baccaratTable) menuEvent(name string) Event { e := t.private(name, t.menu()) e.Color = "casino_menu" e.Menu = &MenuView{Kind: MenuBaccaratBet, Commands: []MenuCommand{ {Command: "bet ", Desc: "Place a wager (min/max accepted)"}, {Command: "bet on player|banker|tie", Desc: "Wager on a specific outcome"}, {Command: "stop", Desc: "Leave the table"}, }} return e } func (t *baccaratTable) menu() string { return "New baccarat hand! Place your bet." } func (t *baccaratTable) join(name string) []Event { t.mu.Lock() defer t.mu.Unlock() if t.players.has(name) { return []Event{t.menuEvent(name)} } if t.players.count() >= t.Config.MaxPlayers { return []Event{t.private(name, "That baccarat table is full.")} } t.players.add(name, &baccaratPlayer{Participant: Participant{Name: name, Connected: true}}) events := []Event{t.playerAction(name, fmt.Sprintf("%s sits down.", name), "You sit down.")} events = append(events, t.menuEvent(name)) return events } func (t *baccaratTable) leave(name string) []Event { t.mu.Lock() defer t.mu.Unlock() p := t.players.get(name) if p == nil { return nil } // A wager is committed for the round in flight; everyone else may stand // up whenever they like. if p.HasBet { return []Event{t.private(name, "You're in the middle of a baccarat round!")} } t.players.remove(name) events := []Event{t.playerAction(name, fmt.Sprintf("%s stands up.", name), "You stand up.")} if t.players.count() == 0 { events = append(events, t.event("The baccarat table closes.", true)) } return events } func (t *baccaratTable) bet(name, spot string, amount int, wallet 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 baccarat.")} } if spot == "" { spot = p.lastSpot } if spot == "" { return []Event{t.private(name, "Bet on what? Try \"bet on player\", \"bet on banker\", or \"bet on tie\".")} } if !baccaratSpotValid(spot) { return []Event{t.private(name, "Baccarat bets must be on player, banker, or tie.")} } if p.HasBet { return []Event{t.private(name, "You have already bet this round.")} } if amount < t.Config.MinBet || amount > t.Config.MaxBet { return []Event{t.private(name, fmt.Sprintf("Baccarat bets must be %s.", betRangeText(t.Config.MinBet, t.Config.MaxBet)))} } wager, ok := wallet.Wager(amount) if !ok { return []Event{t.private(name, "You don't have enough chips and credits for that bet.")} } p.HasBet, p.Wager, p.Wallet, p.lastBet, p.lastSpot = true, wager, wallet, amount, spot events := []Event{t.playerAction(name, fmt.Sprintf("%s bets %d credits on %s.", name, amount, spot), fmt.Sprintf("You bet %d credits on %s.", amount, spot))} if t.players.count() > 1 && t.phase == baccaratWaiting { t.phase = baccaratBetting t.window.open(t.Config.BettingTicks) events = append(events, t.event(fmt.Sprintf("Baccarat betting is open for %d ticks.", t.Config.BettingTicks), true)) } if allBet(t.players.participants()) { events = append(events, t.deal()...) } return events } func (t *baccaratTable) tick() []Event { t.mu.Lock() defer t.mu.Unlock() if t.phase != baccaratBetting { return nil } sitOuts, resolve, pending := t.window.advance(t.players.participants()) if pending { return nil } var events []Event for _, name := range sitOuts { events = append(events, t.event(fmt.Sprintf("%s sits out this baccarat round.", name), true)) } if resolve { events = append(events, t.deal()...) } else { t.resetRound() } return events } func (t *baccaratTable) action(name, action string) []Event { return []Event{t.private(name, "That game has no player actions.")} } func (t *baccaratTable) rebet(name string) (string, int) { if p := t.players.get(name); p != nil && p.lastBet > 0 { return p.lastSpot, p.lastBet } return "", t.Config.MinBet } // deal plays out the hand by the punto banco tableau, settles every wager, // and resets the table for the next round. func (t *baccaratTable) deal() []Event { player, banker := playTableau(t.shoe) playerTotal, bankerTotal := baccaratTotal(player), baccaratTotal(banker) round := t.event("New baccarat hand!", true) round.Baccarat = t.snapshot(player, banker) events := []Event{round, t.event(baccaratOutcomeMessage(playerTotal, bankerTotal), true)} for _, p := range t.players.ordered() { if !p.HasBet { continue } result, payout := settleBaccaratBet(p.lastSpot, p.Wager.Total, playerTotal, bankerTotal, t.Rules) if payout > 0 && p.Wallet != nil { p.Wallet.PayCredits(payout) } publicResult, privateResult := baccaratResultMessages(p.Name, result, payout) settlement := t.playerAction(p.Name, publicResult, privateResult) if result == "win" { if p.lastSpot == baccaratSpotTie { settlement.Color = "casino_jackpot_win" } else { settlement.Color = "casino_big_win" } } events = append(events, settlement) } t.resetRound() for _, p := range t.players.ordered() { if p.Connected { events = append(events, t.menuEvent(p.Name)) } } return events } func (t *baccaratTable) snapshot(player, banker []card) *BaccaratSnapshot { snapshot := &BaccaratSnapshot{ PlayerScore: fmt.Sprintf("%d", baccaratTotal(player)), BankerScore: fmt.Sprintf("%d", baccaratTotal(banker)), } for _, c := range player { snapshot.Player = append(snapshot.Player, CardView{Rank: c.rank, Suit: c.suit}) } for _, c := range banker { snapshot.Banker = append(snapshot.Banker, CardView{Rank: c.rank, Suit: c.suit}) } for _, p := range t.players.ordered() { if !p.HasBet { continue } snapshot.Bets = append(snapshot.Bets, BaccaratBetView{Name: p.Name, Spot: p.lastSpot, Amount: p.Wager.Total}) } return snapshot } func (t *baccaratTable) resetRound() { t.phase = baccaratWaiting t.window.close() clearBets(t.players.participants()) } // markDisconnected removes the player from the table, forfeiting any wager // they committed (deal only settles seated bettors). An open betting window // resolves or resets on the next tick now that they no longer hold it open. func (t *baccaratTable) markDisconnected(name string) []Event { t.mu.Lock() defer t.mu.Unlock() if !t.players.has(name) { return nil } t.players.remove(name) events := []Event{t.event(fmt.Sprintf("%s stands up.", name), true)} if t.players.count() == 0 { events = append(events, t.event("The baccarat table closes.", true)) } return events } func (t *baccaratTable) hasParticipant(name string) bool { t.mu.Lock() defer t.mu.Unlock() return t.players.has(name) } // baccaratTotal scores a hand modulo 10. func baccaratTotal(hand []card) int { total := 0 for _, c := range hand { total += c.baccaratValue() } return total % 10 } // playTableau deals the initial four cards and applies the punto banco // drawing rules, returning the final player and banker hands. func playTableau(sh *shoe) (player, banker []card) { player = []card{sh.draw(), sh.draw()} banker = []card{sh.draw(), sh.draw()} playerTotal, bankerTotal := baccaratTotal(player), baccaratTotal(banker) if playerTotal >= 8 || bankerTotal >= 8 { // A natural 8 or 9 ends the hand immediately. return player, banker } playerThird := -1 if playerTotal <= 5 { c := sh.draw() playerThird = c.baccaratValue() player = append(player, c) } if playerThird < 0 { if bankerTotal <= 5 { banker = append(banker, sh.draw()) } return player, banker } if bankerDraws(bankerTotal, playerThird) { banker = append(banker, sh.draw()) } return player, banker } // bankerDraws applies the banker tableau when the player took a third card. func bankerDraws(bankerTotal, playerThird int) bool { switch bankerTotal { case 0, 1, 2: return true case 3: return playerThird != 8 case 4: return playerThird >= 2 && playerThird <= 7 case 5: return playerThird >= 4 && playerThird <= 7 case 6: return playerThird == 6 || playerThird == 7 default: // 7 return false } } // settleBaccaratBet returns the result label and total payout (stake plus // winnings) for one wager. Player and banker bets push on a tie. Winnings // round down to whole credits, so the banker commission effectively rounds // up: never bet less than one commission unit. func settleBaccaratBet(spot string, bet int, playerTotal, bankerTotal int, rules BaccaratConfig) (string, int) { switch { case playerTotal > bankerTotal: if spot == baccaratSpotPlayer { return "win", bet * 2 } case bankerTotal > playerTotal: if spot == baccaratSpotBanker { return "win", bet + int(math.Floor(float64(bet)*rules.BankerPayout)) } default: if spot == baccaratSpotTie { return "win", bet + int(math.Floor(float64(bet)*rules.TiePayout)) } return "push", bet } return "loss", 0 } func baccaratOutcomeMessage(playerTotal, bankerTotal int) string { switch { case playerTotal > bankerTotal: return fmt.Sprintf("Player wins %d over %d.", playerTotal, bankerTotal) case bankerTotal > playerTotal: return fmt.Sprintf("Banker wins %d over %d.", bankerTotal, playerTotal) default: return fmt.Sprintf("Player and banker tie at %d.", playerTotal) } } func baccaratResultMessages(name, result string, payout int) (string, string) { switch result { case "win": return name + " wins!", fmt.Sprintf("You win! +%d credits", payout) case "push": return name + " pushes.", fmt.Sprintf("You push. +%d credits", payout) default: return name + " loses.", "You lose!" } }