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() }