CardSimEngine/cardsim/deck.go

282 lines
10 KiB
Go
Raw Normal View History

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