diff options
| author | historia <[not public]> | 2026-10-05 17:05:14 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-10-05 17:05:14 -0400 |
| commit | e42952469dadfb3535633c66c881050ac128703c (patch) | |
| tree | a78f7396b72cdbd6fa4f6c454c830f40cf2ad0cb /internal/casino | |
| parent | 90ad8c0c33b519d5830c69d4128bdc984b5e9de8 (diff) | |
| download | thehouseoficarus-e42952469dadfb3535633c66c881050ac128703c.tar.gz | |
fix: gather verbs, walk oom, admin path bypass, roulette refund
Diffstat (limited to 'internal/casino')
| -rw-r--r-- | internal/casino/cash_hunt.go | 4 | ||||
| -rw-r--r-- | internal/casino/craps_ux.go | 2 | ||||
| -rw-r--r-- | internal/casino/craps_wagers.go | 8 | ||||
| -rw-r--r-- | internal/casino/pai_gow.go | 19 | ||||
| -rw-r--r-- | internal/casino/pai_gow_rules.go | 5 | ||||
| -rw-r--r-- | internal/casino/pai_gow_test.go | 28 | ||||
| -rw-r--r-- | internal/casino/plinko.go | 4 | ||||
| -rw-r--r-- | internal/casino/registry.go | 2 | ||||
| -rw-r--r-- | internal/casino/roulette.go | 14 | ||||
| -rw-r--r-- | internal/casino/roulette_test.go | 8 | ||||
| -rw-r--r-- | internal/casino/settlement_test.go | 2 | ||||
| -rw-r--r-- | internal/casino/table_hints.go | 2 | ||||
| -rw-r--r-- | internal/casino/table_ux.go | 2 | ||||
| -rw-r--r-- | internal/casino/video_poker_config.go | 6 |
14 files changed, 83 insertions, 23 deletions
diff --git a/internal/casino/cash_hunt.go b/internal/casino/cash_hunt.go index 7c20a5a..a07b09a 100644 --- a/internal/casino/cash_hunt.go +++ b/internal/casino/cash_hunt.go @@ -36,10 +36,10 @@ func cashHuntPrize(risk, ticket int) int { func validateCashHunt(c TableConfig) error { if c.MaxPlayers > 1 { - return fmt.Errorf("Cash Hunt has one seat") + return fmt.Errorf("cash hunt has one seat") } if c.MaxBet > MaxCredits/cashHuntMaxMultiplier { - return fmt.Errorf("Cash Hunt max_bet must not exceed %d", MaxCredits/cashHuntMaxMultiplier) + return fmt.Errorf("cash hunt max_bet must not exceed %d", MaxCredits/cashHuntMaxMultiplier) } return nil } diff --git a/internal/casino/craps_ux.go b/internal/casino/craps_ux.go index 9538e7c..a4495cb 100644 --- a/internal/casino/craps_ux.go +++ b/internal/casino/craps_ux.go @@ -56,7 +56,7 @@ func (t *crapsTable) commands(name string) []MenuCommand { // "rules" stays available to a seated player at every phase of every // variant; "list" rides along whenever the wager table names more // than one bet. - commands = withTableHints(commands, t.wagerRows(name)) + commands = withTableHints(commands, t.wagerRows()) } return append(commands, MenuCommand{Command: "table", Desc: "View the felt"}) } diff --git a/internal/casino/craps_wagers.go b/internal/casino/craps_wagers.go index beb8722..33b465b 100644 --- a/internal/casino/craps_wagers.go +++ b/internal/casino/craps_wagers.go @@ -9,7 +9,7 @@ import ( // where two bets share the same payout (pass/don't pass, the place numbers, // all free odds) so the list stays compact and the Win/action column rarely // wraps. Payouts are profit odds; winning stakes return alongside the profit. -func (t *crapsTable) wagerRows(name string) []WagerRow { +func (t *crapsTable) wagerRows() []WagerRow { c := t.Config.Craps rows := []WagerRow{} passWin := "Come out: 7/11 wins, 2/3/12 loses / Point on: Roll the point to win." @@ -52,7 +52,7 @@ func (t *crapsTable) wagerRows(name string) []WagerRow { WagerRow{"hard 6/8", "9:1", "Roll this pair before next 7"}, ) } - rows = append(rows, crapsPropRows(c, false)...) + rows = append(rows, crapsPropRows(false)...) default: // bank and diceless rows = append(rows, WagerRow{"pass", "1:1", passWin}, @@ -71,7 +71,7 @@ func (t *crapsTable) wagerRows(name string) []WagerRow { WagerRow{"hard 6/8", "9:1", "Roll this pair before next 7"}, ) } - rows = append(rows, crapsPropRows(c, !t.rolled)...) + rows = append(rows, crapsPropRows(!t.rolled)...) } return rows } @@ -87,7 +87,7 @@ func crapsBuyLayRows() []WagerRow { // crapsPropRows is the one-roll section of the list: field, the proposition // bets and (when bonuses are allowed and still open) the Fire and ATS bets. -func crapsPropRows(c CrapsConfig, bonuses bool) []WagerRow { +func crapsPropRows(bonuses bool) []WagerRow { rows := []WagerRow{ {"field", "1:1", "Next roll is 2-4 or 9-12"}, {"seven", "4:1", "Next roll totals 7"}, diff --git a/internal/casino/pai_gow.go b/internal/casino/pai_gow.go index a2dafcf..8f581db 100644 --- a/internal/casino/pai_gow.go +++ b/internal/casino/pai_gow.go @@ -747,8 +747,11 @@ func (t *paiGowTable) tick() []Event { if t.allBetLocked() { return t.dealLocked() } - // A lone player has no time limit; wait for their bet instead. - if t.activePlayersLocked() <= 1 { + // A lone seated player has no time limit; wait for their bet instead. + // Count seated players, not bettors: with one bettor and one idle seat + // activePlayersLocked() is 1 forever, so the window never closes and + // the round stalls. + if t.seatedPlayersLocked() <= 1 { return nil } t.ticks-- @@ -901,6 +904,18 @@ func (t *paiGowTable) activePlayersLocked() int { return n } +// seatedPlayersLocked counts connected seats, bettors or not. The betting +// window is bounded by seats present, so an idle seat still lets it expire. +func (t *paiGowTable) seatedPlayersLocked() int { + n := 0 + for _, p := range t.players { + if !p.disconnected { + n++ + } + } + return n +} + // unbetLocked takes back every wager before the deal. Refunds go through // PayCredits with the original stake handles so the ledger entries resolve in // the same save; wagers are only cleared on success. diff --git a/internal/casino/pai_gow_rules.go b/internal/casino/pai_gow_rules.go index 494cde7..edfa38c 100644 --- a/internal/casino/pai_gow_rules.go +++ b/internal/casino/pai_gow_rules.go @@ -393,8 +393,9 @@ type FortuneRow struct { Odds, Envy int } -// Wizard of Odds Fortune table 8, the lowest-edge published fixed schedule -// among its eight tables. With Envy disabled, quads receive a custom +5. +// FortunePayTable returns the Wizard of Odds Fortune table 8, the lowest-edge +// published fixed schedule among its eight tables. With Envy disabled, quads +// receive a custom +5. func FortunePayTable(envy bool) []FortuneRow { rows := []FortuneRow{{"Natural seven-card straight flush", 1000, 500}, {"Royal flush + royal match", 750, 250}, {"Wild seven-card straight flush", 500, 150}, {"Five aces", 250, 100}, {"Royal flush", 150, 50}, {"Straight flush", 50, 20}, {"Four of a kind", 25, 5}, {"Full house", 5, 0}, {"Flush", 4, 0}, {"Three of a kind", 3, 0}, {"Straight", 2, 0}, {"Three pair", 1, 0}} if !envy { diff --git a/internal/casino/pai_gow_test.go b/internal/casino/pai_gow_test.go index 7be9e8a..100b87d 100644 --- a/internal/casino/pai_gow_test.go +++ b/internal/casino/pai_gow_test.go @@ -838,3 +838,31 @@ func TestPaiGowBettingWindowTimeoutNotifiesNonBettor(t *testing.T) { t.Fatal("non-bettor was not told the betting window closed") } } + +// Regression: with one bettor and one idle seated player the betting window +// must still close. activePlayersLocked counts bettors, so it used to stay at 1 +// forever and the round stalled until the idle seat bet or left. +func TestPaiGowBettingWindowExpiresWithOneBettorAndIdleSeat(t *testing.T) { + tab := pgTestTable(false) // BettingTicks 2 + w := &testWallet{chips: 100} + tab.join("a") + tab.join("b") + tab.bet("a", "main", 10, w) + if tab.phase != "betting" { + t.Fatalf("setup: wanted betting, got %s", tab.phase) + } + notified := false + for i := 0; i < 4; i++ { + for _, e := range tab.tick() { + if e.PrivateTo == "b" && strings.Contains(e.Message, "betting window closed") { + notified = true + } + } + } + if tab.phase == "betting" { + t.Fatal("round stalled in betting with one bettor and one idle seat") + } + if !notified { + t.Fatal("idle seat was not sat out when the window closed") + } +} diff --git a/internal/casino/plinko.go b/internal/casino/plinko.go index 412b72c..d50cf6f 100644 --- a/internal/casino/plinko.go +++ b/internal/casino/plinko.go @@ -12,8 +12,8 @@ import ( const plinkoRows = 16 -// RTP is the target return percentage for the one-decimal paytables. -// Fixed expert edge prizes alone return 30.517578125%. +// PlinkoConfig holds the RTP target for the one-decimal paytables. Fixed +// expert edge prizes alone return 30.517578125%. type PlinkoConfig struct { RTP float64 `yaml:"rtp,omitempty"` } diff --git a/internal/casino/registry.go b/internal/casino/registry.go index b13125d..716782e 100644 --- a/internal/casino/registry.go +++ b/internal/casino/registry.go @@ -107,7 +107,7 @@ func init() { houseEdge: bigSixHouseEdge, validate: func(c TableConfig) error { if c.BettingTicks != 0 && c.BettingTicks != 25 { - return fmt.Errorf("Big 6 betting_ticks must be 25") + return fmt.Errorf("big 6 betting_ticks must be 25") } return nil }, diff --git a/internal/casino/roulette.go b/internal/casino/roulette.go index ad0a706..1069a68 100644 --- a/internal/casino/roulette.go +++ b/internal/casino/roulette.go @@ -822,11 +822,19 @@ func (t *rouletteTable) markDisconnected(name string) []Event { if p == nil { return nil } - // No outcome has been sampled or revealed during the spin. Disconnect - // refunds are safe, just as pre-deal baccarat refunds are. + // Once the spin starts the winning pocket is fixed, and from frame 5 it is + // on screen, so a disconnect refund would allow selective losses. Forfeit + // the stake while the wheel is spinning, matching Big 6; refund only before + // the spin starts. var events []Event if p.Wallet != nil { - if _, _, err := p.Wallet.RefundOutstanding(); err != nil { + var err error + if p.HasBet && t.phase == phasePlaying { + err = p.Wallet.PayCredits(0, p.Wager.ID) + } else { + _, _, err = p.Wallet.RefundOutstanding() + } + if err != nil { events = append(events, t.private(name, "Your stake remains pending for startup recovery.")) } } diff --git a/internal/casino/roulette_test.go b/internal/casino/roulette_test.go index 97bff2a..a0e04d4 100644 --- a/internal/casino/roulette_test.go +++ b/internal/casino/roulette_test.go @@ -356,7 +356,13 @@ func TestRouletteRefundAndPersistenceFailure(t *testing.T) { table.markDisconnected("a") table.refundAll() table.tick() - if w.chips != 100 || len(w.open) != 0 || w.paid != 0 { + if spinning { + // Once the spin starts the winning pocket is fixed; disconnecting + // forfeits the stake instead of allowing a selective refund. + if w.chips != 90 || len(w.open) != 0 || w.paid != 0 { + t.Fatalf("disconnect forfeit: %+v", w) + } + } else if w.chips != 100 || len(w.open) != 0 || w.paid != 0 { t.Fatalf("disconnect refund: %+v", w) } } diff --git a/internal/casino/settlement_test.go b/internal/casino/settlement_test.go index 9c110d6..79954bc 100644 --- a/internal/casino/settlement_test.go +++ b/internal/casino/settlement_test.go @@ -17,7 +17,7 @@ func (w *recoveringSettlementWallet) PayCredits(n int, ids ...string) error { return w.testWallet.PayCredits(n, ids...) } -func receiptLines(events []Event, name string) []SettlementLine { +func receiptLines(events []Event, name string) []SettlementLine { //nolint:unparam // keeps the recipient explicit at every call site var lines []SettlementLine for _, e := range events { if e.PrivateTo == name { diff --git a/internal/casino/table_hints.go b/internal/casino/table_hints.go index 5b1d86e..3a87c94 100644 --- a/internal/casino/table_hints.go +++ b/internal/casino/table_hints.go @@ -8,7 +8,7 @@ import "fmt" // table controls never need a per-game switch. All entries are seated only; // a standing player is not told how to bet at a game they have not joined. -func (t *crapsTable) wagers(name string) []WagerRow { return t.wagerRows(name) } +func (t *crapsTable) wagers(name string) []WagerRow { return t.wagerRows() } func (t *crapsTable) hints(name string) []MenuCommand { return t.commands(name) } func (t *rouletteTable) wagers(name string) []WagerRow { return t.wagerRows() } diff --git a/internal/casino/table_ux.go b/internal/casino/table_ux.go index 5d70aaf..6ba76f2 100644 --- a/internal/casino/table_ux.go +++ b/internal/casino/table_ux.go @@ -283,7 +283,7 @@ func (t *blackjackTable) insuranceMenuLocked(p *blackjackPlayer) Event { return e } -// settlementLine is one settled bet within a round. Label names the bet for +// SettlementLine is one settled bet within a round. Label names the bet for // the itemized line (empty for a lone anonymous hand); Result is the engine's // settlement word ("win", "loss", "bust", "push", "blackjack", // "surrender", "insurance"). diff --git a/internal/casino/video_poker_config.go b/internal/casino/video_poker_config.go index 88fcbc4..aa01389 100644 --- a/internal/casino/video_poker_config.go +++ b/internal/casino/video_poker_config.go @@ -5,8 +5,10 @@ import ( "strings" ) -// Cabinet and Payout are authored settings. The remaining fields identify the -// selected game and its resolved schedule; schedules are never separate games. +// VideoPokerConfig is the authored cabinet/payout pair plus the rest of the +// cabinet settings. Cabinet and Payout are authored settings; the remaining +// fields identify the selected game and its resolved schedule; schedules are +// never separate games. type VideoPokerConfig struct { Cabinet string `yaml:"cabinet,omitempty"` Payout string `yaml:"payout,omitempty"` |
