package casino import ( "fmt" "sort" "strings" ) // crapsReturn is the pure payout math shared by settlement and by the // player-facing "what could win on the next roll" projection, so the two can // never disagree. It mirrors the money path of crapsTable.pay exactly. func crapsReturn(b *crapsBet, profit, den, oddsProfit int, refundOdds bool) int { payout := 0 if profit >= 0 { payout = b.stake.Total + b.stake.Total/den*profit if b.kind == "buy" || b.kind == "lay" { base := payout - b.stake.Total if b.kind == "buy" { base = b.stake.Total } // Removals (zero profit) return the full stake without commission. if profit > 0 { payout -= (base + 19) / 20 } } } if b.odds.Total > 0 { if refundOdds { payout += b.odds.Total } else if oddsProfit >= 0 { x, y := crapsTrueOdds(b.number) if strings.HasPrefix(b.kind, "dont") { x, y = y, x } payout += b.odds.Total + b.odds.Total/y*x } } return payout } // crapsNet returns settlement profit: returns (stake included) less commit. func crapsNet(b *crapsBet, profit, den, oddsProfit int, refundOdds bool) int { return crapsReturn(b, profit, den, oddsProfit, refundOdds) - (b.stake.Total + b.odds.Total) } // crapsRefundOdds mirrors come bets' come-out refundOdds policy. func crapsRefundOdds(b *crapsBet, point int) bool { return point == 0 && b.kind == "come" } // crapsRollView is one possible dice pair: its total, the pair shown, and the // player's combined settlement profit if it rolled next. type CrapsRollView struct { Total int Rolls string Net int } // projectionFor evaluates one player's live bets as if the listed pair rolled // with the current table point, via the same rules engine used for settlement. func (t *crapsTable) projectionFor(p *crapsPlayer) []CrapsRollView { var rows []CrapsRollView for total := 2; total <= 12; total++ { for a := 1; a <= 6; a++ { b := total - a if b < 1 || b > 6 || a > b { continue } net := 0 for _, bet := range p.bets { done, x, y := crapsOutcome(bet.kind, bet.number, a, b, t.point, t.Config.Craps.Field12) if !done { // An unresolved contract keeps its stake; only bonuses // can complete within a single non-settling roll. if bet.kind == "fire" { x = crapsFireProfit(len(t.made)) } else if bet.kind == "small" || bet.kind == "tall" || bet.kind == "all" { lo, hi := 2, 12 if bet.kind == "small" { hi = 6 } if bet.kind == "tall" { lo = 8 } if crapsBonusComplete(lo, hi, t.seen, a+b) { x = 34 if bet.kind == "all" { x = 175 } } else { continue } } else { continue } } net += crapsNet(bet, x, y, x, crapsRefundOdds(bet, t.point)) } rows = append(rows, CrapsRollView{total, fmt.Sprintf("%d+%d", a, b), net}) } } return rows } func crapsFireProfit(made int) int { switch made { case 4: return 24 case 5: return 249 case 6: return 999 } return 0 } func crapsBonusComplete(lo, hi int, seen map[int]bool, next int) bool { for n := lo; n <= hi; n++ { if n != 7 && !seen[n] && n != next { return false } } return true } // crapsRollLabel names the roll result a bet cares about: hardways and hop // bets distinguish their exact pair ("4+4"); every other bet reads by total. func crapsRollLabel(bet *crapsBet, a, b int) string { s := a + b switch bet.kind { case "hard": if a == b { return fmt.Sprintf("%d+%d", a, b) } if s == 7 { return "7" } return fmt.Sprintf("easy %d", s) case "hop": if bet.number == a*10+b || bet.number == b*10+a { return fmt.Sprintf("%d+%d", a, b) } return "any other" } return fmt.Sprint(s) } // rollTable lists each roll result and its combined payout across the // player's bets, one row per roll. Dice-distinguishing bets (hardways, hop) // name their pair; everything else reads by total. Pairs that share a // label and outcome collapse; across bets, same-roll payouts add. Tags // stay single-level: the tag expander does not nest. func (t *crapsTable) rollTable(p *crapsPlayer) string { agg := map[string]int{} for _, bet := range p.bets { perLabel := map[string]int{} for a := 1; a <= 6; a++ { for b := a; b <= 6; b++ { done, x, y := crapsOutcome(bet.kind, bet.number, a, b, t.point, t.Config.Craps.Field12) if !done { // Unresolved contracts stay put; only bonuses can // complete within a single non-settling roll. switch bet.kind { case "fire": x = crapsFireProfit(len(t.made)) case "small", "tall", "all": lo, hi := 2, 12 if bet.kind == "small" { hi = 6 } if bet.kind == "tall" { lo = 8 } if !crapsBonusComplete(lo, hi, t.seen, a+b) { continue } x = 34 if bet.kind == "all" { x = 175 } default: continue } } net := crapsNet(bet, x, y, x, crapsRefundOdds(bet, t.point)) if net != 0 { perLabel[crapsRollLabel(bet, a, b)] = net } } } for label, net := range perLabel { agg[label] += net } } if len(agg) == 0 { if len(p.bets) == 0 { return "No open wagers." } return "{casino_action}Your place, buy and hardway bets are OFF on the come-out — nothing pays until a point is set.{/}" } labels := []string{} for label := range agg { labels = append(labels, label) } sort.Slice(labels, func(i, j int) bool { ni, nj := agg[labels[i]], agg[labels[j]] if (ni > 0) != (nj > 0) { return ni > 0 } if ni != nj { return ni > nj } return labels[i] < labels[j] }) var sb strings.Builder sb.WriteString("{casino_emphasis}ROLL RESULTS ON YOUR BETS{/}\n") for _, label := range labels { net := agg[label] tag, amount := "casino_result_loss", fmt.Sprint(net) if net > 0 { tag, amount = "casino_result_win", "+"+amount } fmt.Fprintf(&sb, " {casino_payout}%s{/}%s {%s}%s{/}\n", label, strings.Repeat(" ", max(1, 10-len(label))), tag, amount) } sb.WriteString("{casino_action}Hardways and hops name their pair; totals cover both orders.{/}") return sb.String() }