CardSimEngine/cardsim/deck.go

206 lines
7.4 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 a card at a specific location in the Deck. The card previously
// at that location and all locations after are shifted one card later.
// 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.
// Place new card on bottom and "bubble" into position.
// Takes O(N) time. If this turns out to be a problem, implement a more
// efficient data structure.
d.cards = append(d.cards, card)
for i := len(d.cards) - 1; i > idx; i-- {
d.cards[i], d.cards[i-1] = d.cards[i-1], d.cards[i]
}
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 %d", WarningTopClamped, loFrac))
loFrac = 0.0
}
if loFrac > 1.0 {
errs.Add(Warningf("%w: loFrac was %d", WarningBottomClamped, loFrac))
loFrac = 1.0
}
if hiFrac < 0.0 {
errs.Add(Warningf("%w: hiFrac was %d", WarningTopClamped, hiFrac))
hiFrac = 0.0
}
if hiFrac > 1.0 {
errs.Add(Warningf("%w: hiFrac was %d", 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()
}