package casino import ( "fmt" "strings" ) // RulesText is the shared rules view for table commands and room fixtures. func (c TableConfig) RulesText() string { switch c.Game { case GameBlackjack: return c.BlackjackRulesText() case GameBaccarat: return c.BaccaratRulesText() case GameRoulette: rules := c.Label() + "\nStraight numbers pay 35:1; splits 17:1; streets and trios 11:1; corners 8:1; lines 5:1.\n" + "1to12/13to24/25to36 and rows 2:1; other outside bets 1:1; the American basket pays 6:1.\n" + "Combos: bet 10 on split 1 2 | bet 10 on street 1 2 3 | bet 10 on corner 1 2 4 5 |\n" + "bet 10 on line 1 2 3 4 5 6 | bet 10 on trio 0 1 2 | bet 10 on basket.\n" if c.Roulette.Racetrack { rules += "Racetrack (single zero): bet 10 on voisins | tiers | orphelins | jeu zero | neighbors 17.\n" } return rules + "Zero pockets lose all outside bets. One wager per spin. Use help roulette." default: return c.Label() } } func (c TableConfig) Label() string { if c.Game == GameRoulette { label := "Roulette / Single zero" if c.Roulette.DoubleZero { label = "Roulette / Double zero" } if c.Roulette.Racetrack { label += " / Racetrack" } return label } if c.Game != GameBaccarat { return gameTitle(c.Game) } switch c.Baccarat.Normalize().Variant { case BaccaratSuper6: return "Baccarat / Super 6" case BaccaratEZ: return "Baccarat / EZ" default: return "Baccarat / Commission" } } func (c *tableCore[P]) overview() string { c.mu.Lock() defer c.mu.Unlock() return c.overviewLocked() } func (c *tableCore[P]) overviewLocked() string { // Concise enough to sit beside a look minimap without wrapping into it. // The betting countdown lives in the menu heading and warning lines. return fmt.Sprintf("%s | Limits %s | %d/%d seated", c.Config.Label(), compactBetRange(c.Config.MinBet, c.Config.MaxBet), c.players.count(), c.Config.MaxPlayers) } // compactBetRange renders the wager range in the short form used by the // one-line table overview (the verbose form stays in the betting menu). func compactBetRange(minBet, maxBet int) string { if maxBet == maxBetUnlimited { return fmt.Sprintf("%d+", minBet) } return fmt.Sprintf("%d-%d", minBet, maxBet) } func (c *tableCore[P]) warning(name, text string) Event { e := c.private(name, text) e.Color = "casino_warning" return e } // controlLocked queues departures without changing committed hands or stakes. func (c *tableCore[P]) controlLocked(name, action string) ([]Event, bool) { if action != "leave_after" && action != "sitout" { return nil, false } for _, p := range c.players.participants() { if p.Name != name { continue } if action == "leave_after" { p.LeaveAfter = true return []Event{c.private(name, "You’ll leave after this round. Your current hand still needs your actions.")}, true } if p.HasBet { p.SitOutAfter = true return []Event{c.private(name, "You’ll sit out after this round. Your current wager and actions are unchanged.")}, true } p.SittingOut = true return []Event{c.private(name, "You’re sitting out. Place a bet to rejoin; stop to leave.")}, true } return []Event{c.private(name, "You aren’t seated at this table.")}, true } // Called only after resetRound, when seat removal cannot disturb active indices. func (c *tableCore[P]) finishControlsLocked() []Event { var events []Event for _, p := range c.players.participants() { if p.LeaveAfter { c.removeSeatLocked(p.Name) events = append(events, c.playerAction(p.Name, p.Name+" stands up.", "Round complete. You stand up.")) } else if p.SitOutAfter { p.SitOutAfter = false p.SittingOut = true events = append(events, c.private(p.Name, "You’re now sitting out. Place a bet to rejoin.")) } } return c.closeIfEmptyLocked(events) } func (c *tableCore[P]) seatStatusLocked(name string) string { for _, p := range c.players.participants() { if p.Name != name { continue } if p.LeaveAfter { return "Leaving after this round" } if p.SitOutAfter { return "Sitting out after this round" } if p.SittingOut { return "Sitting out | Bet to rejoin" } if p.HasBet { return fmt.Sprintf("Wager committed: %d", p.Wager.Total) } } var waiting []string for _, p := range c.players.participants() { if p.Connected && !p.SittingOut && !p.HasBet { waiting = append(waiting, p.Name) } } if len(waiting) > 0 { return "Waiting for bets: " + strings.Join(waiting, ", ") } return "" } func (t *blackjackTable) status(name string) []Event { t.mu.Lock() defer t.mu.Unlock() e := t.private(name, t.overviewLocked()) if text := t.seatStatusLocked(name); text != "" { e.Message += "\n" + text } events := []Event{e} switch t.phase { case blackjackPlaying: events[0].Snapshot = t.snapshot(false) if t.activeName() == name { events = append(events, t.turnMenuLocked(name)) } else { events[0].Message += "\n" + t.activeName() + "’s turn | You are waiting" } case blackjackInsurance: events[0].Snapshot = t.snapshot(false) if p := t.players.get(name); p != nil && p.HasBet && !p.declinedInsurance && p.insurance == 0 { events = append(events, t.insuranceMenuLocked(p)) } default: if p := t.players.get(name); p != nil && !p.HasBet { events = append(events, t.menuEvent(name)) } } return events } func (t *baccaratTable) status(name string) []Event { t.mu.Lock() defer t.mu.Unlock() e := t.private(name, t.overviewLocked()) if text := t.seatStatusLocked(name); text != "" { e.Message += "\n" + text } events := []Event{e} if p := t.players.get(name); p != nil && !p.HasBet { events = append(events, t.menuEvent(name)) } return events } func (t *blackjackTable) insuranceMenuLocked(p *blackjackPlayer) Event { cost := p.hands[0].bet / 2 e := t.private(p.Name, "Insurance decision") e.Color = "casino_menu" e.Menu = &MenuView{Heading: fmt.Sprintf("Insurance | Costs %d | Pays %g:1 | %d ticks remaining", cost, t.Rules.InsurancePayout, t.timer.remaining()), Commands: []MenuCommand{{Command: "insurance", Desc: "Buy insurance", Cost: cost}, {Command: "decline", Desc: "Continue without insurance (also the timeout default)"}}} if cost == 0 { e.Menu.Commands = e.Menu.Commands[1:] e.Menu.Heading = "Bet too small to insure | decline to continue" } return e } func resultText(result string, stake, returned int) string { label := strings.ToUpper(result) switch result { case "blackjack": label = "BLACKJACK" case "bust", "loss", "dealer blackjack": label = "LOSS" } return fmt.Sprintf("%s | Net %+d credits | %d returned (stake included)", label, returned-stake, returned) }