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 } type baccaratTable struct { mu sync.Mutex tableBase Rules BaccaratConfig participants map[string]*baccaratPlayer order []string phase baccaratPhase timer countdown 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, participants: make(map[string]*baccaratPlayer), phase: baccaratWaiting, shoe: newShoe(rules.Decks, rules.ShuffleAt, rng), } } func (t *baccaratTable) seats() []*Participant { seats := make([]*Participant, 0, len(t.order)) for _, name := range t.order { if p := t.participants[name]; p != nil { seats = append(seats, &p.Participant) } } return seats } 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 p := t.participants[name]; p != nil { p.Connected = true return []Event{t.menuEvent(name)} } if len(t.participants) >= t.Config.MaxPlayers { return []Event{t.private(name, "That baccarat table is full.")} } t.participants[name] = &baccaratPlayer{Participant: Participant{Name: name, Connected: true}} t.order = append(t.order, name) 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.participants[name] if p == nil { return nil } if t.phase != baccaratWaiting { return []Event{t.private(name, "You're in the middle of a baccarat round!")} } delete(t.participants, name) for i, n := range t.order { if n == name { t.order = append(t.order[:i], t.order[i+1:]...) break } } events := []Event{t.playerAction(name, fmt.Sprintf("%s stands up.", name), "You stand up.")} if len(t.participants) == 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.participants[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 between %d and %d.", 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 len(t.participants) > 1 && t.phase == baccaratWaiting { t.phase = baccaratBetting t.timer.start(t.Config.BettingTicks) events = append(events, t.event(fmt.Sprintf("Baccarat betting is open for %d ticks.", t.Config.BettingTicks), true)) } if allBet(t.seats()) { 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 } t.timer.tick() if allBet(t.seats()) { return t.deal() } if !t.timer.expired() { return nil } var events []Event for _, name := range sitOutNames(t.seats()) { events = append(events, t.event(fmt.Sprintf("%s sits out this baccarat round.", name), true)) } if anyBet(t.seats()) { 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.participants[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 _, name := range t.order { p := t.participants[name] if p == nil || !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(name, result, payout) settlement := t.playerAction(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 _, name := range t.order { if p := t.participants[name]; p != nil && p.Connected { events = append(events, t.menuEvent(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 _, name := range t.order { p := t.participants[name] if p == nil || !p.HasBet { continue } snapshot.Bets = append(snapshot.Bets, BaccaratBetView{Name: name, Spot: p.lastSpot, Amount: p.Wager.Total}) } return snapshot } func (t *baccaratTable) resetRound() { t.phase = baccaratWaiting t.timer.stop() clearBets(t.seats()) } func (t *baccaratTable) markDisconnected(name string) []Event { t.mu.Lock() defer t.mu.Unlock() if p := t.participants[name]; p != nil { p.Connected = false } return nil } func (t *baccaratTable) markConnected(name string) { t.mu.Lock() defer t.mu.Unlock() if p := t.participants[name]; p != nil { p.Connected = true } } func (t *baccaratTable) hasParticipant(name string) bool { t.mu.Lock() defer t.mu.Unlock() return t.participants[name] != nil } // 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. 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.Round(float64(bet)*rules.BankerPayout)) } default: if spot == baccaratSpotTie { return "win", bet + int(math.Round(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!" } }