diff options
Diffstat (limited to 'internal/casino/craps.go')
| -rw-r--r-- | internal/casino/craps.go | 157 |
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 } |
