206 lines
7.4 KiB
Go
206 lines
7.4 KiB
Go
|
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()
|
||
|
}
|