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) // Grow the label column to reach the 50-wide minimum box (a readable footer // without forcing tiny one-wager receipts wide). sum := 4 for _, w := range widths { sum += w + 2 } widths[0] += max(0, 50-sum) inner := max(50, 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 } } } if settled == 0 { // Nothing settled yet: the whole totals footer is noise, so end the // receipt at the wager rows with the flat bottom border. out = append(out, paint("dim", bl+strings.Repeat(h, inner)+br)) return strings.Join(out, "\n") } 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) 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") }