From 7eab582d8a8ee2af1a6baae7baf0696c7005de2e Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Fri, 18 Sep 2026 15:16:44 -0400 Subject: fix: harden triggers, economy, casino, world data, and admin/net paths --- internal/casino/blackjack.go | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) (limited to 'internal/casino/blackjack.go') diff --git a/internal/casino/blackjack.go b/internal/casino/blackjack.go index 7b30699..16a480c 100644 --- a/internal/casino/blackjack.go +++ b/internal/casino/blackjack.go @@ -75,8 +75,14 @@ type blackjackTable struct { ceiling countdown activePlayer int activeHand int - dealer blackjackHand - shoe *shoe + // turnName/turnHand identify the current turn so the absolute turn + // ceiling is restarted only when the turn actually changes. A third party + // joining or leaving mid-round re-prompts the same turn and must not + // extend a hung bettor's deadline. + turnName string + turnHand int + dealer blackjackHand + shoe *shoe } // loneTurnGrace multiplies the action window into an absolute per-turn @@ -94,7 +100,7 @@ func newBlackjackTable(roomID int, cfg TableConfig, rng *rand.Rand) *blackjackTa return &blackjackTable{ tableBase: tableBase{RoomID: roomID, Config: cfg}, Rules: rules, players: newSeatSet[*blackjackPlayer](), phase: blackjackWaiting, - shoe: newShoe(rules.Decks, rules.ShuffleAt, rng), + shoe: newShoe(rules.Decks, rules.ShuffleAt, rng), turnHand: -1, } } @@ -465,6 +471,7 @@ func (t *blackjackTable) beginPlay() { t.phase = blackjackPlaying t.timer.start(t.Rules.ActionTicks) t.activePlayer, t.activeHand = 0, 0 + t.turnName, t.turnHand = "", -1 } // roundCardCeiling is the heuristic number of cards this table's round is @@ -475,19 +482,29 @@ func (t *blackjackTable) beginPlay() { // guaranteed to hold it and draw() may still rebuild an exhausted shoe // mid-round (a deliberate shuffle-through). func (t *blackjackTable) roundCardCeiling() int { - return blackjackRoundCards(t.Config.MaxPlayers, t.Rules.MaxSplitHands) + // Size the boundary reshuffle from the seats actually in the round, not + // the configured maximum: using MaxPlayers made ensureForRound dominate + // the cut card, so shuffle_at never fired on a table whose maximum seats + // implied more cards than the cut card allowed. + return blackjackRoundCards(t.bettorCount(), t.Rules.MaxSplitHands) } func (t *blackjackTable) prompt() []Event { // Every freshly-prompted turn gets a full action window; rejected inputs - // and timeouts can never extend the deadline. The turn ceiling restarts - // with the window for the same reason. + // and timeouts can never extend the deadline. t.timer.start(t.Rules.ActionTicks) - t.ceiling.start(t.Rules.ActionTicks * loneTurnGrace) name := t.activeName() if name == "" { return t.settle() } + // 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 { + t.ceiling.start(t.Rules.ActionTicks * loneTurnGrace) + t.turnName = name + t.turnHand = t.activeHand + } p := t.players.get(name) h := p.hands[t.activeHand] ranks := make([]string, len(h.cards)) @@ -710,6 +727,7 @@ func (t *blackjackTable) resetRound() { t.window.close() t.timer.stop() t.ceiling.stop() + t.turnName, t.turnHand = "", -1 for _, p := range t.players.ordered() { p.HasBet, p.Wager, p.hands, p.insurance, p.insuranceStakeID, p.declinedInsurance = false, Wager{}, nil, 0, "", false } @@ -837,7 +855,8 @@ func (t *blackjackTable) markDisconnectedLocked(name string) []Event { } switch { case index < t.activePlayer: - // The same player stays active, one seat earlier. + // The same player stays active, one seat earlier. prompt() sees the + // unchanged turn and keeps the existing ceiling. t.activePlayer-- case index == t.activePlayer: // The next bettor slides into the active seat. -- cgit v1.2.3