aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/craps_projection.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/casino/craps_projection.go')
-rw-r--r--internal/casino/craps_projection.go225
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()
+}