From 39ad552cb5f48c288e1d7a62e80ceb180882eae0 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Wed, 30 Sep 2026 17:30:00 -0400 Subject: chore: remove dead casino code and fold duplicated menu/odds logic --- internal/casino/blackjack.go | 2 +- internal/casino/core.go | 4 +--- internal/casino/craps.go | 35 ++++++++++++++++++++++++-------- internal/casino/craps_projection.go | 19 ----------------- internal/casino/craps_projection_test.go | 4 +--- internal/casino/craps_simplified_test.go | 35 ++++++++++++++++++++++++++++++++ internal/casino/craps_variants.go | 14 ++++++++++--- internal/casino/craps_variants_test.go | 5 +---- internal/casino/manager_test.go | 4 ---- internal/casino/types.go | 3 --- 10 files changed, 76 insertions(+), 49 deletions(-) create mode 100644 internal/casino/craps_simplified_test.go (limited to 'internal/casino') diff --git a/internal/casino/blackjack.go b/internal/casino/blackjack.go index 925b7ef..53bee6c 100644 --- a/internal/casino/blackjack.go +++ b/internal/casino/blackjack.go @@ -535,7 +535,7 @@ func (t *blackjackTable) prompt() []Event { // The absolute turn ceiling belongs to the turn, not the prompt: restart // it only when the active (player, hand) changes. A seat event that // re-prompts the same turn must not reset a hung bettor's deadline. - if name != t.turnName || t.activeHand != t.turnHand { + if changed { t.ceiling.start(t.Rules.ActionTicks * loneTurnGrace) t.turnName = name t.turnHand = t.activeHand diff --git a/internal/casino/core.go b/internal/casino/core.go index a927ab1..fdb7877 100644 --- a/internal/casino/core.go +++ b/internal/casino/core.go @@ -181,9 +181,7 @@ func settleStakeOutcome(w Wallet, payout int, stakeIDs []string, payoutErr error } // A zero payout is a loss: consume the stakes in one save so recovery can // never refund a round that resolved. - if w != nil { - w.ResolveStakesByIDs(stakeIDs...) - } + w.ResolveStakesByIDs(stakeIDs...) return false } diff --git a/internal/casino/craps.go b/internal/casino/craps.go index 9e48667..410a372 100644 --- a/internal/casino/craps.go +++ b/internal/casino/craps.go @@ -207,17 +207,22 @@ func (t *crapsTable) bet(name, spot string, amount int, w Wallet) []Event { continue } against := strings.HasPrefix(b.kind, "dont") - x, y := crapsTrueOdds(b.number) + // The odds payout is a whole-ratio return on the stake, so the wager + // must divide evenly by the ratio's denominator. Don't odds invert + // the pass ratio, so against the same number the other side of the + // true odds is the denominator. (crapsReturn applies the full + // ratio at payout time.) + num, den := crapsTrueOdds(b.number) + payoutDen := den if against { - x, y = y, x + payoutDen = num } - _ = x limit := t.Config.Craps.oddsLimit(b.number, against) if b.odds.Total > 0 { continue } // Compare without multiplying a potentially very large stake by Nx. - if (amount-1)/limit >= b.stake.Total || amount%y != 0 { + if (amount-1)/limit >= b.stake.Total || amount%payoutDen != 0 { return reject("Odds already placed, above the free-odds limit, or not in exact payout units.") } stake, err := w.Wager(amount) @@ -575,14 +580,26 @@ func (t *crapsTable) markDisconnected(name string) []Event { var events []Event // Refunding after observing intermediate dice would turn contract bets into // free options. Disconnect consumes live bets; shutdown alone refunds them. + // Every live bet resolves as a loss covered by ONE wallet save: PayCredits + // with a zero return consumes every listed stake in that one save the same + // way a losing round does, instead of one save per bet. + var ids []string + var lines []settlementLine for _, b := range p.bets { - line, failed := t.pay(p, b, -1, 1, -1, false) - var lines []settlementLine - if !failed { - lines = append(lines, line) + ids = append(ids, b.stake.ID) + if b.odds.Total > 0 { + ids = append(ids, b.odds.ID) } - events = append(events, t.settlementEvent(p, lines, failed)) + lines = append(lines, settlementLine{ + Label: crapsBetLabel(b), Stake: b.stake.Total + b.odds.Total, + Returned: 0, Result: "loss", + }) + } + failed := false + if p.Wallet != nil { + failed = p.Wallet.PayCredits(0, ids...) != nil } + events = append(events, t.settlementEvent(p, lines, failed)) t.remove(name) events = append(events, t.syncRoundClock()...) return append(events, t.event(name+" leaves craps; live wagers are forfeited.", true)) diff --git a/internal/casino/craps_projection.go b/internal/casino/craps_projection.go index 96d64b7..c36f69f 100644 --- a/internal/casino/craps_projection.go +++ b/internal/casino/craps_projection.go @@ -95,25 +95,6 @@ func crapsBonusComplete(lo, hi int, seen map[int]bool, next int) bool { } return true } -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) -} func (t *crapsTable) rollTable(p *crapsPlayer) string { agg := map[string]int{} pairsMatter := false diff --git a/internal/casino/craps_projection_test.go b/internal/casino/craps_projection_test.go index be48691..3745249 100644 --- a/internal/casino/craps_projection_test.go +++ b/internal/casino/craps_projection_test.go @@ -48,9 +48,7 @@ func (t *crapsTable) simulateNet(name string, a, b int) int { wallets := map[string]*testWallet{} for _, seat := range t.players.ordered() { wallet := &testWallet{} - for _, wager := range seat.Wallet.(*testWallet).open { - wallet.open = append(wallet.open, wager) - } + wallet.open = append(wallet.open, seat.Wallet.(*testWallet).open...) wallets[seat.Name] = wallet cp := &crapsPlayer{Participant: Participant{Name: seat.Name, Connected: true, HasBet: seat.HasBet, Wallet: wallet}} for _, bet := range seat.bets { diff --git a/internal/casino/craps_simplified_test.go b/internal/casino/craps_simplified_test.go new file mode 100644 index 0000000..e36bc2d --- /dev/null +++ b/internal/casino/craps_simplified_test.go @@ -0,0 +1,35 @@ +package casino + +import "testing" + +func TestSimplifiedCrapsAll36SettlementsAndHouseEdge(t *testing.T) { + // Total returns for a one-credit wager, including the winning stake. + returns := [13]int{2: 4, 3: 2, 4: 2, 10: 3, 11: 3, 12: 6} + totalReturned, wins := 0, 0 + for a := 1; a <= 6; a++ { + for b := 1; b <= 6; b++ { + tb := variantFixture("simplified") + tb.join("a") + w := &testWallet{chips: 100} + tb.bet("a", "pass", 1, w) + want := returns[a+b] + for _, row := range tb.projectionFor(tb.players.get("a")) { + if row.Total == a+b && row.Net != want-1 { + t.Fatalf("preview for %d+%d: got %d, want %d", a, b, row.Net, want-1) + } + } + tb.resolveRoll(a, b) + if w.paid != want || len(w.open) != 0 || tb.players.get("a").HasBet || tb.point != 0 { + t.Fatalf("roll %d+%d: paid %d, want %d; open %v, point %d", a, b, w.paid, want, w.open, tb.point) + } + totalReturned += w.paid + if w.paid > 0 { + wins++ + } + } + } + // 36 credits wagered, 35 returned: house edge exactly 1/36 = 2.777...%. + if totalReturned != 35 || wins != 12 { + t.Fatalf("all 36 rolls: returned %d, wins %d", totalReturned, wins) + } +} diff --git a/internal/casino/craps_variants.go b/internal/casino/craps_variants.go index b3be51b..e64fd0f 100644 --- a/internal/casino/craps_variants.go +++ b/internal/casino/craps_variants.go @@ -75,10 +75,18 @@ func (c CrapsConfig) outcome(kind string, n, a, b, point int) (bool, int, int) { s := a + b switch c.variant() { case "simplified": - if s == 2 || s == 3 || s == 4 || s == 10 || s == 11 || s == 12 { + switch s { + case 2: + return true, 3, 1 + case 3, 4: return true, 1, 1 + case 10, 11: + return true, 2, 1 + case 12: + return true, 5, 1 + default: + return true, -1, 1 } - return true, -1, 1 case "high_point": if n == 0 { if s == 2 || s == 3 { @@ -161,7 +169,7 @@ func (c CrapsConfig) variantRulesText() string { rules := "" switch c.variant() { case "simplified": - rules = "Pass only, one roll: 2/3/4/10/11/12 win 1:1; 5/6/7/8/9 lose. No point. A loss passes the dice." + rules = "Pass only, one roll of two dice. Net payouts: 2 pays 3:1; 3/4 pay 1:1; 10/11 pay 2:1; 12 pays 5:1. Totals 5/6/7/8/9 lose. Winning bets also return their stake. No point, odds, come, place, side bets or multi-roll wagers. House edge: 1/36 (approximately 2.78%). A loss passes the dice." case "high_point": rules = "Pass only, 1:1. Come-out 2/3 roll again, 11/12 win; 4-10 establish a point. The very next roll wins only if HIGHER than the point; equal/lower loses. Loss passes the dice." case "die_rich": diff --git a/internal/casino/craps_variants_test.go b/internal/casino/craps_variants_test.go index 687a496..284a3df 100644 --- a/internal/casino/craps_variants_test.go +++ b/internal/casino/craps_variants_test.go @@ -51,10 +51,7 @@ func TestVariantAllOutcomes(t *testing.T) { switch v { case "simplified": done = true - profit = -1 - if s <= 4 || s >= 10 { - profit = 1 - } + profit = [13]int{0, 0, 3, 1, 1, -1, -1, -1, -1, -1, 2, 2, 5}[s] case "high_point": if n == 0 { done = s >= 11 diff --git a/internal/casino/manager_test.go b/internal/casino/manager_test.go index 43bd67f..889618c 100644 --- a/internal/casino/manager_test.go +++ b/internal/casino/manager_test.go @@ -58,10 +58,6 @@ func (w *testWallet) ResolveStakesByIDs(stakeIDs ...string) { } } -// ResolveStakes mirrors the production wallet: every outstanding stake is -// settled (no longer refundable). -func (w *testWallet) ResolveStakes() { w.open = nil } - // RefundOutstanding returns every outstanding stake to the balances and // resolves the stakes, mirroring the production refund path: credits are // paid through PayCredits (recorded in paid), chips return as chips. diff --git a/internal/casino/types.go b/internal/casino/types.go index 56a09d8..62a8f34 100644 --- a/internal/casino/types.go +++ b/internal/casino/types.go @@ -217,9 +217,6 @@ type Wallet interface { // startup, never paid on top of the refund — and the game must not // announce the win. PayCredits(amount int, stakeIDs ...string) error - // ResolveStakes consumes every pending stake for this player: the round - // resolved as a loss or a forfeit, so the stakes must not be refunded. - ResolveStakes() // ResolveStake consumes ONE pending stake by wallet handle (a single // hand paid or lost while a sibling hand's round is still in flight). ResolveStake(stakeID string) -- cgit v1.2.3