package casino import ( "fmt" "sort" "strings" ) // crapsReturn is shared by wallet settlement and next-roll previews. 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") && profit > 0 { base := payout - b.stake.Total if b.kind == "buy" { base = b.stake.Total } // The house rounds its 5% vig down to whole credits; the bet units // above keep the common stakes exact so this rarely bites. payout -= base / 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 } func crapsNet(b *crapsBet, profit, den, oddsProfit int, refundOdds bool) int { return crapsReturn(b, profit, den, oddsProfit, refundOdds) - b.stake.Total - b.odds.Total } func crapsRefundOdds(b *crapsBet, point int) bool { return point == 0 && b.kind == "come" } type CrapsRollView struct { Total int Rolls string Net int } func (t *crapsTable) rollPairs() [][2]int { var pairs [][2]int for a := 1; a <= 6; a++ { if t.Config.Craps.variant() == "die_rich" { pairs = append(pairs, [2]int{a, 0}) continue } for b := a; b <= 6; b++ { pairs = append(pairs, [2]int{a, b}) } } return pairs } func (t *crapsTable) projectionFor(p *crapsPlayer) []CrapsRollView { var rows []CrapsRollView for _, pair := range t.rollPairs() { a, b := pair[0], pair[1] net := 0 for _, bet := range p.bets { done, x, y := t.outcomeFor(bet, a, b) if done { net += crapsNet(bet, x, y, x, crapsRefundOdds(bet, t.point)) } } label := fmt.Sprintf("%d+%d", a, b) if b == 0 { label = fmt.Sprint(a) } rows = append(rows, CrapsRollView{a + b, label, net}) } sort.SliceStable(rows, func(i, j int) bool { return rows[i].Total < rows[j].Total }) 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 } // rollTable previews the next roll as one compact line of every total 2-12 // (1-6 on the single-die table): wins are bold green, losses bold red, and // totals no open wager settles are dim. Hardway and hop bets are pair // specific, so the best pair outcome stands in for that total. The line wraps // on token boundaries with a hanging indent so the continuation lines line up // under the first total. func (t *crapsTable) rollTable(p *crapsPlayer) string { if len(p.bets) == 0 { return "" } best := map[int]int{} found := map[int]bool{} for _, row := range t.projectionFor(p) { if !found[row.Total] || row.Net > best[row.Total] { best[row.Total] = row.Net found[row.Total] = true } } lo, hi := 2, 12 if t.Config.Craps.variant() == "die_rich" { lo, hi = 1, 6 } type outcomeToken struct { tagged string width int } tokens := make([]outcomeToken, 0, hi-lo+1) for total := lo; total <= hi; total++ { token := fmt.Sprintf("%d(-)", total) tag := "dim" switch { case found[total] && best[total] > 0: token, tag = fmt.Sprintf("%d(+%d)", total, best[total]), "casino_result_win" case found[total] && best[total] < 0: token, tag = fmt.Sprintf("%d(%d)", total, best[total]), "casino_result_loss" } tokens = append(tokens, outcomeToken{fmt.Sprintf("{%s}%s{/}", tag, token), len(token)}) } const ( head = "OUTCOMES:" width = 80 ) indent := strings.Repeat(" ", len(head)+1) var sb strings.Builder sb.WriteString("{casino_emphasis}" + head + "{/}") lineLen := len(head) for i, tk := range tokens { // The first token always follows the heading; later tokens wrap whole, // never splitting a total from its payout. if i > 0 && lineLen+1+tk.width > width { sb.WriteString("\n" + indent) lineLen = len(indent) sb.WriteString(tk.tagged) lineLen += tk.width continue } sb.WriteByte(' ') sb.WriteString(tk.tagged) lineLen += 1 + tk.width } return sb.String() }