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 { origLen := d.Len() d.cards = Strip(d.cards, shouldRemove) return origLen - d.Len() } // DeckDebugger is a Card[C], intended for use only as a debug action, that // lists the top 10 cards of the deck (without checking if they are drawable) // and allows various sorts of deck manipulation for free. It can't be drawn. type DeckDebugger[C StatsCollection] struct{} // Title implements Card[C]. func (DeckDebugger[C]) Title(p *Player[C]) Message { return MsgStr("Debug Mode Deck Controls") } // Urgent implements Card[C] as used in permanent actions. It's always valid // to use the deck debugger. Debug actions do not check urgency flags, but this // marks itself as urgent-compatible just in case. func (DeckDebugger[C]) Urgent(p *Player[C]) bool { return true } // Drawn implements Card[C]. It can't be drawn. func (DeckDebugger[C]) Drawn(p *Player[C]) bool { return false } // EventText implements Card[C]. It lists the top ten cards of the deck and // a few deck-related and hand-related stats. func (DeckDebugger[C]) EventText(p *Player[C]) (Message, error) { var msgs []Message msgs = append(msgs, Msgf("The Deck contains %d cards.", p.Deck.Len())) if p.Deck.Len() > 0 { portion := p.Deck.cards msgs = append(msgs, nil) topness := "All" if p.Deck.Len() > 10 { portion = p.Deck.cards[:10] topness = "Top 10" } msgs = append(msgs, Msgf("%s cards in the Deck:", topness)) for i, c := range portion { urgency := " " if c.Urgent(p) { urgency = "!" } msgs = append(msgs, Msgf(" %s %2d) %v", urgency, i+1, c.Title(p))) } } msgs = append(msgs, nil) msgs = append(msgs, Msgf("At the start of each turn, the Player draws to %d cards. The player has %d cards in hand.", p.HandLimit, len(p.Hand))) return MultiMessage(msgs), nil } // Options implements Card[C]. It offers many possible actions. func (DeckDebugger[C]) Options(p *Player[C]) ([]CardOption[C], error) { ret := []CardOption[C]{ &BasicOption[C]{ Text: MsgStr("Draw a card."), Effect: func(p *Player[C]) error { return p.Draw() }, Output: MsgStr("Done."), }, &BasicOption[C]{ Text: MsgStr("Shuffle the deck."), Effect: func(p *Player[C]) error { return p.Deck.Shuffle() }, Output: MsgStr("Done."), }, &BasicOption[C]{ Text: MsgStr("Shuffle top half."), Effect: func(p *Player[C]) error { return p.Deck.ShuffleTop(0.5) }, Output: MsgStr("Done."), }, &BasicOption[C]{ Text: MsgStr("Shuffle bottom half."), Effect: func(p *Player[C]) error { return p.Deck.ShuffleBottom(0.5) }, Output: MsgStr("Done."), }, } for _, n := range []int{1, 3, 5, 10} { if p.Deck.Len() <= n { break } // We don't want the functions we're creating to all share the same "n" // field -- we want to create distinct functions that move distinct // numbers of cards. For more information on what's going on here, see // https://eli.thegreenplace.net/2019/go-internals-capturing-loop-variables-in-closures/ // // For the curious, the Go developers don't like the gotcha that // this "shadow variable with itself" workaround patches over either. // Here's the guy who implemented apologizing for it and discussing // changing it: https://github.com/golang/go/discussions/56010 // // "Loop variables being per-loop instead of per-iteration is the only // design decision I know of in Go that makes programs incorrect more // often than it makes them correct." -- Russ Cox (rsc) n := n invN := p.Deck.Len() - n ret = append(ret, &BasicOption[C]{ Text: Msgf("Move the top %d card(s) to the bottom of the deck, in order.", n), Effect: func(p *Player[C]) error { p.Deck.cards = append(p.Deck.cards, p.Deck.cards[:n]...) p.Deck.cards = p.Deck.cards[n:] return nil }, Output: MsgStr("Done."), }, &BasicOption[C]{ Text: Msgf("Move the bottom %d card(s) to the top of the deck, in order.", n), Effect: func(p *Player[C]) error { p.Deck.cards = append(p.Deck.cards, p.Deck.cards[:invN]...) p.Deck.cards = p.Deck.cards[invN:] return nil }, Output: MsgStr("Done."), }, ) if n > 1 { ret = append(ret, &BasicOption[C]{ Text: Msgf("Shuffle the top %d card(s) of the deck.", n), Effect: func(p *Player[C]) error { return p.Deck.ShufflePart(0, n) }, Output: MsgStr("Done."), }) } } return ret, nil } // Then implements Card[C]. It refunds the action point. func (DeckDebugger[C]) Then(p *Player[C], o CardOption[C]) error { p.ActionsRemaining++ return nil }