package game import ( "fmt" "strings" "thehouseoficarus/internal/cardart" "thehouseoficarus/internal/casino" "thehouseoficarus/internal/color" ) var ( paiGowWin = color.ColorSpec{Fg: color.NearestXterm256(80, 220, 100), Bold: true} paiGowLoss = color.ColorSpec{Fg: color.NearestXterm256(240, 80, 80), Bold: true} paiGowPush = color.ColorSpec{Fg: 15, Bold: true} ) func renderPaiGowFelt(cfg casino.TableConfig, mode string, unicode bool) string { c := newCrapsCanvas(24) c.box(0, 0, 76, 23) c.center(1, 75, 1, strings.ToUpper(cfg.PaiGow.Label()), crapsGold) c.center(1, 75, 2, "LIMITS "+betLimitsText(cfg.MinBet, cfg.MaxBet), crapsInk) note := "5% COMMISSION" switch cfg.PaiGow.Normalize().Variant { case casino.PaiGowEZ: note = "NO COMMISSION | DEALER QUEEN-HIGH PUSHES" case casino.PaiGowNoPush: note = "NO COMMISSION | SPLIT: HIGHER EXTRA CARD WINS, TIES LOSE" case casino.PaiGowFaceUp: note = "NO COMMISSION | DEALER FACE UP | PUSH ACE HIGH " + strings.ReplaceAll(cfg.PaiGow.PushAceHigh, "_", "/") + ":1" } c.center(1, 75, 3, note, crapsInk) c.box(0, 4, 76, 19) c.text(3, 5, "FORTUNE - BEST OF ALL SEVEN CARDS", crapsGold) c.text(50, 5, "ODDS", crapsGold) envy := "ENVY OFF" if cfg.PaiGow.Envy { envy = "ENVY" } // Center the Envy header and its values over the same narrow field so the // numbers sit under the label instead of hanging off its right edge. const envyLeft, envyRight = 62, 66 c.center(envyLeft, envyRight, 5, envy, crapsGold) for i, row := range casino.FortunePayTable(cfg.PaiGow.Envy) { c.text(3, 6+i, row.Hand, crapsInk) c.text(50, 6+i, fmt.Sprintf("%d:1", row.Odds), crapsInk) if cfg.PaiGow.Envy { c.center(envyLeft, envyRight, 6+i, fmt.Sprintf("%d", row.Envy), crapsInk) } } c.center(1, 75, 18, "Highest prize only. Winning Fortune wagers also return their stake.", crapsMuted) note = "Envy disabled: quads improved from 25:1 to 30:1." if cfg.PaiGow.Envy { note = "Envy pays when other players hit quads or better." } c.center(1, 75, 20, note, crapsInk) c.center(1, 75, 21, "High must outrank low. Copies lose.", crapsMuted) joker := "Joker: ace or completes straight/flush." if cfg.PaiGow.A2345High() { joker = "Joker: ace or completes straight/flush. A2345 is second-highest straight." } c.center(1, 75, 22, joker, crapsMuted) return c.render(mode, unicode) } // paiGowNumberLabels centers the card's original position number under it. func paiGowNumberLabels(nums []int, mode string) []string { labels := make([]string, len(nums)) for i, n := range nums { labels[i] = color.Render(mode, color.ColorSpec{Fg: 15, Bold: true}, fmt.Sprintf("%d", n)) } return labels } // paiGowCardBlock renders one labeled run of cards: an optional centered // header over the card art and, when nums is set, position numbers below. func paiGowCardBlock(header string, cards []casino.CardView, nums []int, mode string, unicode bool, size int, style string, store *cardart.Store) []string { if len(cards) == 0 { return nil } var labels []string if len(nums) > 0 { labels = paiGowNumberLabels(nums, mode) } lines := renderCardRow(cards, labels, 0, mode, unicode, size, style, store) if header == "" { return lines } width := 0 for _, line := range lines { width = max(width, color.VisibleLen(line)) } return append([]string{rouletteCenter(header, width)}, lines...) } // paiGowJoinBlocks sets two card blocks side by side with a gutter between // them, so the high hand and the low hand read as separate groups. func paiGowJoinBlocks(left, right []string, gutter string) []string { height := max(len(left), len(right)) leftWidth := 0 for _, line := range left { leftWidth = max(leftWidth, color.VisibleLen(line)) } lines := make([]string, 0, height) for i := 0; i < height; i++ { l, r := "", "" if i < len(left) { l = left[i] } if i < len(right) { r = right[i] } lines = append(lines, l+strings.Repeat(" ", leftWidth-color.VisibleLen(l))+gutter+r) } return lines } // paiGowStrength paints a hand's name yellow when it won its comparison and // white when it did not. func paiGowStrength(mode, strength string, winning bool) string { spec := color.ColorSpec{Fg: 15, Bold: true} if winning { spec = color.ColorSpec{Fg: pokerYellow, Bold: true} } return color.Render(mode, spec, strength) } // paiGowArranged renders the preview: the five-card high hand, a gap, then the // two-card low hand, each with its original position numbers underneath. func paiGowArranged(s *casino.PaiGowSnapshot, mode string, unicode bool, size int, style string, store *cardart.Store) []string { high := paiGowCardBlock(color.Render(mode, paiGowPush, "HIGH"), s.PreviewHigh, s.PreviewHighNums, mode, unicode, size, style, store) low := paiGowCardBlock(color.Render(mode, paiGowPush, "LOW"), s.PreviewLow, s.PreviewLowNums, mode, unicode, size, style, store) lines := []string{"Your arranged hand — high first, then low. Type set to lock it, or choose again:"} return append(lines, paiGowJoinBlocks(high, low, " ")...) } // paiGowOutcome renders the comparison narrative and settlement summary. func paiGowOutcome(sd *casino.PaiGowShowdown, mode string) []string { var lines []string if len(sd.Hands) >= 4 { switch { case sd.Hands[0].Winning && sd.Hands[2].Winning: lines = append(lines, "You take both hands.") case !sd.Hands[0].Winning && !sd.Hands[2].Winning: lines = append(lines, "The dealer takes both hands.") default: high, low := "You take the high hand", "you take the low hand" if !sd.Hands[0].Winning { high = "The dealer takes the high hand" } if !sd.Hands[2].Winning { low = "the dealer takes the low hand" } lines = append(lines, high+"; "+low+".") } } spec, label := paiGowLoss, "DEALER WINS THE ROUND" switch { case strings.HasPrefix(sd.Result, "win"): spec, label = paiGowWin, "YOU WIN THE ROUND" case strings.HasPrefix(sd.Result, "push"): spec, label = paiGowPush, "PUSH — YOUR WAGER IS RETURNED" if detail := strings.Trim(strings.TrimPrefix(sd.Result, "push"), " ()"); detail != "" { label = "PUSH (" + strings.ToUpper(detail) + ") — YOUR WAGER IS RETURNED" } } lines = append(lines, color.Render(mode, spec, label)) if sd.Fortune != "" { if sd.FortuneWin { lines = append(lines, "Fortune: "+color.Render(mode, paiGowWin, strings.ToUpper(sd.Fortune)+" — WIN")) } else { lines = append(lines, "Fortune: no win.") } } if sd.PushAce != "" { if sd.PushAceWin { lines = append(lines, "Push Ace High: "+color.Render(mode, paiGowWin, sd.PushAce+" — WIN")) } else { lines = append(lines, "Push Ace High: "+sd.PushAce+".") } } if sd.ShowEnvy && sd.Envy > 0 { lines = append(lines, "Envy: "+color.Render(mode, paiGowWin, fmt.Sprintf("+%d credits", sd.Envy))) } switch { case sd.Pending: lines = append(lines, color.Render(mode, paiGowLoss, "PAYMENT PENDING — type settle to retry the same result.")) case sd.Paid: net := sd.Payout - sd.Stake netSpec, netText := paiGowPush, "Net 0 credits." if net > 0 { netSpec, netText = paiGowWin, fmt.Sprintf("Net +%d credits.", net) } else if net < 0 { netSpec, netText = paiGowLoss, fmt.Sprintf("Net %d credits.", net) } lines = append(lines, color.Render(mode, netSpec, netText)) } return lines } func paiGowShowdown(s *casino.PaiGowSnapshot, mode string, unicode bool, size int, style string, store *cardart.Store) []string { sd := s.Showdown var lines []string for i, hand := range sd.Hands { header := hand.Label if hand.Strength != "" { header += " (" + paiGowStrength(mode, hand.Strength, hand.HasWin && hand.Winning) + ")" } lines = append(lines, paiGowCardBlock(header, hand.Cards, nil, mode, unicode, size, style, store)...) if i == 1 { lines = append(lines, "") } } lines = append(lines, "") return append(lines, paiGowOutcome(sd, mode)...) } func renderPaiGow(s *casino.PaiGowSnapshot, mode string, unicode bool, size int, style string, store *cardart.Store) string { size = cardRenderSize(unicode, size) var lines []string if s.ShowFelt { lines = append(lines, renderPaiGowFelt(s.Config, mode, unicode)) } switch { case s.Showdown != nil: lines = append(lines, paiGowShowdown(s, mode, unicode, size, style, store)...) case s.Preview: lines = append(lines, paiGowArranged(s, mode, unicode, size, style, store)...) case len(s.Cards) > 0: lines = append(lines, "Your seven cards (positions 1-7):") nums := make([]int, len(s.Cards)) for i := range nums { nums[i] = i + 1 } lines = append(lines, paiGowCardBlock("", s.Cards, nums, mode, unicode, size, style, store)...) } // Face Up reveals the dealer before the player's decision; the showdown // replaces this block once it exists. if s.Showdown == nil && len(s.DealerHigh) > 0 { lines = append(lines, "") lines = append(lines, paiGowCardBlock("Dealer high ("+s.DealerHighName+")", s.DealerHigh, nil, mode, unicode, size, style, store)...) lines = append(lines, paiGowCardBlock("Dealer low ("+s.DealerLowName+")", s.DealerLow, nil, mode, unicode, size, style, store)...) } if len(s.Tiebreaker) > 0 { lines = append(lines, "Tiebreaker — player / dealer:") lines = append(lines, paiGowCardBlock("", s.Tiebreaker, nil, mode, unicode, size, style, store)...) } if s.Showdown == nil && s.Result != "" { lines = append(lines, s.Result) } return strings.Join(lines, "\n") }