diff options
Diffstat (limited to 'internal/casino/pai_gow.go')
| -rw-r--r-- | internal/casino/pai_gow.go | 141 |
1 files changed, 83 insertions, 58 deletions
diff --git a/internal/casino/pai_gow.go b/internal/casino/pai_gow.go index b853829..44893c1 100644 --- a/internal/casino/pai_gow.go +++ b/internal/casino/pai_gow.go @@ -3,6 +3,7 @@ package casino import ( "fmt" "math/rand" + "sort" "strings" "sync" ) @@ -147,19 +148,14 @@ func (t *paiGowTable) hintsLocked(name string) []MenuCommand { // deals, so there is no later chance to add them. if p.fortune.Total == 0 { commands = append(commands, MenuCommand{Command: "bet <amount> fortune", Desc: "Optional Fortune side bet; place before your main bet"}) - } else { - commands = append(commands, MenuCommand{Command: "unbet fortune", Desc: "Take back your Fortune side bet"}) } - if faceUp { - if p.pushace.Total == 0 { - commands = append(commands, MenuCommand{Command: "bet <amount> pushace", Desc: "Optional Push Ace High side bet; place before your main bet"}) - } else { - commands = append(commands, MenuCommand{Command: "unbet pushace", Desc: "Take back your Push Ace High side bet"}) - } + if faceUp && p.pushace.Total == 0 { + commands = append(commands, MenuCommand{Command: "bet <amount> pushace", Desc: "Optional Push Ace High side bet; place before your main bet"}) } commands = append(commands, MenuCommand{Command: "bet <amount>", Desc: "Main wager; deals once every bettor is in"}) - } else { - commands = append(commands, MenuCommand{Command: "unbet main", Desc: "Take back your main wager before the deal"}) + } + if p.main.Total > 0 || p.fortune.Total > 0 || p.pushace.Total > 0 { + commands = append(commands, MenuCommand{Command: "unbet", Desc: "Take back all your wagers before the deal"}) } case "setting": if p.main.Total > 0 && !p.ready { @@ -179,7 +175,7 @@ func (t *paiGowTable) hintsLocked(name string) []MenuCommand { case "settling": commands = append(commands, MenuCommand{Command: "settle", Desc: "Retry the same pending payment"}) } - return append(commands, MenuCommand{Command: "table rules", Desc: "Rules and Fortune felt"}, MenuCommand{Command: "rules", Desc: "Learn Pai Gow"}, MenuCommand{Command: "stop", Desc: "Leave after finishing your wager"}) + return append(commands, MenuCommand{Command: "table", Desc: "View the felt"}, MenuCommand{Command: "rules", Desc: "Learn Pai Gow"}, MenuCommand{Command: "stop", Desc: "Leave after finishing your wager"}) } func (t *paiGowTable) hints(name string) []MenuCommand { t.mu.Lock() @@ -193,7 +189,7 @@ func (t *paiGowTable) wagers(name string) []WagerRow { if p == nil || t.phase == "setting" || t.phase == "settling" { return nil } - rows := []WagerRow{{Bet: "main", Payout: "See table rules", Win: "Beat both dealer hands"}, {Bet: "fortune", Payout: "See Fortune felt", Win: "Best seven-card bonus; independent of main result"}} + rows := []WagerRow{{Bet: "main", Payout: "See the felt", Win: "Beat both dealer hands"}, {Bet: "fortune", Payout: "See Fortune felt", Win: "Best seven-card bonus; independent of main result"}} if t.Config.PaiGow.Variant == PaiGowFaceUp { rows = append(rows, WagerRow{Bet: "pushace", Payout: "40/15/5:1", Win: "Dealer ace-high pai gow; both 40:1"}) } @@ -207,6 +203,25 @@ func pgViews(cards []card) []CardView { return out } +// pgSortHand orders a freshly dealt hand small to large so the numbered +// positions read naturally: by rank first, then by suit. The Joker sorts last. +func pgSortHand(cards []card) { + sort.SliceStable(cards, func(i, j int) bool { + ri, rj := pgSortRank(cards[i]), pgSortRank(cards[j]) + if ri != rj { + return ri < rj + } + return cards[i].suit < cards[j].suit + }) +} + +func pgSortRank(c card) int { + if c.rank == "JK" { + return 15 + } + return pokerRank(c) +} + // pgCardNums maps each card in hand to its 1-based position in all. Dealt // cards are unique, so the match is unambiguous and preserves hand order. func pgCardNums(hand, all []card) []int { @@ -359,7 +374,7 @@ func (t *paiGowTable) bet(name, spot string, amount int, w Wallet) []Event { return fail("Place side bets before your main wager.") } if spot == "main" && p.pushace.Total > amount/2 { - return fail("Push Ace High is capped at half the main wager; take it back with unbet pushace first.") + return fail("Push Ace High is capped at half the main wager; take it back with unbet first.") } if amount < t.Config.MinBet || amount > t.Config.MaxBet || amount > pgMaxWager { return fail("Wager must be " + betRangeText(t.Config.MinBet, min(t.Config.MaxBet, pgMaxWager)) + ".") @@ -372,16 +387,19 @@ func (t *paiGowTable) bet(name, spot string, amount int, w Wallet) []Event { return fail(walletErrorMessage(err)) } p.wallet = w + // A fresh wager starts a new round in the seat's view. Without this a side + // bet placed before the next main wager would leave the previous round's + // cards and showdown on screen. + p.cards = nil + p.split = pgSplit{} + p.pending = pgSplit{} + p.showdown = nil + p.result = "" + p.ready = false + p.paid = false if spot == "main" { p.main = stake p.last = amount - p.cards = nil - p.split = pgSplit{} - p.pending = pgSplit{} - p.showdown = nil - p.result = "" - p.ready = false - p.paid = false } else if spot == "pushace" { p.pushace = stake } else { @@ -467,6 +485,7 @@ func (t *paiGowTable) dealLocked() []Event { continue } p.cards = packets[i] + pgSortHand(p.cards) p.ready = p.disconnected || autoPush if p.ready { p.split = pgHouseRules(p.cards, t.a2345High()) @@ -526,7 +545,7 @@ func (t *paiGowTable) action(name, action string) []Event { return append([]Event{t.private(name, "Autoset is "+state+".")}, t.statusLocked(name, false)...) } if strings.HasPrefix(action, "unbet") { - return t.unbetLocked(p, action) + return t.unbetLocked(p) } if action == "deal" && (t.phase == "waiting" || t.phase == "betting") && p.main.Total > 0 { if t.allBetLocked() { @@ -707,23 +726,35 @@ 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 { + return nil + } t.ticks-- if t.ticks > 0 { return nil } return t.finishBettingLocked() case "setting": + // A lone player has no time limit; wait for them to set. + if t.activePlayersLocked() <= 1 { + return nil + } t.ticks-- if t.ticks > 0 { return nil } + var events []Event for _, p := range t.players { if p.main.Total > 0 && !p.ready { p.split = pgHouseRules(p.cards, t.a2345High()) p.ready = true + e := t.private(p.name, "Your hand timed out and was set the house way.") + e.Color = "casino_warning" + events = append(events, e) } } - return t.prepareSettlementLocked() + return append(events, t.prepareSettlementLocked()...) } return nil } @@ -776,17 +807,23 @@ func (t *paiGowTable) afterSeatChangeLocked() []Event { } // finishBettingLocked closes the betting window on the timer: side bets on -// seats that never placed a main wager are returned, and the round deals when -// at least one main wager is live. With no main wager the table resets. +// seats that never placed a main wager are returned, every non-bettor is told +// the window closed (no silent timer), and the round deals when at least one +// main wager is live. With no main wager the table resets. func (t *paiGowTable) finishBettingLocked() []Event { var events []Event + deals := t.anyMainLocked() for _, p := range t.players { if p.main.Total > 0 { continue } - events = append(events, t.refundSideBetsLocked(p)...) + if side := t.refundSideBetsLocked(p); len(side) > 0 { + events = append(events, side...) + } else if deals { + events = append(events, t.private(p.name, "The betting window closed; you did not place a main wager and sit out this round.")) + } } - if !t.anyMainLocked() { + if !deals { t.resetTableLocked() return events } @@ -830,17 +867,23 @@ func (t *paiGowTable) resetTableLocked() { } } -// unbetLocked takes back one or all of a player's wagers before the deal. -// Refunds go through PayCredits with the original stake handle so the ledger -// entry resolves in the same save; wagers are only cleared on success. -func (t *paiGowTable) unbetLocked(p *pgPlayer, action string) []Event { - target := strings.TrimSpace(strings.TrimPrefix(action, "unbet")) - if target == "" { - target = "all" - } - if target != "all" && target != "main" && target != "fortune" && target != "pushace" { - return []Event{t.private(p.name, "Unbet what? Use unbet main, unbet fortune, unbet pushace, or unbet all.")} +// activePlayersLocked counts the connected players with a live main wager. +// A lone bettor never runs on a clock, so every timeout path checks this +// before decrementing. A seated spectator does not start a timer for them. +func (t *paiGowTable) activePlayersLocked() int { + n := 0 + for _, p := range t.players { + if !p.disconnected && p.main.Total > 0 { + 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. +func (t *paiGowTable) unbetLocked(p *pgPlayer) []Event { if t.phase != "waiting" && t.phase != "betting" { return []Event{t.private(p.name, "You can only take back bets before the cards are dealt.")} } @@ -852,18 +895,9 @@ func (t *paiGowTable) unbetLocked(p *pgPlayer, action string) []Event { total += w.Total ids = append(ids, w.ID) } - switch target { - case "main": - take(&p.main) - case "fortune": - take(&p.fortune) - case "pushace": - take(&p.pushace) - default: - take(&p.main) - take(&p.fortune) - take(&p.pushace) - } + take(&p.main) + take(&p.fortune) + take(&p.pushace) if total == 0 { return []Event{t.private(p.name, "Nothing to take back.")} } @@ -872,16 +906,7 @@ func (t *paiGowTable) unbetLocked(p *pgPlayer, action string) []Event { return []Event{t.private(p.name, payoutFailedMessage())} } } - switch target { - case "main": - p.main = Wager{} - case "fortune": - p.fortune = Wager{} - case "pushace": - p.pushace = Wager{} - default: - p.main, p.fortune, p.pushace = Wager{}, Wager{}, Wager{} - } + p.main, p.fortune, p.pushace = Wager{}, Wager{}, Wager{} if !t.anyMainLocked() && t.phase == "betting" { t.phase = "waiting" t.ticks = 0 |
