package game import ( "fmt" "strings" "unicode/utf8" "thehouseoficarus/internal/cardart" "thehouseoficarus/internal/casino" "thehouseoficarus/internal/color" ) func renderBlackjackSnapshotFor(snapshot *casino.BlackjackSnapshot, mode string, unicode bool, size int, style string, art *cardart.Store, viewer string) string { if snapshot == nil { return "" } size = cardRenderSize(unicode, size) var lines []string dealerTitle := "Dealer:" if snapshot.DealerRevealed { dealerTitle = "Dealer (" + snapshot.DealerScore + "):" } else if snapshot.DealerShownScore != "" { dealerTitle = "Dealer (" + snapshot.DealerShownScore + "):" } 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 { 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") } // renderBaccaratSnapshot draws the player and banker hands followed by the // list of wagers riding on the round. func renderBaccaratSnapshot(snapshot *casino.BaccaratSnapshot, mode string, unicode bool, size int, style string, art *cardart.Store) string { if snapshot == nil { return "" } size = cardRenderSize(unicode, size) var lines []string 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))) } return strings.Join(lines, "\n") } func cardLine(mode, text string) string { spec := color.NoColor() spec.Fg = 39 return color.Render(mode, spec, text) } func cardsLine(mode string, cards []casino.CardView, size int, unicode bool, style string, art *cardart.Store) []string { if len(cards) == 0 { return nil } if size == 1 { parts := make([]string, len(cards)) for i, card := range cards { 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, " ")} } artLines := make([][]string, len(cards)) for i, card := range cards { artLines[i] = cardArtLines(card, size, unicode, style, art) } lines := make([]string, size) for row := 0; row < size; row++ { var lineParts []string for i, card := range artLines { lineParts = append(lineParts, colorizeCardLine(mode, card[row], cards[i], row, size, style)) } lines[row] = strings.Join(lineParts, " ") } return lines } // renderCardRow lays cards side by side, each column padded to its own art // or label width, and appends an optional centered label row. minWidth lets // the caller force a floor (video poker's HELD labels need it) while Pai Gow // passes 0. It is the shared "cards with numbers underneath" presentation. func renderCardRow(cards []casino.CardView, labels []string, minWidth int, mode string, unicode bool, size int, style string, art *cardart.Store) []string { if len(cards) == 0 { return nil } artLines := make([][]string, len(cards)) widths := make([]int, len(cards)) height := 0 for i, card := range cards { artLines[i] = cardsLine(mode, []casino.CardView{card}, size, unicode, style, art) height = max(height, len(artLines[i])) widths[i] = minWidth for _, line := range artLines[i] { widths[i] = max(widths[i], color.VisibleLen(line)) } if i < len(labels) { widths[i] = max(widths[i], color.VisibleLen(labels[i])) } } lines := make([]string, 0, height+1) for row := 0; row < height; row++ { parts := make([]string, len(cards)) for i := range cards { text := "" if row < len(artLines[i]) { text = artLines[i][row] } parts[i] = rouletteCenter(text, widths[i]) } lines = append(lines, strings.Join(parts, " ")) } if len(labels) > 0 { parts := make([]string, len(cards)) for i := range cards { text := "" if i < len(labels) { text = labels[i] } parts[i] = rouletteCenter(text, widths[i]) } lines = append(lines, strings.Join(parts, " ")) } return lines } func cardRenderSize(unicode bool, size int) int { if size == 1 { return 1 } if !unicode { return 5 } if size == 5 || size == 7 || size == 9 { return size } return 7 } var suitSymbols = map[string]string{"hearts": "♥", "spades": "♠", "diamonds": "♦", "clubs": "♣", "joker": "★"} func cardSymbol(card casino.CardView) string { if card.Hidden { return "?" } return card.Rank + suitSymbols[card.Suit] } func cardArtLines(card casino.CardView, size int, unicode bool, style string, art *cardart.Store) []string { if card.Hidden { return generatedCardArt("?", "", size, unicode, style) } key := card.Rank if card.Rank == "JK" { return jokerCardArt(size, unicode, style, art) } if card.Suit != "" { key += strings.ToUpper(card.Suit[:1]) } if configured := art.Art(key, size); len(configured) == size { if unicode && style == "light" { // The shared pip art draws a dark box frame, which leaves the top // and bottom rows looking like the dark style. Replace only those // two rows with the half-block edges the white body meets. configured = append([]string(nil), configured...) configured[0] = strings.Repeat("▄", utf8.RuneCountInString(configured[0])) configured[size-1] = strings.Repeat("▀", utf8.RuneCountInString(configured[size-1])) } return configured } return generatedCardArt(card.Rank, card.Suit, size, unicode, style) } func generatedCardArt(rank, suit string, size int, unicode bool, style string) []string { if size < 5 { size = 5 } border := "-" left, right := "+", "+" if unicode { border, left, right = "─", "┌", "┐" } bottomLeft, bottomRight := left, right if unicode { bottomLeft, bottomRight = "└", "┘" } vertical := "|" if unicode { vertical = "│" } if unicode && style == "light" { lines := []string{strings.Repeat("▄", size)} lines = append(lines, generatedCardBody(rank, suit, size, unicode, vertical)...) return append(lines, strings.Repeat("▀", size)) } lines := []string{left + strings.Repeat(border, size-2) + right} lines = append(lines, generatedCardBody(rank, suit, size, unicode, vertical)...) lines = append(lines, bottomLeft+strings.Repeat(border, size-2)+bottomRight) return lines } func generatedCardBody(rank, suit string, size int, unicode bool, vertical string) []string { inner := size - 2 lines := make([]string, 0, size-2) for row := 1; row < size-1; row++ { content := strings.Repeat(" ", inner) if row == 1 { content = rank + strings.Repeat(" ", inner-len(rank)) } if row == (size-1)/2 { suitText := "" if suit != "" { suitText = suitLetter(suit, unicode) } suitWidth := utf8.RuneCountInString(suitText) leftPadding := (inner - suitWidth) / 2 content = strings.Repeat(" ", leftPadding) + suitText + strings.Repeat(" ", inner-leftPadding-suitWidth) } if row == size-2 { content = strings.Repeat(" ", inner-len(rank)) + rank } lines = append(lines, vertical+content+vertical) } return lines } func suitLetter(suit string, unicode bool) string { if !unicode { return strings.ToUpper(suit[:1]) } return suitSymbols[suit] } func colorizeCardLine(mode, line string, card casino.CardView, row, size int, style string) string { red := card.Suit == "hearts" || card.Suit == "diamonds" rankColor := 27 if red { rankColor = 196 } suitColor := 27 if red { suitColor = 196 } rankLine := row == 1 || row == size-2 light := style == "light" bodyBackground := light && row > 0 && row < size-1 var builder strings.Builder for _, r := range line { if strings.ContainsRune("┌─┐└┘│┬┴├┤┼╔╗╚╝═║╦╩╠╣╬▀▄+|-", r) { spec := color.ColorSpec{Fg: 15} if bodyBackground { spec.Bg = 15 } builder.WriteString(color.Render(mode, spec, string(r))) } else if r == ' ' { if bodyBackground { builder.WriteString(color.Render(mode, color.ColorSpec{Fg: 15, Bg: 15}, " ")) } else { builder.WriteRune(r) } } else { value := suitColor if rankLine { value = rankColor } spec := color.ColorSpec{Fg: value} if bodyBackground { spec.Bg = 15 } builder.WriteString(color.Render(mode, spec, string(r))) } } return builder.String() } func colorizeCardSymbol(mode string, card casino.CardView) string { if card.Hidden { return color.Render(mode, color.ColorSpec{Fg: 15}, "?") } rankColor, suitColor := 27, 27 if card.Suit == "hearts" || card.Suit == "diamonds" { rankColor, suitColor = 196, 196 } runes := []rune(cardSymbol(card)) rankLength := len([]rune(card.Rank)) return color.Render(mode, color.ColorSpec{Fg: rankColor}, string(runes[:rankLength])) + color.Render(mode, color.ColorSpec{Fg: suitColor}, string(runes[rankLength:])) }