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 bet, ok := expandBetNumber(wager.Bet, part.Label); ok { entry.bet = bet } else if part.Label != "" { // The label refines the shared win text (hop's doubles), so append // it: the base phrase stays "Next roll is a/b (doubles)". entry.win = entry.win + " " + part.Label } entry.edge = casino.HouseEdge{Parts: []casino.EdgePart{part}} rows = append(rows, entry) } } return rows } // expandBetNumber replaces the standalone number variable N in a grouped // wager row ("buy N", "odds dont N") with one concrete number. It reports // false when the row has no N token, leaving the label to refine the win text. func expandBetNumber(bet, number string) (string, bool) { fields := strings.Fields(bet) found := false for i, f := range fields { if f == "N" { fields[i] = number found = true } } if !found { return bet, false } return strings.Join(fields, " "), true } // 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] switch category { case "command": physical[i] = g.colorizeCommand(sess, physical[i]) case "": default: 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")) }