diff options
Diffstat (limited to 'internal/casino/baccarat.go')
| -rw-r--r-- | internal/casino/baccarat.go | 377 |
1 files changed, 377 insertions, 0 deletions
diff --git a/internal/casino/baccarat.go b/internal/casino/baccarat.go new file mode 100644 index 0000000..729851d --- /dev/null +++ b/internal/casino/baccarat.go @@ -0,0 +1,377 @@ +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} + return e +} + +func (t *baccaratTable) menu() string { + return "New hand! You can bet, bet <amount> on player|banker|tie (inc. min/max), or stop." +} + +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 <amount> on player\", \"bet <amount> on banker\", or \"bet <amount> 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!" + } +} |
