aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/table_ux.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-24 20:06:41 -0400
committerhistoria <[not public]>2026-09-24 20:07:31 -0400
commit13db3e50cb6e29eedbf25416e5284bf08b0adefb (patch)
treed5e9b99030b25cc7fc102288b694c133f8832f84 /internal/casino/table_ux.go
parentd114c747a7672c7e4daab3a5b5f1b2f066ebc912 (diff)
downloadthehouseoficarus-13db3e50cb6e29eedbf25416e5284bf08b0adefb.tar.gz
feat: casino table ux: aliases, rebet hints, deadline notifications, spectator mode
Diffstat (limited to 'internal/casino/table_ux.go')
-rw-r--r--internal/casino/table_ux.go180
1 files changed, 180 insertions, 0 deletions
diff --git a/internal/casino/table_ux.go b/internal/casino/table_ux.go
new file mode 100644
index 0000000..9543d91
--- /dev/null
+++ b/internal/casino/table_ux.go
@@ -0,0 +1,180 @@
+package casino
+
+import (
+ "fmt"
+ "strings"
+)
+
+func (c TableConfig) Label() string {
+ 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 {
+ state := "Betting open"
+ switch c.phase {
+ case phaseBetting:
+ state = fmt.Sprintf("Betting closes in %d ticks", c.window.timer.remaining())
+ case phasePlaying:
+ state = "Round in progress"
+ case phaseInsurance:
+ state = "Insurance decisions"
+ }
+ return fmt.Sprintf("%s | Limits %s | %d/%d seated | %s", c.Config.Label(), betRangeText(c.Config.MinBet, c.Config.MaxBet), c.players.count(), c.Config.MaxPlayers, state)
+}
+
+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)
+}