package casino import ( "math/rand" "strconv" ) // card is a single playing card shared by every card-based table game. type card struct { rank string suit string } func (c card) String() string { return c.rank + " of " + c.suit } // blackjackValue scores aces as 11 and faces as 10. func (c card) blackjackValue() int { switch c.rank { case "A": return 11 case "K", "Q", "J": return 10 default: value, _ := strconv.Atoi(c.rank) return value } } // baccaratValue scores aces as 1 and faces/tens as 0. func (c card) baccaratValue() int { switch c.rank { case "K", "Q", "J", "10": return 0 case "A": return 1 default: value, _ := strconv.Atoi(c.rank) return value } } // shoe is a shuffled multi-deck card shoe shared by card table games. It // reshuffles once the remaining cards fall to the shuffleAt fraction. type shoe struct { cards []card rng *rand.Rand decks int shuffleAt float64 } func newShoe(decks int, shuffleAt float64, rng *rand.Rand) *shoe { s := &shoe{rng: rng, decks: decks, shuffleAt: shuffleAt} s.shuffle() return s } func (s *shoe) shuffle() { ranks := []string{"A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"} suits := []string{"clubs", "diamonds", "hearts", "spades"} s.cards = make([]card, 0, s.decks*52) for deck := 0; deck < s.decks; deck++ { for _, suit := range suits { for _, rank := range ranks { s.cards = append(s.cards, card{rank: rank, suit: suit}) } } } 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 { if s.decks <= 0 { // A synthetic, explicitly-scripted shoe (tests) has no decks to // rebuild from and no cards left; return a visibly-invalid sentinel // card rather than panicking. Production shoes always have Decks > // 0 (Normalize), so this only surfaces a test fixture that drew // past its script. return card{rank: "?", suit: "?"} } // 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] s.cards = s.cards[:len(s.cards)-1] 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 // already dealt to hands reappear from the new shoe. func (s *shoe) shuffleIfDue() { if s.shuffleAt > 0 && len(s.cards) <= int(float64(s.decks*52)*s.shuffleAt) { s.shuffle() } }