Compare commits

..

5 Commits

Author SHA1 Message Date
b806264154
Fraction-specified shufflers.
Convenience methods for "shuffle the bottom third of the deck" and stuff like that.
2023-04-01 19:23:14 -07:00
b8c0e5603a
Add a general shuffler and deck shuffling. 2023-04-01 19:13:42 -07:00
20561c574c
sliceutil: helpers for mid-slice insert/delete.
Manipulating the hand, deck, etc. is going to use these operations a lot.
2023-04-01 18:49:06 -07:00
99e9e35b1d
Card.Drawn, to give cards a chance to not show up
A card can be shuffled into the deck because of a certain condition, and then that condition could cease to apply. If the card should not be presented to the player, it gets one last chance to hide.

There is currently no direct mechanism for making a card _already in the hand_ disappear if it is not relevant, although there are various ways to implement a Rule to do this.
2023-04-01 18:05:57 -07:00
fd35090b34
Generics tutorial on Rule 2023-04-01 17:56:31 -07:00
4 changed files with 245 additions and 14 deletions

View File

@ -10,9 +10,19 @@ type Card[C StatsCollection] interface {
Title(p *Player[C]) (Message, error) Title(p *Player[C]) (Message, error)
// Urgent reports whether the card is considered urgent. If // Urgent reports whether the card is considered urgent. If
// the player hasa any // the player has any urgent cards in hand, they cannot choose to act
// on a non-urgent card.
Urgent(p *Player[C]) bool Urgent(p *Player[C]) bool
// Drawn is invoked after a card is drawn, before presenting it to the
// player. If Drawn returns `false`, the card is discarded without being
// put into the hand or shown to the player and a replacement is drawn
// instead. To put a card back on the bottom of the deck (or similar)
// use p.Deck.Insert (or a related function) to put it back explicitly
// in the right position. Do not put it right back on top of the deck or
// you'll create an infinite loop.
Drawn(p *Player[C]) bool
// EventText returns the text to display on the card. If it returns an // EventText returns the text to display on the card. If it returns an
// error that is not a warning, the game crashes. // error that is not a warning, the game crashes.
EventText(p *Player[C]) (Message, error) EventText(p *Player[C]) (Message, error)
@ -44,7 +54,8 @@ type CardOption[C StatsCollection] interface {
Enact(p *Player[C]) (Message, error) Enact(p *Player[C]) (Message, error)
} }
// A BasicCard is a Card with fixed title, text, options, and optional post-option callback. // A BasicCard is a Card with fixed title, text, options, and optional
// post-option callback. It never does anything in particular when drawn.
type BasicCard[C StatsCollection] struct { type BasicCard[C StatsCollection] struct {
CardTitle Message CardTitle Message
IsUrgent bool IsUrgent bool
@ -76,6 +87,10 @@ func (b *BasicCard[C]) Then(p *Player[C], option CardOption[C]) error {
return b.AfterOption(p, option) return b.AfterOption(p, option)
} }
func (b *BasicCard[C]) Drawn(p *Player[C]) bool {
return true
}
// A BasicOption is a CardOption with fixed text, effects, and output. // A BasicOption is a CardOption with fixed text, effects, and output.
type BasicOption[C StatsCollection] struct { type BasicOption[C StatsCollection] struct {
Text Message Text Message

View File

@ -23,7 +23,7 @@ const (
// The Deck stores cards yet-to-be-dealt. // The Deck stores cards yet-to-be-dealt.
type Deck[C StatsCollection] struct { type Deck[C StatsCollection] struct {
cards []Card[C] cards []Card[C]
rand rand.Rand rand *rand.Rand
} }
// Len returns the number of cards in the Deck. // Len returns the number of cards in the Deck.
@ -31,8 +31,8 @@ func (d *Deck[C]) Len() int {
return len(d.cards) return len(d.cards)
} }
// Insert puts a card at a specific location in the Deck. The card previously // Insert puts one or more cards at a specific location in the Deck. Cards
// at that location and all locations after are shifted one card later. // 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 // Negative indexes are counted from the bottom of the deck. BottomOfDeck is
// a sentinel value for the bottommost position; -1 is one card above. // a sentinel value for the bottommost position; -1 is one card above.
// //
@ -44,7 +44,7 @@ func (d *Deck[C]) Len() int {
// WarningTopClamped. Like all warnings, these can be safely ignored and the // 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 // program is in a well-defined state, but you may want to check for them
// if you expect some other behavior. // if you expect some other behavior.
func (d *Deck[C]) Insert(idx int, card Card[C]) error { func (d *Deck[C]) Insert(idx int, card ...Card[C]) error {
var errs ErrorCollector var errs ErrorCollector
// Calculate actual target index. // Calculate actual target index.
switch { switch {
@ -60,14 +60,7 @@ func (d *Deck[C]) Insert(idx int, card Card[C]) error {
idx += d.Len() idx += d.Len()
} }
// remaining case: 0 <= idx <= d.Len(), which is a normal forward insert index. // remaining case: 0 <= idx <= d.Len(), which is a normal forward insert index.
d.cards = InsertInto(d.cards, idx, card...)
// 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() return errs.Emit()
} }
@ -203,3 +196,77 @@ func (d *Deck[C]) InsertRandomRange(loFrac, hiFrac float64, card Card[C]) error
errs.Add(d.Insert(slot, card)) errs.Add(d.Insert(slot, card))
return errs.Emit() 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)
}

View File

@ -7,6 +7,27 @@ import (
) )
// A Rule implements an operation run on every game turn. // A Rule implements an operation run on every game turn.
//
// Rule[C] is a generic interface. Like any other generic type, it describes a
// family of related types: each different kind of StatsCollection that Rule
// could pertain to is the basis of a distinct type of Rule.
//
// When implementing a generic interface, you do not need to implement a
// generic type. In the case of Rule, you are likely to be writing rules for a
// specific simulation. That simulation will have some associated
// StatsCollection type. The rules you write will only need to implement the
// variation of Rule that pertains specifically to that type.
//
// For example, if your `StatsCollection` type is `KoboldMineData`, then rules
// for the simulation referring to it would implement `Rule[KoboldMineData]`
// only. So the `Enact` function you implment would take an argument of type
// `*Player[KoboldMineData]`, not some undefined type `C` that could be any
// StatsCollection. Since it takes a `*Player[KoboldMineData]` as an argument,
// you then know that the player's `Stats` field is not just any
// StatsCollection, it is KoboldMineData specifically. The compiler won't
// require you to convert from "some `StatsCollection`" to "`KoboldMineData`
// specifically" when using the `Player[KoboldMineData].Stats` field,
// because the type of that field is already `KoboldMineData`.
type Rule[C StatsCollection] interface { type Rule[C StatsCollection] interface {
// Label is an internal name the rule can be recognized by. // Label is an internal name the rule can be recognized by.
// Some things may be easier if it is unique, but it does not have to be. // Some things may be easier if it is unique, but it does not have to be.

128
cardsim/sliceutil.go Normal file
View File

@ -0,0 +1,128 @@
package cardsim
import (
"fmt"
"math/rand"
)
// InsertInto inserts one or more items into a slice at an arbitrary index.
// Items already in the slice past the target position move to later positons.
//
// Like `append`, this may move the underlying array and it produces a new
// slice header (under the hood, it uses `append`). It returns the new slice
// (the original is in an undefined state and should no longer be used).
//
// If loc is negative or more than one past the end of T, Insert panics.
func InsertInto[T any](slice []T, loc int, elements ...T) []T {
if loc < 0 || loc > len(slice) {
panic(fmt.Sprintf("can't Insert at location %d in %d-element slice", loc, len(slice)))
}
// is this a no-op?
if len(elements) == 0 {
return slice
}
// is this just an append?
if loc == len(slice) {
return append(slice, elements...)
}
offset := len(elements)
oldLen := len(slice)
newSize := oldLen + offset
if newSize <= cap(slice) {
// We can reslice in place.
slice = slice[:newSize]
// Scoot trailing to their new positions.
copy(slice[loc+offset:], slice[loc:oldLen])
// Insert the new elements.
copy(slice[loc:], elements)
return slice
}
// Reallocate. Do the normal thing of doubling the size as a minimum
// when increasing space for a dynamic array; this amortizes the
// cost of repeatedly reallocating and moving the slice.
newCap := cap(slice) * 2
if newCap < newSize {
newCap = newSize
}
newSlice := make([]T, newSize, newCap)
if loc > 0 {
copy(newSlice, slice[0:loc])
}
copy(newSlice[loc:], elements)
copy(newSlice[loc+offset:], slice[loc:])
return newSlice
}
// DeleteFrom deletes an item from a slice at an arbitrary index. Items after it
// scoot up to close the gap. This returns the modified slice (like Append).
//
// If the provided location is not a valid location in the slice, this panics.
func DeleteFrom[T any](slice []T, loc int) []T {
return DeleteNFrom(slice, loc, 1)
}
// DeleteNFrom deletes N items from a slice at an arbitrary index. Items after
// it scoot up to close the gap. This returns the modified slice (like Append).
//
// If the range of items that would be deleted is not entirely valid within the
// slice, this panics.
func DeleteNFrom[T any](slice []T, loc, n int) []T {
if loc < 0 || loc+n > len(slice) {
panic(fmt.Sprintf("can't delete %d elements from a %d-element slice at location %d", n, len(slice), loc))
}
// Easy cases.
if n == 0 {
return slice
}
if loc == 0 {
return slice[n:]
}
if loc+n == len(slice) {
return slice[0:loc]
}
// Is it shorter to move up or move down?
if len(slice)-loc-n > loc {
// Move forward -- the end is big.
copy(slice[n:], slice[:loc])
return slice[n:]
}
// Move backward -- the beginnng is big or they're the same size
// (and moving backwards preserves more usable append capacity later).
copy(slice[loc:], slice[loc+n:])
return slice[:len(slice)-n]
}
// ShuffleAll shuffles everything in slice, using the provided rand.Rand.
// If no rand.Rand is provided, this uses the default source.
func ShuffleAll[T any](slice []T, r *rand.Rand) {
shuffle := rand.Shuffle
if r != nil {
shuffle = r.Shuffle
}
shuffle(len(slice), func(i, j int) {
slice[i], slice[j] = slice[j], slice[i]
})
}
// ShufflePart shuffles the `n` elements of `slice` starting at `loc`
// in-place, using the provided rand.Rand. If the range of items to
// shuffle is not entirely within `slice`, this panics.
//
// If no rand.Rand is provided, this uses the default source.
func ShufflePart[T any](slice []T, r *rand.Rand, loc, n int) {
if loc < 0 || loc+n > len(slice) {
panic(fmt.Sprintf("can't shuffle %d elements from a %d-element slice at location %d", n, len(slice), loc))
}
if n < 1 {
return
}
ShuffleAll(slice[loc:loc+n], r)
}