package cardsim import ( "errors" "fmt" "math" "math/rand" ) var ( WarningBackwardsRange = Warningf("low end of range was above high end of range, flipping it") WarningTopClamped = Warningf("target above top of deck moved to top") WarningBottomClamped = Warningf("target below bottom of deck moved to bottom") WarningTooFewCards = Warningf("too few cards") ErrorNoCards = errors.New("totally out of cards") ) const ( BottomOfDeck = math.MaxInt ) // The Deck stores cards yet-to-be-dealt. type Deck[C StatsCollection] struct { cards []Card[C] rand *rand.Rand } // Len returns the number of cards in the Deck. func (d *Deck[C]) Len() int { return len(d.cards) } // Insert puts one or more cards at a specific location in the Deck. Cards // at that location and all locations after are shifted deeper into the deck. // Negative indexes are counted from the bottom of the deck. BottomOfDeck is // a sentinel value for the bottommost position; -1 is one card above. // // Inserting at "one past the end", or using the BottomOfDeck sentinel, // adds a card to the bottom of the deck without error. Adding it deeper // than that does the same but returns WarningBottomClamped. Negative indexes // that exceed the number of cards in the deck go to the top of the deck; if // it's more negative than "one before the beginning", this returns // WarningTopClamped. Like all warnings, these can be safely ignored and the // program is in a well-defined state, but you may want to check for them // if you expect some other behavior. func (d *Deck[C]) Insert(idx int, card ...Card[C]) error { var errs ErrorCollector // Calculate actual target index. switch { case idx == BottomOfDeck: idx = d.Len() case idx > d.Len(): errs.Add(fmt.Errorf("%w: position %d requested in %d cards", WarningBottomClamped, idx, d.Len())) idx = d.Len() case idx < -d.Len(): errs.Add(fmt.Errorf("%w: position %d (backwards) requested in %d cards", WarningTopClamped, idx, d.Len())) idx = 0 case idx < 0: idx += d.Len() } // remaining case: 0 <= idx <= d.Len(), which is a normal forward insert index. d.cards = InsertInto(d.cards, idx, card...) return errs.Emit() } // Draw pops the 0th card off the deck and returns it. If the deck has no cards, it returns nil. func (d *Deck[C]) Draw() Card[C] { if d.Len() == 0 { return nil } ret := d.cards[0] // Discard the top card via reslicing. This means that cards and deck slots // hang around until an Append copies the deck structure somewhere else. // If this turns out to be a problem, implement a more efficient algorithm // or data structure. d.cards = d.cards[1:] return ret } // DrawN pops the top N cards off the deck and returns them. If the deck does // not have that many cards, this returns as many cards as it has and // WarningTooFewCards, unless the deck has no cards at all, in which case it // returns nil, ErrorNoCards. func (d *Deck[C]) DrawN(n int) ([]Card[C], error) { if d.Len() == 0 { return nil, ErrorNoCards } if d.Len() <= n { ret := d.cards d.cards = nil if len(ret) < n { return ret, WarningTooFewCards } return ret, nil } ret := d.cards[:n] d.cards = d.cards[n:] return ret, nil } // InsertRandom puts a card randomly in the deck, with equal probability for // any of the Len()+1 possible locations. func (d *Deck[C]) InsertRandom(card Card[C]) error { return d.InsertRandomRange(0.0, 1.0, card) } // InsertRandomTop puts a card randomly in the top part of the deck, within // the fraction of the deck specified by frac, between 0.0 and 1.0 inclusive. // If frac is not in this range, it returns ErrBadFrac and does not insert // the card. // // InsertRandomTop chooses its insertion location uniformly randomly within // the provided fractional range. If `frac * (d.Len()+1)` is not an integer, // InsertRandomTop places the card slightly past the end of the specified range // if its random location lands within the "fractional slot" at the end that // didn't line up evenly. // // Example // ------- // // Consider a 1-card deck, `[A]`. This deck has two places a card (we'll call // it `B`) can be inserted: before or after the existing card, yielding decks // `[A, B]` and `[B, A]`. If `InsertRandomTop` is invoked on this deck, then: // // - **If `frac <= 0.5`:** The only possible result is `[B, A]`. The range // covering 50% of the open slots does not include any portion of any slot // beyond the first. It may include less than the entire first slot, but // there are no other slots possible. // - **If `frac == 1.0`:** Both placements are equally likely. All slots are // completely covered. // - **If `frac == 0.75`:** It will choose `[B, A]` two-thirds of the time and // `[A, B]` one-third of the time. The first 0.5 of the range covers the // first slot (before `A`); the remaining 0.25 covers _half_ of the slot // after A. This slot is therefore half as likely to be chosen as any other // slot -- the only other slot, in this simplified example. // // For most practical purposes, this detail can be safely ignored. func (d *Deck[C]) InsertRandomTop(frac float64, card Card[C]) error { return d.InsertRandomRange(0.0, frac, card) } // InsertRandomBottom puts a card randomly somewhere in the bottom part of // the deck, within the fraction of the deck specified by frac. See // InsertRandomTop for details on edge conditions of range calculation. func (d *Deck[C]) InsertRandomBottom(frac float64, card Card[C]) error { return d.InsertRandomRange(frac, 1.0, card) } // InsertRandomRange puts a card randomly somewhere between the loFrac and // hiFrac fraction of the deck. See InsertRandomTop for details on edge // conditions of range calculation. 0.0 is the slot before the top of the // deck and 1.0 is the slot after the end. // // Warnings it may return include WarningBackwardsRange, WarningTopClamped, // and WarningBottomClamped, and any combination of these (multiple errors // can be aggregated into one and errors.Is will recognize all of them). func (d *Deck[C]) InsertRandomRange(loFrac, hiFrac float64, card Card[C]) error { var errs ErrorCollector // Check argument ordering and bounds. if loFrac > hiFrac { errs.Add(Warningf("%w: %f > %f", WarningBackwardsRange, loFrac, hiFrac)) loFrac, hiFrac = hiFrac, loFrac } if loFrac < 0.0 { errs.Add(Warningf("%w: loFrac was %f", WarningTopClamped, loFrac)) loFrac = 0.0 } if loFrac > 1.0 { errs.Add(Warningf("%w: loFrac was %f", WarningBottomClamped, loFrac)) loFrac = 1.0 } if hiFrac < 0.0 { errs.Add(Warningf("%w: hiFrac was %f", WarningTopClamped, hiFrac)) hiFrac = 0.0 } if hiFrac > 1.0 { errs.Add(Warningf("%w: hiFrac was %f", WarningBottomClamped, hiFrac)) hiFrac = 1.0 } // Pick a random spot in this range and translate it to a card slot. scale := hiFrac - loFrac spotFrac := d.rand.Float64()*scale + loFrac slot := int(spotFrac * float64(d.Len()+1)) if slot > d.Len() { // This can't happen. rand.Float64 cannot return 1.0, so len+1 itself // should not be reachable. slot = d.Len() errs.Add(Warningf("%w: InsertRandomRange(%f, %f, ...) chose slot end-plus-one: %d (limit %d). fraction %f", WarningRecoverableBug, loFrac, hiFrac, slot, d.Len(), spotFrac)) } // okay, after all that, we have a slot! errs.Add(d.Insert(slot, card)) return errs.Emit() } // Shuffle completely shuffles the deck. If the deck has one or fewer cards, // this returns WarningTooFewCards since nothing can be shuffled. func (d *Deck[C]) Shuffle() error { if len(d.cards) < 2 { return WarningTooFewCards } ShuffleAll(d.cards, d.rand) return nil } // ShufflePart shuffles the `n` cards of the deck starting at `loc`. // If the provided range doesn't fit in the deck, this returns // WarningTopClamped and/or WarningBottomClamped. If the eventual range // of cards to be shuffled (after any off-the-end issues are corrected) // is one or less, this returns WarningTooFewCards since nothing can // be shuffled. func (d *Deck[C]) ShufflePart(loc, n int) error { if n < 2 { // Nothing to do. return WarningTooFewCards } var errs ErrorCollector if loc < 0 { errs.Add(Warningf("%w: loc was %d", WarningTopClamped, loc)) loc = 0 } if loc+n > d.Len() { errs.Add(Warningf("%w: deck size %d does not have %d cards at and after location %d", WarningBottomClamped, len(d.cards), n, loc)) n = d.Len() - loc // Now is there anything to do? if n < 2 { errs.Add(WarningTooFewCards) return errs.Emit() } } ShufflePart(d.cards, d.rand, loc, n) return nil } // ShuffleRange shuffles the cards between the specified fractions of // the deck; the top of the deck is 0.0 and the bottom of the deck is // 1.0. This rounds "outward" -- "partial" cards at each end are counted. // This can return the same warnings ShufflePart can in the same circumstances // and may also complain about a backwards range. func (d *Deck[C]) ShuffleRange(loFrac, hiFrac float64) error { var errs ErrorCollector if loFrac > hiFrac { errs.Add(Warningf("%w: %f > %f", WarningBackwardsRange, loFrac, hiFrac)) loFrac, hiFrac = hiFrac, loFrac } low := int(math.Floor(loFrac * float64(d.Len()))) high := int(math.Ceil(hiFrac * float64(d.Len()))) n := 1 + high - low errs.Add(d.ShufflePart(low, n)) return errs.Emit() } // ShuffleTop uses ShuffleRange to shuffle the top frac (between 0.0 and 1.0) // of the deck. See ShuffleRange and ShufflePart for information on // rounding and warnings. func (d *Deck[C]) ShuffleTop(frac float64) error { return d.ShuffleRange(0.0, frac) } // ShuffleBottom uses ShuffleRange to shuffle the bottom frac (between 0.0 and // 1.0) of the deck. See ShuffleRange and ShufflePart for information on // rounding and warnings. func (d *Deck[C]) ShuffleBottom(frac float64) error { return d.ShuffleRange(frac, 1.0) } // Strip removes all cards from the deck where shouldRemove returns true. // shouldRemove is provided with each card in the deck and its index. // It returns how many cards were stripped from the deck. func (d *Deck[C]) Strip(shouldRemove func(idx int, c Card[C]) bool) int { stripped := 0 startStripRange := -1 for i := 0; i < len(d.cards); i++ { if shouldRemove(i, d.cards[i]) { stripped++ if startStripRange < 0 { startStripRange = i } } else if startStripRange >= 0 { d.cards = DeleteNFrom(d.cards, startStripRange, i-startStripRange) i = startStripRange startStripRange = -1 } } if startStripRange >= 0 { d.cards = d.cards[:startStripRange] } return stripped }