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" { sess.WriteLine(g.renderCasinoRules(sess, t.Config)) return } g.emitCasinoEvents(t.Status(sess.Player.Name)) if t.Config.Game == casino.GameRoulette { sess.WriteLine(g.rouletteReminder(sess)) } 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, watch baccarat, or watch roulette.") 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.RouletteSnapshot: for _, name := range s.Players { if name == sess.Player.Name { return true } } 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))) }