aboutsummaryrefslogtreecommitdiff
path: root/internal/game/casino.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/game/casino.go
parentd114c747a7672c7e4daab3a5b5f1b2f066ebc912 (diff)
downloadthehouseoficarus-13db3e50cb6e29eedbf25416e5284bf08b0adefb.tar.gz
feat: casino table ux: aliases, rebet hints, deadline notifications, spectator mode
Diffstat (limited to 'internal/game/casino.go')
-rw-r--r--internal/game/casino.go192
1 files changed, 163 insertions, 29 deletions
diff --git a/internal/game/casino.go b/internal/game/casino.go
index 698cb78..579371c 100644
--- a/internal/game/casino.go
+++ b/internal/game/casino.go
@@ -340,15 +340,36 @@ func (g *Game) casinoMachine(roomID int, gameName string) (*casino.Machine, bool
func (g *Game) executePlay(sess *net.Session, args []string, rawInput string) {
p := sess.Player
- if p == nil || len(args) == 0 {
- sess.WriteLine("Play what?")
+ if p == nil {
return
}
if _, playing := g.Casino.Participation(p.Name); playing {
sess.WriteLine("You're already playing a game. Type \"stop\" between rounds to leave it.")
return
}
- g.startCasinoGame(sess, p, args[0])
+ choices := g.casinoChoices(p.RoomID)
+ input := ""
+ if len(args) > 0 {
+ input = args[0]
+ }
+ choice, ok := resolveCasinoChoice(choices, input)
+ if !ok {
+ g.showCasinoChoices(sess, choices)
+ return
+ }
+ if len(args) > 1 {
+ amount, err := strconv.Atoi(args[1])
+ if !choice.table || len(args) > 3 || (args[1] != "min" && args[1] != "max" && (err != nil || amount <= 0)) {
+ sess.WriteLine("Use play <game> [amount|min|max] [target]. No wager was placed.")
+ return
+ }
+ }
+ g.startCasinoGame(sess, p, choice.address)
+ if len(args) > 1 {
+ if t, joined := g.Casino.Participation(p.Name); joined && t != nil {
+ g.executeBet(sess, args[1:], rawInput)
+ }
+ }
}
func (g *Game) startCasinoGame(sess *net.Session, p *player.Player, gameName string) {
@@ -411,8 +432,12 @@ func (g *Game) executeBet(sess *net.Session, args []string, rawInput string) {
// next token is always the target. That makes numeric targets reachable
// ("bet 5 on 17"), which roulette, craps, and keno need.
amount, spot, hasAmount, sawOn := 0, "", false, false
- for _, arg := range args {
+ for i, arg := range args {
if strings.EqualFold(arg, "on") {
+ if sawOn || spot != "" || i == len(args)-1 {
+ sess.WriteLine("Use bet <amount> on <target>. No wager was placed.")
+ return
+ }
sawOn = true
continue
}
@@ -453,6 +478,16 @@ func (g *Game) executeBet(sess *net.Session, args []string, rawInput string) {
}
spot = strings.ToLower(arg)
}
+ if table.Config.Game == casino.GameBaccarat {
+ switch spot {
+ case "b":
+ spot = "banker"
+ case "p":
+ spot = "player"
+ case "t":
+ spot = "tie"
+ }
+ }
if !hasAmount {
var lastSpot string
lastSpot, amount = g.Casino.Rebet(table.RoomID, table.Config.ID, p.Name)
@@ -512,11 +547,19 @@ func (g *Game) emitCasinoEvents(events []casino.Event) {
if event.PrivateTo != "" && sess.Player != nil && sess.Player.Name == event.PrivateTo {
continue
}
- message := event.Message
- if rendered := g.renderCasinoSnapshot(sess, event.Snapshot); rendered != "" {
- message += "\n" + rendered
+ details := g.seesCasinoDetails(sess, event)
+ if event.Detail && !details {
+ continue
+ }
+ message := g.casinoTimeText(event.Message)
+ if details {
+ if rendered := g.renderCasinoSnapshot(sess, event.Snapshot); rendered != "" {
+ message += "\n" + rendered
+ }
+ }
+ if message != "" {
+ sess.WriteLine(g.colorize(sess, category, message))
}
- sess.WriteLine(g.colorize(sess, category, message))
}
}
if event.PrivateTo == "" {
@@ -526,7 +569,7 @@ func (g *Game) emitCasinoEvents(events []casino.Event) {
if text == "" {
text = event.Message
}
- if text == "" && event.Menu == nil {
+ if text == "" && event.Menu == nil && event.Snapshot == nil {
continue
}
g.charsMu.Lock()
@@ -535,10 +578,13 @@ func (g *Game) emitCasinoEvents(events []casino.Event) {
if sess == nil {
continue
}
- privateMessage := g.casinoPrivateMessage(sess, category, event.Menu, text)
+ privateMessage := g.casinoPrivateMessage(sess, category, event.Menu, g.casinoTimeText(text))
if rendered := g.renderCasinoSnapshot(sess, event.Snapshot); rendered != "" {
privateMessage += "\n" + rendered
}
+ if event.ShowBalance {
+ privateMessage += "\n" + g.casinoBalance(sess)
+ }
sess.WriteLine(g.renderCasinoMessage(sess, category, event.Menu, privateMessage))
if category == "casino_menu" {
g.writePrompt(sess)
@@ -561,15 +607,55 @@ func (g *Game) casinoPrivateMessage(sess *net.Session, category string, menu *ca
}
balance := ""
if menu.ShowBalance {
- chips := g.colorize(sess, "casino_chips", fmt.Sprintf("%d", countChips(sess.Player)))
- credits := g.colorize(sess, "casino_credits", fmt.Sprintf("%d", sess.Player.Credits))
- balance = fmt.Sprintf("You have %s chips and %s credits.", chips, credits)
+ balance = g.casinoBalance(sess)
}
- heading := menu.Heading
+ heading := g.casinoTimeText(menu.Heading)
if heading == "" {
heading = message
}
- return g.renderCasinoCommands(sess, balance, heading, menu.Commands)
+ commands := append([]casino.MenuCommand(nil), menu.Commands...)
+ for i := range commands {
+ if commands[i].Cost > casinoAffordable(sess.Player) {
+ commands[i].Desc += " [unavailable: insufficient funds]"
+ }
+ }
+ if sess.Player.OptionString("casino_display") == "compact" && menu.Repeat {
+ var repeat string
+ for _, c := range commands {
+ if c.Command == "bet" {
+ repeat = g.colorize(sess, "command", "bet / rebet") + " " + c.Desc
+ break
+ }
+ }
+ return g.colorize(sess, "casino_action", heading) + "\n" + balance + "\n " + repeat + "\n " + g.colorize(sess, "command", "bet <amount> [target] | bet min/max | table | sitout | stop")
+ }
+ if sess.Player.OptionString("casino_display") == "compact" && !menu.ShowBalance {
+ var hints []string
+ for _, c := range commands {
+ label := c.Command
+ switch c.Command {
+ case "hit":
+ label = "h:hit"
+ case "stand":
+ label = "st:stand"
+ case "double":
+ label = "dd:double"
+ case "split":
+ label = "sp:split"
+ }
+ if c.Cost > 0 {
+ label += fmt.Sprintf(" (+%d)", c.Cost)
+ }
+ category := "command"
+ if c.Cost > casinoAffordable(sess.Player) {
+ label += " [unavailable]"
+ category = "casino_warning"
+ }
+ hints = append(hints, g.colorize(sess, category, label))
+ }
+ return g.colorize(sess, "casino_action", heading) + "\n " + strings.Join(hints, " ")
+ }
+ return g.renderCasinoCommands(sess, balance, heading, commands)
}
// renderCasinoSnapshot renders a game's structured view for one session. It is
@@ -582,13 +668,17 @@ func (g *Game) renderCasinoSnapshot(sess *net.Session, snapshot casino.Snapshot)
}
mode := g.colorMode(sess)
unicode := sess.Player.OptionBool("unicode")
+ size := sess.Player.OptionInt("card_size")
+ if sess.Player.OptionString("casino_display") == "compact" {
+ size = 1
+ }
switch s := snapshot.(type) {
case *casino.SlotSnapshot:
return renderSlotSnapshot(s, mode, unicode)
case *casino.BlackjackSnapshot:
- return renderBlackjackSnapshot(s, mode, unicode, sess.Player.OptionInt("card_size"), sess.Player.OptionString("card_style"), g.CardArt)
+ return renderBlackjackSnapshotFor(s, mode, unicode, size, sess.Player.OptionString("card_style"), g.CardArt, sess.Player.Name)
case *casino.BaccaratSnapshot:
- return renderBaccaratSnapshot(s, mode, unicode, sess.Player.OptionInt("card_size"), sess.Player.OptionString("card_style"), g.CardArt)
+ return renderBaccaratSnapshot(s, mode, unicode, size, sess.Player.OptionString("card_style"), g.CardArt)
}
return ""
}
@@ -603,7 +693,7 @@ func (g *Game) renderCasinoCommands(sess *net.Session, balance, heading string,
sb.WriteString("\n\n")
}
if heading != "" {
- sb.WriteString(heading)
+ sb.WriteString(g.colorize(sess, "casino_action", heading))
sb.WriteString("\n\n")
}
width := 0
@@ -614,7 +704,11 @@ func (g *Game) renderCasinoCommands(sess *net.Session, balance, heading string,
}
for _, c := range commands {
padded := c.Command + strings.Repeat(" ", width-utf8.RuneCountInString(c.Command))
- cmd := g.colorize(sess, "command", padded)
+ category := "command"
+ if sess.Player != nil && c.Cost > casinoAffordable(sess.Player) {
+ category = "casino_warning"
+ }
+ cmd := g.colorize(sess, category, padded)
if c.Desc != "" {
fmt.Fprintf(&sb, " %s %s\n", cmd, c.Desc)
} else {
@@ -624,7 +718,7 @@ func (g *Game) renderCasinoCommands(sess *net.Session, balance, heading string,
return strings.TrimRight(sb.String(), "\n")
}
-func renderBlackjackSnapshot(snapshot *casino.BlackjackSnapshot, mode string, unicode bool, size int, style string, art *cardart.Store) string {
+func renderBlackjackSnapshotFor(snapshot *casino.BlackjackSnapshot, mode string, unicode bool, size int, style string, art *cardart.Store, viewer string) string {
if snapshot == nil {
return ""
}
@@ -636,11 +730,35 @@ func renderBlackjackSnapshot(snapshot *casino.BlackjackSnapshot, mode string, un
} else if snapshot.DealerShownScore != "" {
dealerTitle = "Dealer (" + snapshot.DealerShownScore + "):"
}
- lines = append(lines, cardLine(mode, dealerTitle))
- lines = append(lines, cardsLine(mode, snapshot.Dealer, size, unicode, style, art)...)
+ if size == 1 {
+ lines = append(lines, cardLine(mode, dealerTitle)+" "+strings.Join(cardsLine(mode, snapshot.Dealer, size, unicode, style, art), " "))
+ } else {
+ lines = append(lines, cardLine(mode, dealerTitle))
+ lines = append(lines, cardsLine(mode, snapshot.Dealer, size, unicode, style, art)...)
+ }
for _, player := range snapshot.Players {
- lines = append(lines, cardLine(mode, fmt.Sprintf("%s (%s):", player.Name, player.Score)))
- lines = append(lines, cardsLine(mode, player.Cards, size, unicode, style, art)...)
+ name := player.Name
+ if name == viewer {
+ name = "You"
+ }
+ marker := " "
+ if player.Active {
+ marker = "> "
+ }
+ detail := ""
+ if player.Hand > 0 {
+ detail = fmt.Sprintf(" | Hand %d/%d | Wager %d | %s", player.Hand, player.Hands, player.Bet, player.Status)
+ }
+ if player.Active && player.Name != viewer {
+ detail = strings.ReplaceAll(detail, "Your turn", "Playing")
+ }
+ title := fmt.Sprintf("%s%s (%s)%s:", marker, name, player.Score, detail)
+ if size == 1 {
+ lines = append(lines, cardLine(mode, title)+" "+strings.Join(cardsLine(mode, player.Cards, size, unicode, style, art), " "))
+ } else {
+ lines = append(lines, cardLine(mode, title))
+ lines = append(lines, cardsLine(mode, player.Cards, size, unicode, style, art)...)
+ }
}
return strings.Join(lines, "\n")
}
@@ -653,10 +771,14 @@ func renderBaccaratSnapshot(snapshot *casino.BaccaratSnapshot, mode string, unic
}
size = cardRenderSize(unicode, size)
var lines []string
- lines = append(lines, cardLine(mode, "Player ("+snapshot.PlayerScore+"):"))
- lines = append(lines, cardsLine(mode, snapshot.Player, size, unicode, style, art)...)
- lines = append(lines, cardLine(mode, "Banker ("+snapshot.BankerScore+"):"))
- lines = append(lines, cardsLine(mode, snapshot.Banker, size, unicode, style, art)...)
+ if size == 1 {
+ lines = append(lines, cardLine(mode, "Player ("+snapshot.PlayerScore+"):")+" "+strings.Join(cardsLine(mode, snapshot.Player, size, unicode, style, art), " "), cardLine(mode, "Banker ("+snapshot.BankerScore+"):")+" "+strings.Join(cardsLine(mode, snapshot.Banker, size, unicode, style, art), " "))
+ } else {
+ lines = append(lines, cardLine(mode, "Player ("+snapshot.PlayerScore+"):"))
+ lines = append(lines, cardsLine(mode, snapshot.Player, size, unicode, style, art)...)
+ lines = append(lines, cardLine(mode, "Banker ("+snapshot.BankerScore+"):"))
+ lines = append(lines, cardsLine(mode, snapshot.Banker, size, unicode, style, art)...)
+ }
for _, bet := range snapshot.Bets {
lines = append(lines, cardLine(mode, fmt.Sprintf("%s bets %d on %s.", bet.Name, bet.Amount, bet.Spot)))
}
@@ -676,7 +798,16 @@ func cardsLine(mode string, cards []casino.CardView, size int, unicode bool, sty
if size == 1 {
parts := make([]string, len(cards))
for i, card := range cards {
- parts[i] = colorizeCardSymbol(mode, card)
+ if !unicode {
+ if card.Hidden {
+ parts[i] = "[??]"
+ } else {
+ suit := map[string]string{"hearts": "H", "diamonds": "D", "spades": "S", "clubs": "C"}[card.Suit]
+ parts[i] = card.Rank + suit
+ }
+ } else {
+ parts[i] = colorizeCardSymbol(mode, card)
+ }
}
return []string{strings.Join(parts, " ")}
}
@@ -696,6 +827,9 @@ func cardsLine(mode string, cards []casino.CardView, size int, unicode bool, sty
}
func cardRenderSize(unicode bool, size int) int {
+ if size == 1 {
+ return 1
+ }
if !unicode {
return 5
}