// Package cards holds the deck primitives every card game on Pete shares. // // gogobee never had this: blackjack carried its own deck, UNO carried another, // and hold'em leaned on a third-party one. Three shuffles, three bugs to fix // three times. The games ported over here consolidate onto this instead. // // Two rules hold throughout: // // The RNG is threaded, never global. Every shuffle takes an explicit *rand.Rand, // so a hand is reproducible from its seed — which is what makes the engines // testable, and what lets us re-deal a disputed hand and show the player exactly // what the shoe did. // // A Deck is a plain value. No pointers into it, no timers hanging off it, so a // game in progress serializes to JSON and survives a redeploy. package cards import "math/rand/v2" // Suit is one of the four French suits. type Suit uint8 const ( Spades Suit = iota Hearts Diamonds Clubs ) // Rank runs Ace(1) through King(13). Ace is low here; games that want it high // (blackjack's soft 11, hold'em's wheel) say so themselves. type Rank uint8 const ( Ace Rank = 1 Jack Rank = 11 Queen Rank = 12 King Rank = 13 ) var ( suitGlyphs = [4]string{"♠", "♥", "♦", "♣"} rankNames = [14]string{"", "A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"} ) // Card is one playing card. The short JSON keys keep a serialized shoe small — // a six-deck blackjack state is 312 of these. type Card struct { Rank Rank `json:"r"` Suit Suit `json:"s"` } // String renders the card the way a table shows it: "A♠", "10♥". func (c Card) String() string { if c.Rank < Ace || c.Rank > King || c.Suit > Clubs { return "??" } return rankNames[c.Rank] + suitGlyphs[c.Suit] } // Red reports whether the card is a red suit — the one thing every renderer // needs and nobody should re-derive. func (c Card) Red() bool { return c.Suit == Hearts || c.Suit == Diamonds } // Deck is an ordered pile of cards. The next card to come off is at index 0. type Deck []Card // NewDeck builds n standard 52-card decks in fixed order. Shuffle before use: // an unshuffled deck is a bug at a table, but it's exactly what a test wants. func NewDeck(n int) Deck { if n < 1 { n = 1 } d := make(Deck, 0, 52*n) for i := 0; i < n; i++ { for s := Spades; s <= Clubs; s++ { for r := Ace; r <= King; r++ { d = append(d, Card{Rank: r, Suit: s}) } } } return d } // Shuffle permutes the deck in place using the supplied RNG. Passing a seeded // *rand.Rand gives the same shuffle every time, which is the whole point. func (d Deck) Shuffle(rng *rand.Rand) { rng.Shuffle(len(d), func(i, j int) { d[i], d[j] = d[j], d[i] }) } // Draw takes the top card. ok is false when the deck is spent; the caller // decides whether that means reshuffle or fold, because the two games that hit // it disagree. func (d *Deck) Draw() (c Card, ok bool) { if len(*d) == 0 { return Card{}, false } c = (*d)[0] *d = (*d)[1:] return c, true } // Hand renders a run of cards for display: "A♠ 10♥". func Hand(cs []Card) string { s := "" for i, c := range cs { if i > 0 { s += " " } s += c.String() } return s } // NewRNG seeds a generator from two uint64s. Games store the seed alongside the // hand so a finished hand can be replayed exactly as it was dealt. func NewRNG(seed1, seed2 uint64) *rand.Rand { return rand.New(rand.NewPCG(seed1, seed2)) } func (s Suit) String() string { if s > Clubs { return "?" } return suitGlyphs[s] }