diff options
| author | historia <[not public]> | 2026-10-06 12:57:23 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-10-06 12:57:23 -0400 |
| commit | 9666676766bf21364f4fc4d0d1d336e72f2a91b4 (patch) | |
| tree | 0cade34091d7602790dc38cd44053f0b74bbadf2 /internal/game/render_cards.go | |
| parent | 4bc1daaf259e0c19c5fe4a12ec8bc7f6e5a735c1 (diff) | |
| download | thehouseoficarus-9666676766bf21364f4fc4d0d1d336e72f2a91b4.tar.gz | |
refactor: split casino.go, dedup helpers, remove audit docs
Diffstat (limited to 'internal/game/render_cards.go')
| -rw-r--r-- | internal/game/render_cards.go | 330 |
1 files changed, 330 insertions, 0 deletions
diff --git a/internal/game/render_cards.go b/internal/game/render_cards.go new file mode 100644 index 0000000..ca1fc62 --- /dev/null +++ b/internal/game/render_cards.go @@ -0,0 +1,330 @@ +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:])) +} |
