diff options
| author | historia <[not public]> | 2026-09-30 05:33:45 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-30 05:33:45 -0400 |
| commit | 0fcb03633e3008169173d76c8408f5410592b3eb (patch) | |
| tree | 814797fb33a129b1c49f8ae309c6647027f5e528 /internal/casino/craps_projection.go | |
| parent | 735428f0667f67c12fdf3caca872d7dc65e2ce26 (diff) | |
| download | thehouseoficarus-0fcb03633e3008169173d76c8408f5410592b3eb.tar.gz | |
feat: bank craps added to casino
Diffstat (limited to 'internal/casino/craps_projection.go')
| -rw-r--r-- | internal/casino/craps_projection.go | 225 |
1 files changed, 225 insertions, 0 deletions
diff --git a/internal/casino/craps_projection.go b/internal/casino/craps_projection.go new file mode 100644 index 0000000..5be2f28 --- /dev/null +++ b/internal/casino/craps_projection.go @@ -0,0 +1,225 @@ +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() +} |
