aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/settlement.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-10-01 22:47:41 -0400
committerhistoria <[not public]>2026-10-01 22:47:41 -0400
commit8eea0e4e06a4aaa6a18edabd9897d3bea1ea2ca0 (patch)
tree63698c0618f382a553d4558a33e3d2087039c378 /internal/casino/settlement.go
parent6d9908b29c8187196d23b126b1165c7e0bd0fc76 (diff)
downloadthehouseoficarus-8eea0e4e06a4aaa6a18edabd9897d3bea1ea2ca0.tar.gz
feat: consistent wager outcome table across casino games
Diffstat (limited to 'internal/casino/settlement.go')
-rw-r--r--internal/casino/settlement.go153
1 files changed, 153 insertions, 0 deletions
diff --git a/internal/casino/settlement.go b/internal/casino/settlement.go
new file mode 100644
index 0000000..46d3723
--- /dev/null
+++ b/internal/casino/settlement.go
@@ -0,0 +1,153 @@
+package casino
+
+import (
+ "fmt"
+ "strings"
+)
+
+// Attach receipts to the recipient's final board/menu where possible, so the
+// renderer can place them between the reveal and the next-action prompt.
+// Departing recipients still receive their standalone private receipt.
+func withSettlementReceipts(events, receipts []Event) []Event {
+ for _, receipt := range receipts {
+ attached := false
+ for i := len(events) - 1; i >= 0; i-- {
+ if events[i].PrivateTo == receipt.PrivateTo && events[i].Snapshot != nil && !events[i].Public {
+ events[i].Settlement = receipt.Settlement
+ events[i].SettlementNote = receipt.SettlementNote
+ attached = true
+ break
+ }
+ }
+ if !attached {
+ events = append(events, receipt)
+ }
+ }
+ return events
+}
+
+// profitOdds reduces an exact profit fraction without rounding currency.
+func profitOdds(num, den int) string {
+ if den <= 0 {
+ return "Varies"
+ }
+ a, b := num, den
+ if a < 0 {
+ a = -a
+ }
+ for b != 0 {
+ a, b = b, a%b
+ }
+ if a == 0 {
+ return "0:1"
+ }
+ return fmt.Sprintf("%d:%d", num/a, den/a)
+}
+
+func returnResult(stake, returned int) string {
+ switch {
+ case returned > stake:
+ return "win"
+ case returned < stake:
+ return "loss"
+ default:
+ return "push"
+ }
+}
+
+func rouletteSettlementOdds(w rouletteWager, pocket string) string {
+ if s, ok := w.covers(pocket); ok {
+ return profitOdds(s.num-s.den, s.den)
+ }
+ odds := ""
+ for _, s := range w.shares {
+ next := profitOdds(s.num-s.den, s.den)
+ if odds != "" && odds != next {
+ return "Varies"
+ }
+ odds = next
+ }
+ if odds == "" {
+ return "Varies"
+ }
+ return odds
+}
+
+func baccaratSettlementOdds(spot string, player, banker []card, rules BaccaratConfig) string {
+ rules = rules.Normalize()
+ switch spot {
+ case baccaratSpotPlayer:
+ return "1:1"
+ case baccaratSpotBanker:
+ if rules.Variant == BaccaratSuper6 && baccaratTotal(banker) > baccaratTotal(player) && baccaratTotal(banker) == 6 {
+ return "1:2"
+ }
+ return payoutRatio(rules.BankerPayout)
+ case baccaratSpotTie:
+ return payoutRatio(rules.TiePayout)
+ case "player_pair", "banker_pair":
+ return "11:1"
+ case "dragon_7":
+ return "40:1"
+ case "panda_8":
+ return "25:1"
+ case "super_6":
+ return "12:1"
+ default:
+ return "Varies"
+ }
+}
+
+// For losing or active bets, inspect legal winning outcomes. Variable games
+// (field, horn and variant pass lines) must not advertise one fixed rate.
+func (t *crapsTable) settlementOdds(b *crapsBet) string {
+ odds := ""
+ for _, pair := range t.rollPairs() {
+ done, num, den := t.outcomeFor(b, pair[0], pair[1])
+ if !done || num <= 0 {
+ continue
+ }
+ next := profitOdds(num, den)
+ if odds != "" && odds != next {
+ return "Varies"
+ }
+ odds = next
+ }
+ if odds == "" {
+ odds = "Varies"
+ }
+ if b.kind == "buy" || b.kind == "lay" {
+ odds += " fee"
+ }
+ return odds
+}
+
+// Keep the base bet and its free-odds wager separate, including come-out
+// refunds where the base loses but the odds push.
+func (t *crapsTable) crapsSettlementLines(b *crapsBet, num, den int, refundOdds, active bool) []SettlementLine {
+ base := *b
+ base.odds = Wager{}
+ returned := crapsReturn(&base, num, den, num, false)
+ odds := t.settlementOdds(b)
+ if num > 0 && !active {
+ odds = profitOdds(num, den)
+ if b.kind == "buy" || b.kind == "lay" {
+ odds += " fee"
+ }
+ }
+ lines := []SettlementLine{{Label: crapsBetLabel(b), Odds: odds, Stake: b.stake.Total, Returned: returned, Result: returnResult(b.stake.Total, returned), Unresolved: active}}
+ if b.odds.Total > 0 {
+ x, y := crapsTrueOdds(b.number)
+ if strings.HasPrefix(b.kind, "dont") {
+ x, y = y, x
+ }
+ paid := 0
+ if refundOdds {
+ paid = b.odds.Total
+ } else if num >= 0 {
+ paid = b.odds.Total + b.odds.Total/y*x
+ }
+ lines = append(lines, SettlementLine{Label: crapsBetLabel(b) + " odds", Odds: profitOdds(x, y), Stake: b.odds.Total, Returned: paid, Result: returnResult(b.odds.Total, paid), Unresolved: active})
+ }
+ return lines
+}