package game import ( "regexp" "strings" "unicode/utf8" "thehouseoficarus/internal/casino" "thehouseoficarus/internal/net" ) // casinoCommandVariable matches a command variable: an uppercase run such as // N in "bet N on BET" or MAX in "bet [1-4|MAX]". Literal command text is // lowercase, so every uppercase run is a placeholder the player replaces. var casinoCommandVariable = regexp.MustCompile(`[A-Z]+`) // colorizeCommand paints literal command text in the command color and each // uppercase variable in the distinct variable color. Mode "none" collapses to // plain text, and text without variables gets a single color wrapper. func (g *Game) colorizeCommand(sess *net.Session, text string) string { matches := casinoCommandVariable.FindAllStringIndex(text, -1) if len(matches) == 0 { return g.colorize(sess, "command", text) } var sb strings.Builder last := 0 for _, m := range matches { if m[0] > last { sb.WriteString(g.colorize(sess, "command", text[last:m[0]])) } sb.WriteString(g.colorize(sess, "command_variable", text[m[0]:m[1]])) last = m[1] } if last < len(text) { sb.WriteString(g.colorize(sess, "command", text[last:])) } return sb.String() } // Wrap before Session.WriteLine: generic prose wrapping strips alignment from // overlong lines. Every casino menu uses the same two-space command indent and // hanging description column, capped at 80 even when prose wrapping is off. func (g *Game) casinoCommandColumns(sess *net.Session, commands []casino.MenuCommand) string { limit := 80 if sess.Player != nil { if wrap := sess.Player.OptionInt("wrap_width"); wrap > 0 { limit = min(limit, wrap) } } width := 0 for _, c := range commands { n := utf8.RuneCountInString(c.Command) // Unusually long commands get their own lines instead of squeezing every // other command's description into a tiny column. if n <= limit/2 { width = max(width, n) } } var lines []string for _, c := range commands { if sess.Player != nil && c.Cost > casinoAffordable(sess.Player) { continue } n := utf8.RuneCountInString(c.Command) if n > limit/2 { for _, line := range casinoWrapWords(c.Command, max(1, limit-2)) { lines = append(lines, " "+g.colorizeCommand(sess, line)) } for _, line := range casinoWrapWords(c.Desc, max(1, limit-4)) { lines = append(lines, " "+line) } continue } if strings.TrimSpace(c.Desc) == "" { lines = append(lines, " "+g.colorizeCommand(sess, c.Command)) continue } indent := width + 4 desc := casinoWrapWords(c.Desc, max(1, limit-indent)) cmd := c.Command + strings.Repeat(" ", width-n) lines = append(lines, " "+g.colorizeCommand(sess, cmd)+" "+desc[0]) for _, line := range desc[1:] { lines = append(lines, strings.Repeat(" ", indent)+line) } } return strings.Join(lines, "\n") } // Input is uncolored command/description text; paint only after measuring. func casinoWrapWords(text string, width int) []string { var lines []string line := "" for _, word := range strings.Fields(text) { if line != "" && utf8.RuneCountInString(line)+1+utf8.RuneCountInString(word) > width { lines = append(lines, line) line = "" } runes := []rune(word) for len(runes) > width { lines = append(lines, string(runes[:width])) runes = runes[width:] } if line != "" { line += " " } line += string(runes) } if line != "" { lines = append(lines, line) } return lines }