aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/cards.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-18 05:31:09 -0400
committerhistoria <[not public]>2026-09-18 05:31:09 -0400
commit153acb7f644d755c45533b7c5311e3ffa47e8973 (patch)
tree7645e6f7b65bc3830b078e3b9336d2e6b199d584 /internal/casino/cards.go
parent87d08f0151e643951769e323995f1c5ed89ee200 (diff)
downloadthehouseoficarus-153acb7f644d755c45533b7c5311e3ffa47e8973.tar.gz
fix: harden engine across casino, economy, concurrency, security, and content
Diffstat (limited to 'internal/casino/cards.go')
-rw-r--r--internal/casino/cards.go31
1 files changed, 27 insertions, 4 deletions
diff --git a/internal/casino/cards.go b/internal/casino/cards.go
index b1c2cbb..27da8aa 100644
--- a/internal/casino/cards.go
+++ b/internal/casino/cards.go
@@ -68,12 +68,17 @@ func (s *shoe) shuffle() {
s.rng.Shuffle(len(s.cards), func(i, j int) { s.cards[i], s.cards[j] = s.cards[j], s.cards[i] })
}
+// remaining reports the cards left in the shoe.
+func (s *shoe) remaining() int { return len(s.cards) }
+
func (s *shoe) draw() card {
if len(s.cards) == 0 {
- // The shoe ran dry mid-round (only possible when the configured
- // deck count is very small relative to the table): rebuild it
- // rather than panic. The cut-card reshuffle itself happens
- // between rounds via shuffleIfDue, never per draw.
+ // Shuffle-through: if the shoe runs out mid-round it is rebuilt from
+ // all decks, so a card already dealt this round can reappear. This
+ // matches real blackjack, where an exhausted shoe is shuffled through
+ // mid-round; ensureForRound makes it rare by reshuffling at round
+ // boundaries, and this keeps a directly-constructed table from
+ // panicking.
s.shuffle()
}
card := s.cards[len(s.cards)-1]
@@ -81,6 +86,24 @@ func (s *shoe) draw() card {
return card
}
+// ensureForRound reshuffles when the shoe is unlikely to cover the round that
+// is about to be dealt. It MUST be called at a round boundary, before any card
+// of that round is dealt: a rebuild there cannot redeal a card the current
+// round already holds, which is exactly what draw()'s exhausted-shoe
+// shuffle-through does. need is a heuristic estimate of the cards one round
+// can consume (see blackjackRoundCards/baccaratRoundCards), not a certified
+// maximum, so coverage is best-effort.
+func (s *shoe) ensureForRound(need int) {
+ if s.decks <= 0 {
+ // No deck composition to refill from: this is a synthetic shoe with
+ // an explicit card script (tests), which must not be replaced.
+ return
+ }
+ if len(s.cards) < need {
+ s.shuffle()
+ }
+}
+
// shuffleIfDue reshuffles the shoe once its remaining cards have fallen
// to the shuffleAt fraction (the cut card). It must be called at round
// boundaries only — shuffling between individual draws would let cards