aboutsummaryrefslogtreecommitdiff
path: root/internal/game/render_casino_settlement.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game/render_casino_settlement.go')
-rw-r--r--internal/game/render_casino_settlement.go185
1 files changed, 185 insertions, 0 deletions
diff --git a/internal/game/render_casino_settlement.go b/internal/game/render_casino_settlement.go
new file mode 100644
index 0000000..bfe101c
--- /dev/null
+++ b/internal/game/render_casino_settlement.go
@@ -0,0 +1,185 @@
+package game
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+
+ "thehouseoficarus/internal/casino"
+ "thehouseoficarus/internal/color"
+)
+
+// renderCasinoSettlement is shared by every game. Paint is supplied by the
+// recipient's palette; widths are measured before adding ANSI sequences.
+// The receipt fits the MUD's minimum 80-column wrap width. Long labels wrap
+// rather than losing the hand number or wager identity through truncation.
+func renderCasinoSettlement(lines []casino.SettlementLine, unicode bool, paint func(string, string) string) string {
+ if len(lines) == 0 {
+ return ""
+ }
+ dash := "—"
+ h, v, tl, tr, bl, br, tj, bj, lj, rj, cross := "─", "│", "╭", "╮", "╰", "╯", "┬", "┴", "├", "┤", "┼"
+ if !unicode {
+ dash = "-"
+ h, v, tl, tr, bl, br, tj, bj, lj, rj, cross = "-", "|", "+", "+", "+", "+", "+", "+", "+", "+", "+"
+ }
+ type row struct {
+ cells [5]string
+ resultTag, netTag string
+ }
+ rows := make([]row, 0, len(lines))
+ widths := [5]int{5, 6, 4, 6, 3}
+ var staked, returned, active int64
+ settled := 0
+ netTag := func(net int64) string {
+ if net > 0 {
+ return "casino_result_win"
+ }
+ if net < 0 {
+ return "casino_result_loss"
+ }
+ return "casino_result_push"
+ }
+ for _, line := range lines {
+ label, odds := line.Label, line.Odds
+ if label == "" {
+ label = "Wager"
+ }
+ if odds == "" {
+ odds = "Varies"
+ }
+ if !unicode {
+ label = strings.NewReplacer("·", "-", "—", "-").Replace(label)
+ odds = strings.ReplaceAll(odds, "—", "-")
+ }
+ net := int64(line.Returned) - int64(line.Stake)
+ result, rt := "LOSS", "casino_result_loss"
+ switch {
+ case strings.HasPrefix(line.Result, "win"), line.Result == "blackjack":
+ result, rt = "WIN", "casino_result_win"
+ case strings.HasPrefix(line.Result, "push"):
+ result, rt = "PUSH", "casino_result_push"
+ case line.Result == "insurance":
+ rt = netTag(net)
+ if net > 0 {
+ result = "WIN"
+ } else if net == 0 {
+ result = "PUSH"
+ }
+ }
+ nt, amount := netTag(net), fmt.Sprintf("%+d", net)
+ if net == 0 {
+ amount = "0"
+ }
+ if line.Unresolved {
+ result, rt, nt, amount = "UNRESOLVED", "warning", "warning", dash
+ active += int64(line.Stake)
+ } else {
+ staked += int64(line.Stake)
+ returned += int64(line.Returned)
+ settled++
+ }
+ r := row{[5]string{label, strconv.Itoa(line.Stake), odds, result, amount}, rt, nt}
+ rows = append(rows, r)
+ for i, s := range r.cells {
+ widths[i] = max(widths[i], color.VisibleLen(s))
+ }
+ }
+ widths[0] = min(widths[0], 24)
+ widths[2] = min(widths[2], 10)
+ // Borders and cell padding occupy sixteen columns.
+ widths[0] = min(widths[0], 79-16-widths[1]-widths[2]-widths[3]-widths[4])
+ widths[0] = max(widths[0], 5)
+ inner := 4
+ for _, w := range widths {
+ inner += w + 2
+ }
+ // A readable footer without forcing tiny one-wager receipts into a wide box.
+ inner = max(inner, 50)
+ sum := 4
+ for _, w := range widths {
+ sum += w + 2
+ }
+ widths[0] += inner - sum
+ border := func(left, join, right string) string {
+ parts := make([]string, 5)
+ for i, w := range widths {
+ parts[i] = strings.Repeat(h, w+2)
+ }
+ return paint("dim", left+strings.Join(parts, join)+right)
+ }
+ pad := func(s string, w int, right bool) string {
+ spaces := strings.Repeat(" ", max(0, w-color.VisibleLen(s)))
+ if right {
+ return spaces + s
+ }
+ return s + spaces
+ }
+ formatRow := func(cells [5]string, rt, nt string) string {
+ parts := make([]string, 5)
+ for i, s := range cells {
+ tag := "casino_action"
+ if i == 3 {
+ tag = rt
+ }
+ if i == 4 {
+ tag = nt
+ }
+ parts[i] = " " + paint(tag, pad(s, widths[i], i == 1 || i == 2 || i == 4)) + " "
+ }
+ return paint("dim", v) + strings.Join(parts, paint("dim", v)) + paint("dim", v)
+ }
+ out := []string{border(tl, tj, tr), formatRow([5]string{"Wager", "Staked", "Odds", "Result", "Net"}, "casino_action", "casino_action"), border(lj, cross, rj)}
+ for _, r := range rows {
+ // Wrapping uses runes, matching the existing MUD visible-width contract.
+ remaining := r.cells
+ for {
+ var cells [5]string
+ more := false
+ for i, s := range remaining {
+ runes := []rune(strings.Join(strings.Fields(s), " "))
+ n := min(len(runes), widths[i])
+ cells[i] = string(runes[:n])
+ remaining[i] = string(runes[n:])
+ more = more || len(runes) > n
+ }
+ out = append(out, formatRow(cells, r.resultTag, r.netTag))
+ if !more {
+ break
+ }
+ }
+ }
+ out = append(out, border(lj, bj, rj))
+ footer := func(text, tag string) {
+ out = append(out, paint("dim", v)+" "+paint(tag, pad(text, inner-2, false))+" "+paint("dim", v))
+ }
+ summary := fmt.Sprintf("Staked: %d Returned: %d", staked, returned)
+ if active > 0 {
+ summary = fmt.Sprintf("Settled: %d Returned: %d Active: %d", staked, returned, active)
+ }
+ // Large multi-hand totals can exceed one footer line; never wrap the frame.
+ for color.VisibleLen(summary) > inner-2 {
+ cut := strings.LastIndex(summary[:inner-2], " ")
+ if cut < 0 {
+ cut = inner - 2
+ }
+ footer(summary[:cut], "casino_action")
+ summary = strings.TrimSpace(summary[cut:])
+ }
+ footer(summary, "casino_action")
+ net := returned - staked
+ title, amount := "NET EVEN", "0 credits"
+ if net > 0 {
+ title, amount = "NET PROFIT", fmt.Sprintf("+%d credits", net)
+ }
+ if net < 0 {
+ title, amount = "NET LOSS", fmt.Sprintf("%d credits", net)
+ }
+ tag := netTag(net)
+ if settled == 0 {
+ title, amount, tag = "NET PENDING", dash, "warning"
+ }
+ footer(title+strings.Repeat(" ", max(1, inner-2-len(title)-color.VisibleLen(amount)))+amount, tag)
+ out = append(out, paint("dim", bl+strings.Repeat(h, inner)+br))
+ return strings.Join(out, "\n")
+}