diff options
| author | historia <[not public]> | 2026-09-24 20:06:41 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-24 20:07:31 -0400 |
| commit | 13db3e50cb6e29eedbf25416e5284bf08b0adefb (patch) | |
| tree | d5e9b99030b25cc7fc102288b694c133f8832f84 /internal/game/casino_ux.go | |
| parent | d114c747a7672c7e4daab3a5b5f1b2f066ebc912 (diff) | |
| download | thehouseoficarus-13db3e50cb6e29eedbf25416e5284bf08b0adefb.tar.gz | |
feat: casino table ux: aliases, rebet hints, deadline notifications, spectator mode
Diffstat (limited to 'internal/game/casino_ux.go')
| -rw-r--r-- | internal/game/casino_ux.go | 221 |
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))) +} |
