aboutsummaryrefslogtreecommitdiff
path: root/internal/game/casino_ux.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game/casino_ux.go')
-rw-r--r--internal/game/casino_ux.go221
1 files changed, 221 insertions, 0 deletions
diff --git a/internal/game/casino_ux.go b/internal/game/casino_ux.go
new file mode 100644
index 0000000..fdd4533
--- /dev/null
+++ b/internal/game/casino_ux.go
@@ -0,0 +1,221 @@
+package game
+
+import (
+ "fmt"
+ "regexp"
+ "strconv"
+ "strings"
+
+ "thehouseoficarus/internal/casino"
+ "thehouseoficarus/internal/net"
+)
+
+type casinoChoice struct {
+ address string
+ names []string
+ table bool
+}
+
+// Exact addresses beat aliases; prefixes must identify exactly one game.
+func resolveCasinoChoice(choices []casinoChoice, input string) (casinoChoice, bool) {
+ input = strings.ToLower(input)
+ for _, c := range choices {
+ if input != "" && strings.EqualFold(c.address, input) {
+ return c, true
+ }
+ }
+ if input != "" {
+ var exact []casinoChoice
+ for _, c := range choices {
+ for _, name := range c.names {
+ if strings.EqualFold(name, input) {
+ exact = append(exact, c)
+ break
+ }
+ }
+ }
+ if len(exact) == 1 {
+ return exact[0], true
+ }
+ if len(exact) > 1 {
+ return casinoChoice{}, false
+ }
+ }
+ matches := []casinoChoice{}
+ for _, c := range choices {
+ matched := input == ""
+ for _, name := range c.names {
+ if strings.EqualFold(name, input) || (input != "" && strings.HasPrefix(strings.ToLower(name), input)) {
+ matched = true
+ }
+ }
+ if matched {
+ matches = append(matches, c)
+ }
+ }
+ if len(matches) == 1 {
+ return matches[0], true
+ }
+ return casinoChoice{}, false
+}
+
+func (g *Game) casinoChoices(roomID int) []casinoChoice {
+ room, err := g.World.LoadRoom(roomID)
+ if err != nil {
+ return nil
+ }
+ var choices []casinoChoice
+ for _, c := range room.CasinoTables {
+ names := []string{string(c.Game)}
+ switch c.Game {
+ case casino.GameBlackjack:
+ names = append(names, "bj")
+ case casino.GameBaccarat:
+ names = append(names, "bac")
+ }
+ choices = append(choices, casinoChoice{address: string(c.Game), names: names, table: true})
+ }
+ for _, c := range room.CasinoMachines {
+ choices = append(choices, casinoChoice{address: c.ID, names: []string{c.ID, string(c.Game), c.Variant}})
+ }
+ return choices
+}
+
+func (g *Game) showCasinoChoices(sess *net.Session, choices []casinoChoice) {
+ if len(choices) == 0 {
+ sess.WriteLine("There are no casino games here.")
+ return
+ }
+ commands := []casino.MenuCommand{}
+ for _, c := range choices {
+ commands = append(commands, casino.MenuCommand{Command: "play " + c.address, Desc: "Join this game"})
+ }
+ sess.WriteLine(g.renderCasinoCommands(sess, "", "Choose a game (use an unambiguous name or abbreviation):", commands))
+}
+
+func (g *Game) executeTableStatus(sess *net.Session, args []string, _ string) {
+ if sess.Player == nil {
+ return
+ }
+ if len(args) > 0 && args[0] != "rules" && args[0] != "help" {
+ sess.WriteLine("Use table, table rules, or table help.")
+ return
+ }
+ t, playing := g.Casino.Participation(sess.Player.Name)
+ if !playing || t == nil {
+ g.showCasinoChoices(sess, g.casinoChoices(sess.Player.RoomID))
+ return
+ }
+ if len(args) > 0 && args[0] == "rules" {
+ text := t.Config.BlackjackRulesText()
+ if t.Config.Game == casino.GameBaccarat {
+ text = t.Config.BaccaratRulesText()
+ }
+ sess.WriteLine(g.colorize(sess, "casino_action", text))
+ return
+ }
+ g.emitCasinoEvents(t.Status(sess.Player.Name))
+ if len(args) > 0 {
+ sess.WriteLine(g.renderCasinoCommands(sess, "", "Table controls", []casino.MenuCommand{
+ {Command: "bet / rebet", Desc: "Repeat the displayed wager"},
+ {Command: "bet min / bet max", Desc: "Use the table minimum / affordable maximum"},
+ {Command: "h / st / dd / sp", Desc: "Blackjack: hit / stand / double / split (s and d still move)"},
+ {Command: "insurance / decline", Desc: "Answer the insurance offer explicitly"},
+ {Command: "sitout", Desc: "Skip future rounds; bet to rejoin"},
+ {Command: "stop", Desc: "Leave now, or after your committed round"},
+ {Command: "option casino_display compact", Desc: "Short cards and action hints (or detailed for card art)"},
+ {Command: "table rules", Desc: "Review this table’s rules"},
+ }))
+ }
+}
+
+func (g *Game) executeWatch(sess *net.Session, args []string, _ string) {
+ if sess.Player == nil {
+ return
+ }
+ if len(args) == 1 && args[0] == "off" {
+ sess.CasinoWatchTable = ""
+ sess.WriteLine(g.colorize(sess, "casino_action", "You stop watching table details."))
+ return
+ }
+ if len(args) > 1 {
+ sess.WriteLine("Use watch [game] or watch off.")
+ return
+ }
+ choices := []casinoChoice{}
+ for _, c := range g.casinoChoices(sess.Player.RoomID) {
+ if c.table {
+ choices = append(choices, c)
+ }
+ }
+ input := ""
+ if len(args) == 1 {
+ input = args[0]
+ }
+ c, ok := resolveCasinoChoice(choices, input)
+ if !ok {
+ sess.WriteLine("Choose a table to watch: watch blackjack or watch baccarat.")
+ return
+ }
+ t, ok := g.casinoTable(sess.Player.RoomID, c.address)
+ if !ok {
+ return
+ }
+ sess.CasinoWatchRoom = sess.Player.RoomID
+ sess.CasinoWatchTable = t.Config.ID
+ sess.WriteLine(g.colorize(sess, "casino_action", "Watching "+t.Config.Label()+". Detailed public cards are visible; watch off stops watching."))
+ g.emitCasinoEvents(t.Status(sess.Player.Name))
+}
+
+func (g *Game) seesCasinoDetails(sess *net.Session, event casino.Event) bool {
+ if sess.Player == nil {
+ return false
+ }
+ // Slot output is unchanged; watch controls multiplayer tables only.
+ if g.Casino.Table(event.RoomID, event.TableID) == nil {
+ return true
+ }
+ if sess.CasinoWatchRoom == event.RoomID && sess.CasinoWatchTable == event.TableID {
+ return true
+ }
+ // Settlement may have removed a queued departure before these events
+ // are delivered. Their own final cards must still be visible.
+ switch s := event.Snapshot.(type) {
+ case *casino.BlackjackSnapshot:
+ for _, p := range s.Players {
+ if p.Name == sess.Player.Name {
+ return true
+ }
+ }
+ case *casino.BaccaratSnapshot:
+ for _, p := range s.Bets {
+ if p.Name == sess.Player.Name {
+ return true
+ }
+ }
+ }
+ t, ok := g.Casino.Participation(sess.Player.Name)
+ return ok && t != nil && t.RoomID == event.RoomID && t.Config.ID == event.TableID
+}
+
+var casinoTicksPattern = regexp.MustCompile(`([0-9]+) ticks`)
+
+// Casino logic counts engine ticks; present deadlines in actual configured seconds.
+func (g *Game) casinoTimeText(text string) string {
+ ms := 600
+ if g.Ticks != nil {
+ ms = g.Ticks.TickLengthMilliseconds()
+ }
+ return casinoTicksPattern.ReplaceAllStringFunc(text, func(s string) string {
+ n, _ := strconv.Atoi(strings.Fields(s)[0])
+ return fmt.Sprintf("%g seconds", float64(n)*float64(ms)/1000)
+ })
+}
+
+func (g *Game) casinoBalance(sess *net.Session) string {
+ if sess.Player == nil {
+ return ""
+ }
+ p := sess.Player
+ return fmt.Sprintf("Available %s (%s chips + %s credits)", g.colorize(sess, "casino_chips", strconv.Itoa(casinoAffordable(p))), g.colorize(sess, "casino_chips", strconv.Itoa(countChips(p))), g.colorize(sess, "casino_credits", strconv.Itoa(p.Credits)))
+}