aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/craps.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-30 16:16:17 -0400
committerhistoria <[not public]>2026-09-30 16:16:17 -0400
commita4c09c2d38d1f3bc1d3ad4bdb0234bdea7c37e73 (patch)
tree00c9567e620f331a59dd44f1f1fa779ecd94a845 /internal/casino/craps.go
parent724079f8de1ae69965c6267dca3d7fc3cc86afa2 (diff)
downloadthehouseoficarus-a4c09c2d38d1f3bc1d3ad4bdb0234bdea7c37e73.tar.gz
feat: additional craps variants added, standardizard casino command hints
Diffstat (limited to 'internal/casino/craps.go')
-rw-r--r--internal/casino/craps.go157
1 files changed, 63 insertions, 94 deletions
diff --git a/internal/casino/craps.go b/internal/casino/craps.go
index 5e5e876..9e48667 100644
--- a/internal/casino/craps.go
+++ b/internal/casino/craps.go
@@ -43,6 +43,7 @@ type CrapsBetView struct {
Amount, Odds int
}
type CrapsSnapshot struct {
+ Variant string
Phase string
Point int
Dice [2]int
@@ -71,7 +72,7 @@ func crapsTarget(b *crapsBet) string {
return fmt.Sprintf("%s %d", b.kind, b.number)
}
func (t *crapsTable) view() *CrapsSnapshot {
- s := &CrapsSnapshot{Phase: string(t.phase), Point: t.point, Dice: t.dice, Queue: append([]string(nil), t.queue...), Field12: t.Config.Craps.Field12}
+ s := &CrapsSnapshot{Variant: t.Config.Craps.variant(), Phase: string(t.phase), Point: t.point, Dice: t.dice, Queue: append([]string(nil), t.queue...), Field12: t.Config.Craps.Field12}
if t.hasLiveBets() && t.multiplayer {
s.ShootIn = crapsSeconds(crapsBetDelay - t.roundElapsed)
s.DealerIn = crapsSeconds(crapsBetDelay + crapsDealerDelay - t.roundElapsed)
@@ -96,7 +97,7 @@ func (t *crapsTable) view() *CrapsSnapshot {
return s
}
func (t *crapsTable) menu(name string) Event {
- e := t.betMenuEvent(name, t.commands())
+ e := t.betMenuEvent(name, t.commands(name))
e.Snapshot = t.view()
return e
}
@@ -159,7 +160,7 @@ func parseCrapsTarget(spot string) (kind string, n int, ok bool) {
return
}
n, _ = strconv.Atoi(f[1])
- ok = crapsNumber(n) && (kind != "hard" || n%2 == 0)
+ ok = n >= 2 && n <= 12 && n != 7 && (kind != "hard" || (crapsNumber(n) && n%2 == 0))
return
case "hop":
if len(f) != 3 {
@@ -167,6 +168,9 @@ func parseCrapsTarget(spot string) (kind string, n int, ok bool) {
}
a, _ := strconv.Atoi(f[1])
b, _ := strconv.Atoi(f[2])
+ if a > b {
+ a, b = b, a
+ }
return kind, a*10 + b, a >= 1 && a <= 6 && b >= 1 && b <= 6
}
return
@@ -191,6 +195,9 @@ func (t *crapsTable) bet(name, spot string, amount int, w Wallet) []Event {
return reject("That potential payout is too large.")
}
if strings.HasPrefix(spot, "odds ") {
+ if !t.Config.Craps.allows("odds") {
+ return reject("This variant has no free odds.")
+ }
target := strings.TrimPrefix(spot, "odds ")
for _, b := range p.bets {
if crapsTarget(b) != target && !(b.kind == target && (target == "pass" || target == "dontpass")) {
@@ -209,7 +216,8 @@ func (t *crapsTable) bet(name, spot string, amount int, w Wallet) []Event {
if b.odds.Total > 0 {
continue
}
- if int64(amount) > int64(b.stake.Total)*int64(limit) || amount%y != 0 {
+ // Compare without multiplying a potentially very large stake by Nx.
+ if (amount-1)/limit >= b.stake.Total || amount%y != 0 {
return reject("Odds already placed, above the free-odds limit, or not in exact payout units.")
}
stake, err := w.Wager(amount)
@@ -219,13 +227,13 @@ func (t *crapsTable) bet(name, spot string, amount int, w Wallet) []Event {
b.odds = stake
p.Wallet = w
p.lastSpot, p.lastBet = spot, amount
- return []Event{t.private(name, fmt.Sprintf("You add {casino_chips}%d{/} free odds on %s.", amount, target)), t.betSummary(p)}
+ return []Event{t.private(name, fmt.Sprintf("You add {casino_chips}%d{/} free odds on %s.", amount, target)), t.betSummary(p), t.reminder(name)}
}
return reject("Place an established line/come bet before taking odds.")
}
kind, n, ok := parseCrapsTarget(spot)
- if !ok {
- return reject("Unknown craps target. Use table rules.")
+ if !ok || !t.Config.Craps.allows(kind) || ((kind == "place" || kind == "buy" || kind == "lay") && !t.Config.Craps.number(n)) {
+ return reject("Unknown or unavailable craps target. Use table rules.")
}
if (kind == "pass" || kind == "dontpass") && t.point != 0 {
return reject("Line bets are accepted only on come-out.")
@@ -244,12 +252,7 @@ func (t *crapsTable) bet(name, spot string, amount int, w Wallet) []Event {
unit := 1
switch kind {
case "place":
- switch n {
- case 4, 10, 5, 9:
- unit = 5
- default:
- unit = 6
- }
+ _, unit = crapsPlaceOdds(n)
case "buy":
_, unit = crapsTrueOdds(n)
case "lay":
@@ -271,35 +274,13 @@ func (t *crapsTable) bet(name, spot string, amount int, w Wallet) []Event {
p.lastSpot, p.lastBet = spot, amount
events := []Event{t.playerAction(name, fmt.Sprintf("%s bets %d on %s.", name, amount, spot), fmt.Sprintf("You bet {casino_chips}%d{/} on %s.", amount, spot))}
events = append(events, t.syncRoundClock()...)
- return append(events, t.betSummary(p))
+ return append(events, t.betSummary(p), t.reminder(name))
}
func (t *crapsTable) pay(p *crapsPlayer, b *crapsBet, profit, den int, oddsProfit int, refundOdds bool) (settlementLine, bool) {
- payout := 0
- if profit >= 0 {
- payout = b.stake.Total + b.stake.Total/den*profit
- if b.kind == "buy" || b.kind == "lay" {
- commissionBase := payout - b.stake.Total
- if b.kind == "buy" {
- commissionBase = b.stake.Total
- }
- // Removals (zero profit) return the full stake without commission.
- if profit > 0 {
- payout -= (commissionBase + 19) / 20
- }
- }
- }
+ payout := crapsReturn(b, profit, den, oddsProfit, refundOdds)
ids := []string{b.stake.ID}
if b.odds.Total > 0 {
ids = append(ids, b.odds.ID)
- 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
- }
}
line := settlementLine{Label: crapsBetLabel(b), Stake: b.stake.Total + b.odds.Total, Returned: payout, Result: "push"}
if payout > line.Stake {
@@ -310,7 +291,17 @@ func (t *crapsTable) pay(p *crapsPlayer, b *crapsBet, profit, den int, oddsProfi
return line, p.Wallet.PayCredits(payout, ids...) != nil
}
func (t *crapsTable) roll() []Event {
- a, b := t.rng.Intn(6)+1, t.rng.Intn(6)+1
+ a, b := 0, 0
+ switch t.Config.Craps.variant() {
+ case "die_rich":
+ a = t.rng.Intn(6) + 1
+ case "diceless":
+ // Each independent pack is replaced and reshuffled for every draw.
+ a = t.rng.Perm(6)[0] + 1
+ b = t.rng.Perm(6)[0] + 1
+ default:
+ a, b = t.rng.Intn(6)+1, t.rng.Intn(6)+1
+ }
return t.resolveRoll(a, b)
}
@@ -319,19 +310,24 @@ func (t *crapsTable) resolveRoll(a, b int) []Event {
s := a + b
oldPoint := t.point
sevenOut := oldPoint != 0 && s == 7
+ pointMade := oldPoint != 0 && oldPoint == s
+ r := t.Config.Craps
+ switch r.variant() {
+ case "simplified", "high_point", "die_rich":
+ done, profit, _ := r.outcome("pass", oldPoint, a, b, oldPoint)
+ sevenOut = done && profit < 0
+ pointMade = done && profit > 0 && oldPoint != 0
+ }
t.dice = [2]int{a, b}
t.rolled = true
- if oldPoint == s {
- t.made[s] = true
- }
- if s != 7 {
- t.seen[s] = true
- }
shooter := "Dealer"
if len(t.queue) > 0 {
shooter = t.queue[0]
}
events := []Event{t.event(fmt.Sprintf("{casino_emphasis}%s shoots{/} {casino_payout}%d{/}", shooter, s), true)}
+ if r.variant() == "diceless" {
+ events[0].Message = fmt.Sprintf("{casino_emphasis}%s draws cards{/} [%d] [%d] = %d", shooter, a, b, s)
+ }
var results []Event
for _, p := range t.players.ordered() {
p.autoElapsed = 0
@@ -339,46 +335,7 @@ func (t *crapsTable) resolveRoll(a, b int) []Event {
failed := false
var live []*crapsBet
for _, bet := range p.bets {
- done, x, y := crapsOutcome(bet.kind, bet.number, a, b, oldPoint, t.Config.Craps.Field12)
- switch bet.kind {
- case "fire":
- done = sevenOut
- y = 1
- x = -1
- if done {
- switch len(t.made) {
- case 4:
- x = 24
- case 5:
- x = 249
- case 6:
- x = 999
- }
- }
- case "small", "tall", "all":
- lo, hi := 2, 12
- if bet.kind == "small" {
- hi = 6
- }
- if bet.kind == "tall" {
- lo = 8
- }
- hit := true
- for n := lo; n <= hi; n++ {
- if n != 7 && !t.seen[n] {
- hit = false
- }
- }
- done = hit || s == 7
- x = -1
- y = 1
- if hit {
- x = 34
- if bet.kind == "all" {
- x = 175
- }
- }
- }
+ done, x, y := t.outcomeFor(bet, a, b)
if done {
refundOdds := oldPoint == 0 && bet.kind == "come"
line, err := t.pay(p, bet, x, y, x, refundOdds)
@@ -389,7 +346,7 @@ func (t *crapsTable) resolveRoll(a, b int) []Event {
}
continue
}
- if bet.number == 0 && (bet.kind == "pass" || bet.kind == "dontpass" || bet.kind == "come" || bet.kind == "dontcome") && crapsNumber(s) {
+ if bet.number == 0 && (bet.kind == "pass" || bet.kind == "dontpass" || bet.kind == "come" || bet.kind == "dontcome") && r.pointNumber(s) {
bet.number = s
}
live = append(live, bet)
@@ -400,15 +357,20 @@ func (t *crapsTable) resolveRoll(a, b int) []Event {
results = append(results, t.settlementEvent(p, lines, failed))
}
}
+ if pointMade {
+ t.made[oldPoint] = true
+ }
if s == 7 {
t.seen = map[int]bool{}
+ } else {
+ t.seen[s] = true
}
- if oldPoint == 0 && crapsNumber(s) {
+ if oldPoint == 0 && r.pointNumber(s) {
t.point = s
events = append(events, t.event(fmt.Sprintf("{casino_emphasis}POINT %d{/} / Puck ON. Make your point!", s), true))
- } else if oldPoint == s || sevenOut {
- if oldPoint == s {
- events = append(events, t.event(fmt.Sprintf("{casino_result_win}POINT %d MADE!{/} / Puck OFF. Same shooter, coming out.", s), true))
+ } else if pointMade || sevenOut {
+ if pointMade {
+ events = append(events, t.event(fmt.Sprintf("{casino_result_win}POINT %d MADE!{/} / Puck OFF. Same shooter, coming out.", oldPoint), true))
}
t.point = 0
}
@@ -418,7 +380,11 @@ func (t *crapsTable) resolveRoll(a, b int) []Event {
if len(t.queue) > 1 {
t.queue = append(t.queue[1:], t.queue[0])
}
- events = append(events, t.event("{casino_warning}SEVEN OUT{/} / Puck OFF. Dice pass to the next shooter.", true))
+ message := "{casino_warning}SEVEN OUT{/} / Puck OFF. Dice pass to the next shooter."
+ if r.variant() == "simplified" || r.variant() == "high_point" || r.variant() == "die_rich" {
+ message = "{casino_warning}SHOOTER OUT{/} / Puck OFF. Next shooter."
+ }
+ events = append(events, t.event(message, true))
}
events = append(events, t.finishCrapsControls()...)
t.phase = phaseWaiting
@@ -483,7 +449,7 @@ func (t *crapsTable) action(name, action string) []Event {
p.autoRoll = true
p.autoElapsed = 0
}
- return []Event{t.private(name, "{casino_emphasis}Autoroll ON{/} — every 3 seconds on your turn, respecting the multiplayer betting window. {command}stop{/} returns to manual play.")}
+ return []Event{t.private(name, "{casino_emphasis}Autoroll ON{/} — every 3 seconds on your turn, respecting the multiplayer betting window. {command}stop{/} returns to manual play."), t.reminder(name)}
}
if action == "roll" {
events := t.syncRoundClock()
@@ -505,6 +471,9 @@ func (t *crapsTable) action(name, action string) []Event {
}
oddsOnly := strings.HasPrefix(target, "odds ")
target = strings.TrimPrefix(target, "odds ")
+ if kind, n, ok := parseCrapsTarget(target); ok && kind == "hop" {
+ target = crapsTarget(&crapsBet{kind: kind, number: n})
+ }
for i, b := range p.bets {
if crapsTarget(b) != target && !(b.kind == target && (target == "pass" || target == "dontpass")) {
continue
@@ -518,9 +487,9 @@ func (t *crapsTable) action(name, action string) []Event {
e.Message = payoutFailedMessage()
}
b.odds = Wager{}
- return []Event{e, t.betSummary(p)}
+ return []Event{e, t.betSummary(p), t.reminder(name)}
}
- if b.kind == "pass" || b.kind == "come" || b.kind == "fire" || b.kind == "small" || b.kind == "tall" || b.kind == "all" {
+ if crapsContract(b.kind) {
return []Event{t.private(name, "That contract bet cannot be removed.")}
}
line, failed := t.pay(p, b, 0, 1, 0, true)
@@ -534,7 +503,7 @@ func (t *crapsTable) action(name, action string) []Event {
events := append([]Event{e}, t.finishCrapsControls()...)
events = append(events, t.syncRoundClock()...)
if t.players.has(name) {
- events = append(events, t.betSummary(p))
+ events = append(events, t.betSummary(p), t.reminder(name))
}
return events
}