2023-04-01 18:50:39 +00:00
|
|
|
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]
|
2023-04-02 02:13:42 +00:00
|
|
|
rand *rand.Rand
|
2023-04-01 18:50:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Len returns the number of cards in the Deck.
|
|
|
|
func (d *Deck[C]) Len() int {
|
|
|
|
return len(d.cards)
|
|
|
|
}
|
|
|
|
|
2023-04-02 01:49:06 +00:00
|
|
|
// 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.
|
2023-04-01 18:50:39 +00:00
|
|
|
// 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.
|
2023-04-02 01:49:06 +00:00
|
|
|
func (d *Deck[C]) Insert(idx int, card ...Card[C]) error {
|
2023-04-01 18:50:39 +00:00
|
|
|
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.
|
2023-04-02 01:49:06 +00:00
|
|
|
d.cards = InsertInto(d.cards, idx, card...)
|
2023-04-01 18:50:39 +00:00
|
|
|
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 {
|
2023-04-01 21:39:56 +00:00
|
|
|
errs.Add(Warningf("%w: loFrac was %f", WarningTopClamped, loFrac))
|
2023-04-01 18:50:39 +00:00
|
|
|
loFrac = 0.0
|
|
|
|
}
|
|
|
|
if loFrac > 1.0 {
|
2023-04-01 21:39:56 +00:00
|
|
|
errs.Add(Warningf("%w: loFrac was %f", WarningBottomClamped, loFrac))
|
2023-04-01 18:50:39 +00:00
|
|
|
loFrac = 1.0
|
|
|
|
}
|
|
|
|
if hiFrac < 0.0 {
|
2023-04-01 21:39:56 +00:00
|
|
|
errs.Add(Warningf("%w: hiFrac was %f", WarningTopClamped, hiFrac))
|
2023-04-01 18:50:39 +00:00
|
|
|
hiFrac = 0.0
|
|
|
|
}
|
|
|
|
if hiFrac > 1.0 {
|
2023-04-01 21:39:56 +00:00
|
|
|
errs.Add(Warningf("%w: hiFrac was %f", WarningBottomClamped, hiFrac))
|
2023-04-01 18:50:39 +00:00
|
|
|
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()
|
|
|
|
}
|
2023-04-02 02:13:42 +00:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
2023-04-02 02:23:14 +00:00
|
|
|
|
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
|
2023-04-02 02:31:50 +00:00
|
|
|
// ShuffleBottom uses ShuffleRange to shuffle the bottom frac (between 0.0 and
|
2023-04-02 02:23:14 +00:00
|
|
|
// 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)
|
|
|
|
}
|
2023-04-02 02:31:50 +00:00
|
|
|
|
|
|
|
// 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 {
|
2023-04-02 02:40:28 +00:00
|
|
|
origLen := d.Len()
|
|
|
|
d.cards = Strip(d.cards, shouldRemove)
|
|
|
|
return origLen - d.Len()
|
2023-04-02 02:31:50 +00:00
|
|
|
}
|
2023-04-16 02:16:08 +00:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|