package game import ( "fmt" "math" "strings" "unicode/utf8" "thehouseoficarus/internal/casino" "thehouseoficarus/internal/net" "thehouseoficarus/internal/ui" ) // Only carried, positive-quantity items count. The most precise instrument wins. func (g *Game) casinoEdgePrecision(sess *net.Session) (int, bool) { if sess == nil || sess.Player == nil || g.ItemStore == nil { return 0, false } precision, found := 0, false for _, slot := range sess.Player.Inventory { if slot == nil || slot.Quantity <= 0 { continue } def, err := g.ItemStore.Load(slot.ItemID) if err != nil || def.HouseEdgePrecision == nil { continue } p := *def.HouseEdgePrecision if p != 0 && p != 2 { continue } precision = max(precision, p) found = true } return precision, found } // Convert the unrounded loss, not the displayed/rounded house edge. func casinoRTP(e casino.HouseEdge, precision int) string { if len(e.Parts) != 1 { return "—" } value := math.Round((1-e.Parts[0].Loss)*100*math.Pow10(precision)) / math.Pow10(precision) if value == 0 { value = 0 } // no negative zero return fmt.Sprintf("%.*f%%", precision, value) } type casinoListRow struct { bet, odds, win string edge casino.HouseEdge } // Only remove redundant commission prose. Exceptions such as Banker 6 paying // half or three-card 7 pushing are rules, not redundant explanations. func conciseCasinoOdds(odds string) string { if at := strings.Index(odds, " ("); at >= 0 && strings.HasSuffix(odds, "% commission)") { return odds[:at] } return odds } func (g *Game) casinoListRows(sess *net.Session, t *casino.Table, wagers []casino.WagerRow) []casinoListRow { _, enabled := g.casinoEdgePrecision(sess) var rows []casinoListRow for _, wager := range wagers { row := casinoListRow{bet: wager.Bet, odds: conciseCasinoOdds(wager.Payout), win: wager.Win} if enabled { row.edge = t.HouseEdge(sess.Player.Name, wager.Bet) } if len(row.edge.Parts) <= 1 { rows = append(rows, row) continue } // Different target returns need distinct rows, not a list of unlabeled RTPs. for _, part := range row.edge.Parts { entry := row if strings.Contains(wager.Bet, "") { entry.bet = strings.ReplaceAll(wager.Bet, "", part.Label) } else { entry.win = part.Label + "; " + entry.win } entry.edge = casino.HouseEdge{Parts: []casino.EdgePart{part}} rows = append(rows, entry) } } return rows } // Explicit list-only presentation. Wrap plain cells before applying color and // handing them to the existing ANSI-aware table renderer; never truncate rules. // The table stands alone: list names no heading and no balance above it, and // the first column header carries the bet syntax instead of a hint line below. func (g *Game) showCasinoListTable(sess *net.Session, betHeader string, rows []casinoListRow) { precision, hasItem := g.casinoEdgePrecision(sess) // The item adds a column only when at least one listed wager has a real // analysis; decision actions such as hit/stand have none. rtp := false if hasItem { for _, r := range rows { if len(r.edge.Parts) > 0 { rtp = true break } } } headers := []string{betHeader, "Odds", "Win / action"} categories := []string{"command", "casino_payout", ""} if rtp { headers = append(headers, "RTP") categories = append(categories, "casino_emphasis") } cells := make([][]string, 0, len(rows)) widths := make([]int, len(headers)) for i, h := range headers { widths[i] = utf8.RuneCountInString(h) } for _, r := range rows { odds := r.odds if odds == "" { odds = "—" } row := []string{r.bet, odds, r.win} if rtp { row = append(row, casinoRTP(r.edge, precision)) } for i, c := range row { widths[i] = max(widths[i], utf8.RuneCountInString(c)) } cells = append(cells, row) } width, unicode := 80, true if p := sess.Player; p != nil { unicode = p.OptionBool("unicode") if wrap := p.OptionInt("wrap_width"); wrap > 0 { width = min(width, wrap) } } budget := max(len(headers), width-3*len(headers)-1) for { sum, widest := 0, 0 for i, w := range widths { sum += w if w > widths[widest] { widest = i } } if sum <= budget || widths[widest] <= 1 { break } widths[widest]-- } table := ui.Table{} // Headers wrap too, so narrow terminals never lose a column label. all := append([][]string{headers}, cells...) for rowIndex, row := range all { wrapped := make([][]string, len(row)) height := 1 for i, c := range row { wrapped[i] = casinoWrapWords(c, widths[i]) height = max(height, len(wrapped[i])) } for line := 0; line < height; line++ { physical := make([]string, len(row)) for i, parts := range wrapped { if line >= len(parts) { continue } category := categories[i] if rowIndex == 0 { category = "casino_action" } physical[i] = parts[line] if category != "" { physical[i] = g.colorize(sess, category, physical[i]) } } if rowIndex == 0 && line == 0 { table.Columns = physical } else { table.Rows = append(table.Rows, physical) } } } out := table.Render(unicode, width) sess.WriteLine(strings.Join(out, "\n")) }