cardsim

package
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 16, 2023 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Package cardsim is a general engine for NationStates-like simulations.

Index

Constants

View Source
const (
	HideWarnings   = -1
	NotDebugging   = 0
	DebugWarning   = 1
	DebugInfo      = 2
	DebugDetail    = 3
	DebugFine      = 4
	DebugSuperfine = 5
)

Named debug verbosity levels. Using the raw constants is fine too. This is roughly consistent with "standard" meanings for these debug levels.

View Source
const (
	// The game has not started.
	GameUninitialized = GameState(iota)

	// The game is ready to play.
	GameActive

	// The game is over and the player has lost.
	GameLost

	// The game is over and the player has won.
	GameWon

	// The game is over because of an error.
	GameCrashed

	// The game is over because the player cannot take any actions.
	GameStalled
)
View Source
const (
	// InvalidEnactable is an uninitialized EnactableType value with no meaning.
	// Using it is generally an error. If you initialize EnactableType fields
	// with this value when your program has not yet calculated what type of
	// enactable will be used, CardSimEngine will be able to detect bugs where
	// such a calcualation, inadvertently, does not come to any conclusion.
	// Unlike NothingEnactable, there are no circumstances where this has a
	// specific valid meaning.
	InvalidEnactable = EnactableType(iota)

	// NothingEnactable specifically represents not enacting anything. In some
	// contexts, it's an error to use it; in others, it is a sentinel value
	// for "do not enact anything". Unlike InvalidEnactable, this has a specific
	// valid meaning, it's just that the meaning is specifically "nothing".
	NothingEnactable

	// CardEnactable refers to a card in the hand.
	CardEnactable

	// PermanentActionEnactable refers to an item in the permanent actions list.
	PermanentActionEnactable

	// DebugActionEnactable refers to an item in the debug actions list.
	DebugActionEnactable
)
View Source
const (
	BottomOfDeck = math.MaxInt
)

Variables

View Source
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")
)
View Source
var (
	SectionBreak = &SpecialMessage{MsgStr("      --------------------------------------------------------------------      ")}
	ChapterBreak = &SpecialMessage{MsgStr("      ====================================================================      ")}
)

Messages that various display surfaces or other components may have a special interpretation of and identify specifically. These are largely sentinel values. Nil is a paragraph break.

View Source
var (
	ErrInvalidCard   = errors.New("invalid card specified")
	ErrInvalidChoice = errors.New("invalid choice specified")
	ErrNotUrgent     = errors.New("action not urgent when urgent card is available")
	ErrNoActions     = errors.New("no actions remaining")
	ErrNotDebugging  = errors.New("this is a debug-only feature and you're not in debug mode")

	WarningStalemate         = &Warning{errors.New("no actions can be taken")}
	WarningUncoperativeCards = &Warning{errors.New("a milion cards refused to join the hand")}
)
View Source
var (
	ErrHaltTurn   = errors.New("turn halted")
	ErrDeleteRule = errors.New("delete this rule")
	ErrHaltDelete = fmt.Errorf("%w; %w", ErrHaltTurn, ErrDeleteRule)

	// ErrNotUnique matches errors from functions attempting to identify
	// a single rule by an identifier that is not unique.
	ErrNotUnique = errors.New("not unique")

	// ErrAlreadyRunning means you tried to run the rules engine from inside a rule.
	ErrAlreadyRunning = errors.New("cannot run a turn while running a turn")
)
View Source
var AnyWarning = &Warning{
	E: errors.New("unspecified warning"),
}

AnyWarning is a special Warning, not intended to be returned from anything, which every Warning recognizes that they "are" -- errors.Is(w, AnyWarning) returns true when w is a Warning.

View Source
var (
	WarningRecoverableBug = Warningf("cardsim library has a bug, but can keep going")
)

Generic warnings that may be used in multiple spots in the library without any particular comment that the function may return these.

Functions

func DeleteFrom

func DeleteFrom[T any](slice []T, loc int) []T

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 DeleteNFrom

func DeleteNFrom[T any](slice []T, loc, n int) []T

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 EnsureCapacity added in v0.4.0

func EnsureCapacity[T any](slice []T, req int) []T

EnsureCapacity checks if `cap(slice)` is at least req. If so, it returns slice unchanged. Otherwise, it copies `slice` to a new slice that is at least capacity `req` (but may be larger) and returns the copy.

It is reasonably efficient to use EnsureCapacity consecutively without regard for the final overall capacity that a specific slice will need to be.

func InsertInto

func InsertInto[T any](slice []T, loc int, elements ...T) []T

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 IsSeriousError

func IsSeriousError(e error) bool

IsSeriousError returns whether e is a non-warning error. If e is nil, this returns false.

func IsSpecialMessage

func IsSpecialMessage(m Message, s *SpecialMessage) bool

IsSpecialMessage returns whether a provided Message is a specific SpecialMessage.

func RefundAction added in v0.4.0

func RefundAction[C StatsCollection]() func(c Card[C], p *Player[C], option CardOption[C]) error

RefundAction returns a func that can be used as an AfterOption, which returns the player's action point.

func RunSimpleTerminalUI

func RunSimpleTerminalUI[C StatsCollection](p *Player[C]) error

func ShuffleAll

func ShuffleAll[T any](slice []T, r *rand.Rand)

ShuffleAll shuffles everything in slice, using the provided rand.Rand. If no rand.Rand is provided, this uses the default source.

func ShufflePart

func ShufflePart[T any](slice []T, r *rand.Rand, loc, n int)

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 SortStats

func SortStats(ss []Stat)

SortStats sorts the provided slice of stats in place. It puts all visible stats before all invisible stats, then alphabetizes (case-insensitive).

func Strip

func Strip[T any](slice []T, removeWhen func(idx int, t T) bool) []T

Strip iterates T, removing any element for which removeWhen returns true (when provided the index of the element and the element itself as arguments). It returns the stripped slice.

func VisibleOrDebug

func VisibleOrDebug[C StatsCollection](p *Player[C], s Stat) bool

VisibleOrDebug returns whether s is Visible or p is in debug mode, so a debug-mode player shows all stats.

Types

type BasicCard

type BasicCard[C StatsCollection] struct {
	CardTitle   Message
	IsUrgent    bool
	CardText    Message
	CardOptions []CardOption[C]
	// AfterOption is given the card itself as its first argument.
	AfterOption func(c Card[C], p *Player[C], option CardOption[C]) error
}

A BasicCard is a Card with fixed title, text, options, and optional post-option callback. It never does anything in particular when drawn.

func (*BasicCard[C]) Drawn

func (b *BasicCard[C]) Drawn(_ *Player[C]) bool

Drawn implements Card.

func (*BasicCard[C]) EventText

func (b *BasicCard[C]) EventText(_ *Player[C]) (Message, error)

EventText implements Card.

func (*BasicCard[C]) Options

func (b *BasicCard[C]) Options(_ *Player[C]) ([]CardOption[C], error)

Options implements Card.

func (*BasicCard[C]) Then

func (b *BasicCard[C]) Then(p *Player[C], option CardOption[C]) error

Then implements Card.

func (*BasicCard[C]) Title

func (b *BasicCard[C]) Title(_ *Player[C]) Message

Title implements Card.

func (*BasicCard[C]) Urgent

func (b *BasicCard[C]) Urgent(_ *Player[C]) bool

Urgent implements Card.

type BasicOption

type BasicOption[C StatsCollection] struct {
	Text   Message
	Effect func(*Player[C]) error
	Output Message
}

A BasicOption is a CardOption with fixed text, effects, and output. It's always enabled.

func (*BasicOption[C]) Enabled

func (b *BasicOption[C]) Enabled(p *Player[C]) bool

Enabled implements CardOption.

func (*BasicOption[C]) Enact

func (b *BasicOption[C]) Enact(p *Player[C]) (Message, error)

Enact implements CardOption.

func (*BasicOption[C]) OptionText

func (b *BasicOption[C]) OptionText(p *Player[C]) (Message, error)

OptionText implements CardOption.

type BasicStatsPanel

type BasicStatsPanel[C StatsCollection] struct {
	// Name stores the name of this stats panel, which is also shown in the menu.
	Name Message

	// Intro stores a message to always display before the stats. Optional.
	Intro Message

	// Filter stores a function to decide what stats to show. If this is not
	// provided, the BasicStatsPanel uses VisibleOrDebug by default.
	Filter StatFilter[C]
}

BasicStatsPanel shows some or all of the stats output by C, under a fixed name, introduced by a specific prompt. Stats are shown as a two column table with the name, then the value.

func (*BasicStatsPanel[C]) Info

func (b *BasicStatsPanel[C]) Info(p *Player[C]) ([]Message, error)

Info implements `InfoPanel[C]` by dumpiing p.Stats, showing those items for whch b.Filter returns true.

func (*BasicStatsPanel[C]) Title

func (b *BasicStatsPanel[C]) Title(p *Player[C]) Message

Title implements `InfoPanel[C]` by returning b's `Name`.

type Card

type Card[C StatsCollection] interface {
	// Title is the short name of the card displayed in the hand
	// and at the top of the card output. It receives the current
	// player as an argument.
	Title(p *Player[C]) Message

	// Urgent reports whether the card is considered urgent. If
	// the player has any urgent cards in hand, they cannot choose to act
	// on a non-urgent card.
	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
	// error that is not a warning, the game crashes.
	EventText(p *Player[C]) (Message, error)

	// Options returns the possible actions the player can take for this card.
	// There must be at least one option.
	Options(p *Player[C]) ([]CardOption[C], error)

	// Then is invoked after an option is selected and executed. The selected
	// option is provided as an argument. This allows cards to do certain
	// cleanup for every action -- for example, returning to the deck.
	Then(p *Player[C], option CardOption[C]) error
}

A Card represents an option available to the player. Its methods may be called many times per turn as the player considers their options.

func ActionCounterDebugCard added in v0.4.0

func ActionCounterDebugCard[C StatsCollection]() Card[C]

ActionCounterDebugCard constructs a BasicCard intended for use only as a Debug Action that tinkers with the player's action counter.

func DebugModeCard added in v0.4.0

func DebugModeCard[C StatsCollection]() Card[C]

DebugModeCard constructs a BasicCard to change the player's debug level. It is intended for use only as a Debug Action.

type CardOption

type CardOption[C StatsCollection] interface {
	// OptionText returns the text displayed for this option.  It may be called
	// many times within a turn as the player considers their options. If it
	// returns an error that is not a warning, the game crashes.
	OptionText(p *Player[C]) (Message, error)

	// Enact is called exactly once if the player chooses the option. It is
	// expected to update values in `p`. It returns the text displayed to the
	// player as a result of their action. If it returns an error that is not
	// a warning, the game crashes.
	//
	// After an option is enacted, the card is deleted. If a card should be
	// repeatable, Enact must return it to the deck (on every option) or
	// the card needs to reinsert itself with its Then function.
	Enact(p *Player[C]) (Message, error)

	// Enabled returns whether this option can curently be enacted.
	Enabled(p *Player[C]) bool
}

A CardOption represents a choice a player could make for some card.

func OnlyDiscardFree added in v0.4.0

func OnlyDiscardFree[C StatsCollection](msg Message) []CardOption[C]

OnlyDiscardFree returns a []CardOption[C] providing a single option, which returns the action point. It does not shuffle the card back into the deck or draw a replacement (consider the AfterFunc for that if needed). This can be used for cards that are displayable but not actionable, but show up as cards rather than permanent or debug actions for some reason.

func OptionFunc

func OptionFunc[C StatsCollection](text Message, f func(*Player[C]) (Message, error)) CardOption[C]

OptionFunc attaches a fixed prompt to an Enact-like function. Unlike BasicOption, the enactment function provided to OptionFunc returns the text that should be output as a result of the action, so it is possible to dynamically generate this text.

type Deck

type Deck[C StatsCollection] struct {
	// contains filtered or unexported fields
}

The Deck stores cards yet-to-be-dealt.

func (*Deck[C]) Draw

func (d *Deck[C]) Draw() Card[C]

Draw pops the 0th card off the deck and returns it. If the deck has no cards, it returns nil.

func (*Deck[C]) DrawN

func (d *Deck[C]) DrawN(n int) ([]Card[C], error)

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 (*Deck[C]) Insert

func (d *Deck[C]) Insert(idx int, card ...Card[C]) error

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 (*Deck[C]) InsertRandom

func (d *Deck[C]) InsertRandom(card Card[C]) error

InsertRandom puts a card randomly in the deck, with equal probability for any of the Len()+1 possible locations.

func (*Deck[C]) InsertRandomBottom

func (d *Deck[C]) InsertRandomBottom(frac float64, card Card[C]) error

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 (*Deck[C]) InsertRandomRange

func (d *Deck[C]) InsertRandomRange(loFrac, hiFrac float64, card Card[C]) error

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 (*Deck[C]) InsertRandomTop

func (d *Deck[C]) InsertRandomTop(frac float64, card Card[C]) error

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 (*Deck[C]) Len

func (d *Deck[C]) Len() int

Len returns the number of cards in the Deck.

func (*Deck[C]) Shuffle

func (d *Deck[C]) Shuffle() error

Shuffle completely shuffles the deck. If the deck has one or fewer cards, this returns WarningTooFewCards since nothing can be shuffled.

func (*Deck[C]) ShuffleBottom

func (d *Deck[C]) ShuffleBottom(frac float64) error

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 (*Deck[C]) ShufflePart

func (d *Deck[C]) ShufflePart(loc, n int) error

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 (*Deck[C]) ShuffleRange

func (d *Deck[C]) ShuffleRange(loFrac, hiFrac float64) error

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 (*Deck[C]) ShuffleTop

func (d *Deck[C]) ShuffleTop(frac float64) error

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 (*Deck[C]) Strip

func (d *Deck[C]) Strip(shouldRemove func(idx int, c Card[C]) bool) int

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.

type DeckDebugger added in v0.4.0

type DeckDebugger[C StatsCollection] struct{}

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.

func (DeckDebugger[C]) Drawn added in v0.4.0

func (DeckDebugger[C]) Drawn(p *Player[C]) bool

Drawn implements Card[C]. It can't be drawn.

func (DeckDebugger[C]) EventText added in v0.4.0

func (DeckDebugger[C]) EventText(p *Player[C]) (Message, error)

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]) Options added in v0.4.0

func (DeckDebugger[C]) Options(p *Player[C]) ([]CardOption[C], error)

Options implements Card[C]. It offers many possible actions.

func (DeckDebugger[C]) Then added in v0.4.0

func (DeckDebugger[C]) Then(p *Player[C], o CardOption[C]) error

Then implements Card[C]. It refunds the action point.

func (DeckDebugger[C]) Title added in v0.4.0

func (DeckDebugger[C]) Title(p *Player[C]) Message

Title implements Card[C].

func (DeckDebugger[C]) Urgent added in v0.4.0

func (DeckDebugger[C]) Urgent(p *Player[C]) bool

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.

type EnactableType added in v0.4.0

type EnactableType int

EnactableType is an enumeration representing a category of enactable thing. Debug actions, permanent actions, and cards behave equivalently in many ways, so EnactableType allows parts of the program to work with any of these and represent which one they apply to.

type ErrorCollector

type ErrorCollector struct {
	Errs []error
	// contains filtered or unexported fields
}

ErrorCollector accumulates errors as an operation progresses. The zero ErrorCollector is its correct starting value. When it emits a final error:

  • If it contains exactly zero errors, it returns nil.
  • If it contains exactly one error, it returns it.
  • Otherwise, it returns an error that combines all the errors it collected. The aggregated error is a Warning if and only if all collected errors were also warnings.

An ErrorCollector's Errs should not be written by anything other than Add, or HasFailure may get out of sync.

func (*ErrorCollector) Add

func (ec *ErrorCollector) Add(e error)

Add inserts e into the error collection. If e is nil, this does nothing.

func (*ErrorCollector) Emit

func (ec *ErrorCollector) Emit() error

Emit returns the final error from this ErrorCollector. Do not Add to this ErrorCollector after calling Emit, or the error it emitted might be modified.

If the error collector has collected no errors, this returns nil. If it has collected exactly one error, it returns that error. Otherwise, it returns an aggregate error, which is itself a warning if and only if all aggregated errors are warnings. This may involve wrapping contained warnings, so errors.Is does not erroneously represent a failure as a warning because it contains a warning as a subcomponent.

func (*ErrorCollector) HasFailure

func (ec *ErrorCollector) HasFailure() bool

HasFailure reports whether ec has at least one non-warning error.

func (*ErrorCollector) IsEmpty

func (ec *ErrorCollector) IsEmpty() bool

IsEmpty reports whether ec has zero accumulated errors/warnings.

type Failure

type Failure struct {
	// contains filtered or unexported fields
}

A Failure is an error that is definitely not a Warning. If a Warning is wrapped in a Failure, it stops being a Warning.

func (Failure) As

func (f Failure) As(target any) bool

As implements Go 1.13 error handling for a Failure. It directly unpacks its contained error if possible, bypassing the "re-wrap in failure" behavior that Unwrap otherwise uses.

func (Failure) Error

func (f Failure) Error() string

Error implements error. It returns the message of the underlying error, unchanged.

func (Failure) Is

func (f Failure) Is(target error) bool

Is implements Go 1.13 error handling for a Failure. It specifically rejects the notion that it could be AnyWarning, but otherwise forwards the check to its underlying However, it allows comparisons to other warnings to proceed as normal.

func (Failure) Unwrap

func (f Failure) Unwrap() error

Unwrap returns a new Failure wrapping the error wrapped by the underlying error, if any. If the underlying error wraps nothing, it returns nil.

type GameState

type GameState int

GameState represents various states a player's Game can be in.

func (GameState) Over

func (g GameState) Over() bool

Over returns whether this state represents a game that is over.

type InfoPanel

type InfoPanel[C StatsCollection] interface {
	// Title returns the title of this InfoPanel, which is also used as the
	// label presented to the player to access this panel.
	Title(p *Player[C]) Message

	// Info returns the displayable contents of this InfoPanel. A nil Message
	// in the output is interpreted as a paragraph break.
	Info(p *Player[C]) ([]Message, error)
}

An InfoPanel displays some set of stats to the player. It does not consume an action. It must not advance the state of the game in any way.

type Invisible

type Invisible[T any] struct {
	// Display name of this Stat.
	Name string
	// Value of this Stat. Can be overwritten.
	Value T
}

Invisible is a generic Stat implementation that stores a stat value and name. It's not visible to the player.

func (Invisible[T]) StatName

func (i Invisible[T]) StatName() string

Statname implements Stat.

func (Invisible[T]) String

func (i Invisible[T]) String() string

String implements Stat and fmt.Stringer.

func (Invisible[T]) Visible

func (Invisible[T]) Visible() bool

Visible implements Stat.

type Message

type Message interface {
	fmt.Stringer
}

Message is an opaque interface representing a displayable message. Using an interface here allows for implementation of new message display and formatting features without rewriting all existing callers.

func ErrorMessage added in v0.5.0

func ErrorMessage(e error) Message

ErrorMessage returns a Message representing an Error. This is preferred over Msgf for errors, since future versions of the library may perform special message formatting for errors.

func MsgStr

func MsgStr(s string) Message

MsgStr returns a Message representing a fixed string.

func Msgf

func Msgf(f string, args ...any) Message

Msgf is a Sprintf-like function that produces a Message equivalent to the one created by MsgStr.

type MultiMessage

type MultiMessage []Message

MultiMessage is a sequence of messages treated like one message.

func (MultiMessage) String

func (m MultiMessage) String() string

type PanelCard added in v0.4.0

type PanelCard[C StatsCollection] struct {
	Panel       InfoPanel[C]
	IsUrgent    bool
	CardOptions []CardOption[C]
	// AfterOption is given the card itself as its first argument.
	AfterOption func(c Card[C], p *Player[C], option CardOption[C]) error
}

A PanelCard is a Card that takes its title and text from an InfoPanel, while options, urgency, and the post-option callback are specified (like a BasicCard). It never does anything in particular when drawn.

Omitting all options yields an inactionable card, which can be displayed but not played. This can be useful for adding an info panel as a debug action.

func (*PanelCard[C]) Drawn added in v0.4.0

func (c *PanelCard[C]) Drawn(_ *Player[C]) bool

Drawn implements Card.

func (*PanelCard[C]) EventText added in v0.4.0

func (c *PanelCard[C]) EventText(p *Player[C]) (Message, error)

EventText implements Card.

func (*PanelCard[C]) Options added in v0.4.0

func (c *PanelCard[C]) Options(_ *Player[C]) ([]CardOption[C], error)

Options implements Card.

func (*PanelCard[C]) Then added in v0.4.0

func (c *PanelCard[C]) Then(p *Player[C], option CardOption[C]) error

Then implements Card.

func (*PanelCard[C]) Title added in v0.4.0

func (c *PanelCard[C]) Title(p *Player[C]) Message

Title implements Card.

func (*PanelCard[C]) Urgent added in v0.4.0

func (c *PanelCard[C]) Urgent(_ *Player[C]) bool

Urgent implements Card.

type Player

type Player[C StatsCollection] struct {
	// Stats stores simulation-specific state.
	Stats C

	// Name stores the player's name.
	Name string

	// Rand is a source of randomness that other components can use.
	Rand *rand.Rand

	Deck       *Deck[C]
	Hand       []Card[C]
	TurnNumber int
	State      GameState

	// HandLimit is number of cards to draw to at the start of each turn.
	// If the player has more cards than this already, none will be drawn,
	// but the player will keep them all.
	//
	// If this is 0 or less and the player has no cards in hand, no permanent
	// actions available, and must take an action, the game ends in stalemate.
	HandLimit int

	// ActionsPerTurn is what ActionsRemaining resets to at the start of each
	// turn. If this is 0 or less at the start of a turn, the game ends in
	// stalemate. Activating a card or permanent action spends an action, but
	// the card or action itself can counter this by changing the player's
	// ActionsRemaining by giving the action back -- or force the turn to
	// progress immediately to simulation by setting it to 0.
	ActionsPerTurn   int
	ActionsRemaining int

	// PermanentActions are an "extra hand" of cards that are not discarded when
	// used. An Urgent PermanentAction does not block non-urgent actions and
	// cards in hand from being used, but it can be used even when an urgent
	// card is in the hand.
	PermanentActions []Card[C]

	// DebugActions are PermanentActions only available when the player is in
	// debug mode. InitPlayer adds some standard debugging actions by default.
	DebugActions []Card[C]

	// InfoPanels lists informational views available to the player. The Prompt
	// is the InfoPanel shown before the main action menu.
	InfoPanels []InfoPanel[C]
	Prompt     InfoPanel[C]

	// Rules are the simulation rules executed every turn after the player has
	// run out of remaining actions. See `RuleCollection`'s documentation for
	// more information about how rule execution works.
	Rules *RuleCollection[C]

	// Temporary messages are shown *before* the Prompt. They're cleared just
	// before executing rules for the turn, so rules adding to TemporaryMessages
	// are creating messages that will show up for the next turn. Temporary
	// panels are cleared out at the same time as temporary messages; when
	// available, they are listed separately from standard panels (before them).
	TemporaryMessages []Message
	TemporaryPanels   []InfoPanel[C]

	// DebugLevel stores how verbose the game should be about errors. If this
	// is greater than 0, invisible stats will usually be shown to the player
	// (this is up to individual info panels, though). If this is -1 or lower,
	// warning messages will not be displayed.
	DebugLevel int
}

Player stores all gameplay state for one player at a specific point in time. Game-specific data is stored in Stats.

Player is a generic type -- see https://go.dev/blog/intro-generics for more information on how these work. Think of "Player" as a "type of type" -- when you create one, you tell it what kind of data it needs to keep for the simulation itself, and each Player that works with a different kind of data is a different kind of Player and the compiler will help you with that. This is the same idea as "slice of something" or "map from something to something" -- different kinds of Players are different from each other and "know" what type of data they use, so the compiler can tell you if you're using the wrong type.

Generic types have to use a placeholder to represent the type (or types -- consider maps, which have both keys and values) that will be more specific when the type is actually used. They're called "type parameters", like function parameters, because they're the same kind of idea. A function puts its parameters into variables so you can write a function that works with whatever data it gets; a generic type takes type parameters and represents them with type placeholders so you can write a *type* that works with whatever specific other types it gets.

Just like function parameters have a type that says what kind of data the function works with, type parameters have a "type constraint" that says what kind of types the generic type works with. Go already has a familiar way to express the idea of "what a type has to do": `interface`. In Go, type constraints are just interfaces.

But wait, why use generics at all? Can't we just use an interface in the normal way instead of doing this thing? Well, yes, we could, but then the compiler doesn't know that the "real types" for things matching these interfaces all have to actually be the same type. The compiler will stop you from putting an `Orange` into a `[]Apple`, but it wouldn't stop you from putting a `Fruit` into a `[]Fruit` because, well, of course it wouldn't, they're the same type.

Different simulation games made with `cardsim` are different. Rules made for simulating the economy of a kobold colony and mine wouldn't work at all with data for a simulation about three flocks of otter-gryphons having a territory conflict over a river full of fish. By using generics, the compiler can recognize functions and data and types intended for different simulation games and prevent you from using the wrong one, when it wouldn't be able to if all this stuff was written for "some simulation game, don't care what".

Generic interfaces (like `Card[C]`, `Rule[C]`, `InfoPanel[C]`, and more) don't mean you have to write generics of your own. It's exactly the opposite! Because the interface has this extra type in it, you only need to implement the specific kind of interface that works with your game. There's more detail on this in the comment on `Rule[C]`.

func InitPlayer

func InitPlayer[C StatsCollection](stats C) *Player[C]

InitPlayer returns a mostly-uninitialized Player with the fields that require specific initialization already configured and the provided StatsCollection (if any) already assigned to its Stats. Most fields are not configured and need to be assigned after this.

The Player is initialized with an empty deck, empty rule collection, a hand limit of 1, an actions-per-turn limit of 1, and a random number generator seeded with the nanosecond component of the current time. The Deck shares this random number generator.

func (*Player[C]) CanAct

func (p *Player[C]) CanAct() bool

CanAct returns whether the player has actions theoretically available.

func (*Player[C]) ChapterBreak

func (p *Player[C]) ChapterBreak()

ChapterBreak apends a chapter break to p.TemporaryMessages, unless it is empty or the most recent non-nil message is already a chapter break.

func (*Player[C]) Debug

func (p *Player[C]) Debug(minLevel int, msg Message)

Debug adds a message to the player's temporary messages if their debug level is at least the level specified.

func (*Player[C]) Draw

func (p *Player[C]) Draw() error

Draw draws a card into the hand, informing the card that it has been drawn. If more than a million cards refuse to enter the hand, this gives up and returns WarningUncooperativeCards. If the deck does not have enough cards, this returns WarningTooFewCards.

func (*Player[C]) Emit

func (p *Player[C]) Emit(msg Message)

Emit adds a message to the player's temporary messages.

func (*Player[C]) EnactCard

func (p *Player[C]) EnactCard(cardIdx, choiceIdx int) (Message, error)

EnactCard executes a card choice, removes it from the hand, and decrements the ActionsRemaining. If the card is not Urgent but urgent cards are available, or the player is out of actions, this returns ErrNotUrgent or ErrNoActions. Otherwise, this acts like EnactCardUnchecked.

func (*Player[C]) EnactCardUnchecked

func (p *Player[C]) EnactCardUnchecked(cardIdx, choiceIdx int) (Message, error)

EnactCardUnchecked executes a card choice, removes it from the hand, and decrements the ActionsRemaining. It does not check for conflicting Urgent cards or already being out of actions. If no such card or card choice exists, or the specified choice is not enabled, this returns nil and ErrInvalidCard/ErrInvalidChoice without changing anything. Otherwise, this returns the result of enacting the card. If enacting the card causes a serious error, the State becomes GameCrashed.

func (*Player[C]) EnactDebugActionUnchecked added in v0.4.0

func (p *Player[C]) EnactDebugActionUnchecked(actionIdx, choiceIdx int) (Message, error)

EnactDebugActionUnchecked executes a debug action and decrements the ActionsRemaining, even though most debug actions will want to refund that action point. (Consistency with other actions is important.) It does not check for Urgent cards or for already being out of actions. If no such action or card option exists, or the option is not enabled, this returns nil and ErrInvalidCard or ErrInvalidChoice without changing anything. If the player is not in debug mode (DebugLevel >= 1), this returns ErrNotDebugging. Otherwise, this returns the result of enacting the debug action. If enacting the action causes a serious error, the State becomes GameCrashed.

func (*Player[C]) EnactPermanentAction

func (p *Player[C]) EnactPermanentAction(actionIdx, choiceIdx int) (Message, error)

EnactPermanentAction executes a permanently-available card and decrements the ActionsRemaining. If the action is not Urgent but urgent cards are available, or the player is out of actions, this returns ErrNotUrgent or ErrNoActions. Otherwise, this acts like EnactPermanentActionUnchecked.

func (*Player[C]) EnactPermanentActionUnchecked

func (p *Player[C]) EnactPermanentActionUnchecked(actionIdx, choiceIdx int) (Message, error)

EnactPermanentActionUnchecked executes a permanently-available action and decrements the ActionsRemaining. It does not check for conflicting Urgent cards or already being out of actions. If no such action or card option exists, or the option is not enabled, this returns nil and ErrInvalidCard or ErrInvalidChoice without changing anything. Otherwise, this returns the result of enacting the permanent action. If enacting the card causes a serious error, the State becomes GameCrashed.

func (*Player[C]) FillHand

func (p *Player[C]) FillHand() error

FillHand draws up to the hand limit, informing cards that they have been drawn. If more than a million cards refuse to enter the hand, this gives up and returns WarningUncooperativeCards. If the deck does not have enough cards, this returns WarningTooFewCards.

func (*Player[C]) HasUrgentCards

func (p *Player[C]) HasUrgentCards() bool

HasUrgentCards returns whether any cards in the Hand think they are Urgent.

func (*Player[C]) ReportError

func (p *Player[C]) ReportError(e error)

ReportError adds an error to the temporary messages, depending on its severity and debug settings:

  • If the error is nil, this never does anything.
  • If the error is serious, this emits the error if the debug level is -1 or greater.
  • If the error is only a warning, this emits the error if the debug level is 0 or greater.

func (*Player[C]) SectionBreak

func (p *Player[C]) SectionBreak()

SectionBreak apends a section break to p.TemporaryMessages, unless it is empty or the most recent non-nil message is already a section/chapter break.

func (*Player[C]) Simulate

func (p *Player[C]) Simulate() error

Simulate executes the simulation up to the start of the next turn. If the simulation crashes, the game state becomes GameCrashed. This returns any generated errors; if the debugging mode is 0 or greater, they also become temporary messages for the next turn.

func (*Player[C]) StartNextTurn

func (p *Player[C]) StartNextTurn() error

StartNextTurn increments the turn counter, resets the action counter, and draws back up to full. If the player cannot take any actions after drawing is complete, the game stalls. (If a drawn card would like to force the player to take no actions for a turn, the best approach is to make that card Urgent and make it reset ActionsRemaining to 0 after it runs.)

type Rule

type Rule[C StatsCollection] interface {
	// 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.
	// Label must be constant for the life of the Rule, including after the
	// Rule is removed from the collection (it is called as part of the
	// removal process).
	Label() string

	// Step returns which numerical step this rule should be executed on.
	// It is somewhat arbitrary. Rules are enacted in order from lowest step
	// to highest step; ties are broken randomly every turn. Step must be
	// constant for the life of the Rule, including after the Rule is removed
	// from the collection (it is called during removal).
	Step() int

	// Enact operates the rule. Some special errors are recognized and change
	// the flow of the turn:
	//
	// * ErrHaltTurn skips all further rules (including rules on the same step)
	//   and immediately proceeds to the next turn.
	// * ErrDeleteRule removes the rule from the collection. It will not be
	//   executed on future turns unless it is reinserted into the active
	//   ruleset (by some other rule, option, etc.)
	// * ErrHaltDelete acts like both ErrHaltTurn and ErrDeleteRule.
	// * Any Warning is collected and displayed at the end of the turn.
	// * Any other error crashes the game.
	Enact(*Player[C]) error
}

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 RuleCollection

type RuleCollection[C StatsCollection] struct {
	// contains filtered or unexported fields
}

RuleCollection stores, organizes, and runs all rules currently active for a Player.

func NewRuleCollection

func NewRuleCollection[C StatsCollection]() *RuleCollection[C]

NewRuleCollection initializes an empty RuleCollection.

func (*RuleCollection[C]) Insert

func (r *RuleCollection[C]) Insert(rule Rule[C]) RuleID

Insert adds a Rule to a RuleCollection, returning the ID it assigned to the rule.

func (*RuleCollection[C]) RemoveAllLabel

func (r *RuleCollection[C]) RemoveAllLabel(label string) int

RemoveAllLabel removes every rule with a specific label. It returns how many rules were thus removed.

func (*RuleCollection[C]) RemoveAllStep

func (r *RuleCollection[C]) RemoveAllStep(step int) int

RemoveAllStep removes every rule on a specific step. It returns how many rules were thus removed.

func (*RuleCollection[C]) RemoveID

func (r *RuleCollection[C]) RemoveID(id RuleID) bool

RemoveID removes the rule with the given ID from the collection, if present. It returns whether it found and removed anything.

func (*RuleCollection[C]) RemoveUniqueLabel

func (r *RuleCollection[C]) RemoveUniqueLabel(label string) (bool, error)

RemoveUniqueLabel removes the sole rule with a specific label. If there are no rules with that label, this returns false and nil. If there is exactly one rule with that label, it is removed and this returns true and nil. If there are multiple rules with this label, this returns false and an error like ErrNotUnique.

func (*RuleCollection[C]) RemoveUniqueStep

func (r *RuleCollection[C]) RemoveUniqueStep(step int) (bool, error)

RemoveUniqueStep removes the sole rule on a specific step. Its semantics are equivalent to RemoveUniqueLabel.

func (*RuleCollection[C]) Run

func (r *RuleCollection[C]) Run(p *Player[C]) error

Run runs all rules in the collection against the specified Player, in step order, with ties broken randomly.

It stops early if it observes a failure other than the errors described in Rule.Enact as having special meaning to the rules engine.

type RuleDumper added in v0.4.0

type RuleDumper[C StatsCollection] struct{}

RuleDumper is an InfoPanel[C] that dumps all rules in P.

func (RuleDumper[C]) Info added in v0.4.0

func (RuleDumper[C]) Info(p *Player[C]) ([]Message, error)

func (RuleDumper[C]) Title added in v0.4.0

func (RuleDumper[C]) Title(p *Player[C]) Message

type RuleFunc

type RuleFunc[C StatsCollection] struct {
	Name string
	Seq  int
	F    func(*Player[C]) error
}

RuleFunc implements a Rule represented by a single function. It is the most common type of Rule. (You'll probably only want to implement a full Rule type if the rule needs to have fields of its own or be identifiable by something other than its Name.)

func (*RuleFunc[C]) Enact

func (r *RuleFunc[C]) Enact(p *Player[C]) error

func (*RuleFunc[C]) Label

func (r *RuleFunc[C]) Label() string

func (*RuleFunc[C]) Step

func (r *RuleFunc[C]) Step() int

type RuleID

type RuleID uint64

RuleID is a unique opaque ID for a rule inserted in a collection.

type SpecialMessage

type SpecialMessage struct {
	// contains filtered or unexported fields
}

A SpecialMessage is a specific, uniquely identifiable message.

func (*SpecialMessage) String

func (s *SpecialMessage) String() string

String implements Message.

type Stat

type Stat interface {
	// StatName returns the name of this stat, as displayed to the player.
	StatName() string
	// String prints the value of the stat.
	String() string // compatible with fmt.Stringer
	// Visible returns whether this stat should be displayed to the player
	// during regular gameplay. (Invisible stats are important for debugging.)
	Visible() bool
}

A Stat is some value that can be printed as part of player status. It may not be an actual stored value -- it might only be calculated.

func ExtractStats

func ExtractStats(x any) []Stat

ExtractStats pulls all exported stats out of a struct. It puts methods before fields. If the calculated name of a method conflicts with the calculated name of a stat from a field, the method wins.

A field is a stat if it is of some Stat type or is tagged with `cardsim:"stat"`, `cardsim:"hidden"` (invisible stat), `cardsim:"round2"` (or any integer, 2 is just an example), or `cardsim:"hiddenround3"`. `hiddenstat`, `statround`, and `hiddenstatround` are also accepted, but other orders of these directives are not. A "round" stat must be a float type and it will be rounded to this number of decimal places.

A method is a Stat if it takes 0 arguments, returns exactly 1 value, and starts with Stat or HiddenStat.

The name of these inferred stats is calculated by breaking the name into separate words before each capital letter, unless there are consecutive capital letters, which it interprets as an initialism (followed by the start of another word, if it's not at the end). To insert a space between consecutive capital letters, insert an underscore (`_`). This name inference trims "Stat" and "HiddenStat" off the front of method names.

To override the name extracted from a field name, add `cardsim_name:"name"` to the tag, where the name part is the name you want to use. It will not be formatted further - use normal spaces, capitalization, etc.

func InvisibleStatFunc

func InvisibleStatFunc[T any](name string, f func() T) Stat

InvisibleStatFunc names a function as a stat not visible to the player.

func StatFunc

func StatFunc[T any](name string, f func() T) Stat

StatFunc names a function as a stat visible to the player.

type StatFilter

type StatFilter[C StatsCollection] func(p *Player[C], s Stat) bool

A StatFilter decides whether to show a specific stat in a BasicStatsPanel (and maybe other kinds of stats panels, if they choose to support this).

func All

func All[C StatsCollection](ff ...StatFilter[C]) StatFilter[C]

All returns a StatFilter[C] that requires a Stat to match all provided filters. If no filters are provided, All matches every Stat (it's very easy to meet every requirement when there are no requirements).

func Any

func Any[C StatsCollection](ff ...StatFilter[C]) StatFilter[C]

Any returns a StatFilter[C] that requires a Stat to match any one or more of the filters provided. If no filters are provided, Any never matches a stat (it's very hard to meet at least one requirement out when there are no requirements).

func StatsNamed

func StatsNamed[C StatsCollection](names ...string) StatFilter[C]

StatsNamed returns a StatFilter[C] matching any stat with a listed name.

func VisibleOrDebugStatsNamed

func VisibleOrDebugStatsNamed[C StatsCollection](names ...string) StatFilter[C]

VisibleOrDebugStatsNamed returns a StatFilter[C] matching any visible stat with a listed name, or any stat with a listed name if the player is in debug mode.

type StatLiteral added in v0.2.0

type StatLiteral struct {
	Name      string
	Value     string
	IsVisible bool
}

StatLiteral stores a ready-to-emit stat value.

func EmitHiddenStat added in v0.2.0

func EmitHiddenStat(name string, v any) *StatLiteral

func EmitStat added in v0.2.0

func EmitStat(name string, v any) *StatLiteral

func HiddentStatf added in v0.2.0

func HiddentStatf(name string, f string, args ...any) *StatLiteral

func RoundHiddenStat added in v0.2.0

func RoundHiddenStat[N constraints.Float](name string, val N, decimals int) *StatLiteral

func RoundStat added in v0.2.0

func RoundStat[N constraints.Float](name string, val N, decimals int) *StatLiteral

func Statf added in v0.2.0

func Statf(name string, f string, args ...any) *StatLiteral

func (*StatLiteral) StatName added in v0.2.0

func (s *StatLiteral) StatName() string

func (*StatLiteral) String added in v0.2.0

func (s *StatLiteral) String() string

func (*StatLiteral) Visible added in v0.2.0

func (s *StatLiteral) Visible() bool

type StatsCollection

type StatsCollection interface {
	// Stats returns all the stats in this collection. It's okay for
	// these to be copies rather than pointers. BasicStatsPanel presents
	// stats to the player in this order. It's okay for this list to
	// contain nil entries; these are interpreted as line breaks,
	// section breaks, etc.
	Stats() []Stat
}

A StatsCollection contains stats.

type Stored

type Stored[T any] struct {
	// Display name of this Stat.
	Name string
	// Value of this Stat. Can be overwritten.
	Value T
}

Stored is a generic Stat implementation that stores a stat value and name. It's visible to the player.

func (Stored[T]) StatName

func (s Stored[T]) StatName() string

Statname implements Stat.

func (Stored[T]) String

func (s Stored[T]) String() string

String implements Stat and fmt.Stringer.

func (Stored[T]) Visible

func (Stored[T]) Visible() bool

Visible implements Stat.

type Titled added in v0.4.0

type Titled[C StatsCollection] interface {
	Title(*Player[C]) Message
}

Titled desccribes any type that returns a Message as a title, given a Player (which it may ignore).

type Warning

type Warning struct {
	E error
}

Warning is an error that does not crash the game, but is displayed immediately.

func Warningf

func Warningf(f string, args ...any) *Warning

Warningf calls fmt.Errorf with the provided arguments, wraps the error created as a Warning, and returns it.

func (*Warning) Error

func (w *Warning) Error() string

Error implements the error interface. A warning's error message is the message of its underlying error, unmodified.

func (*Warning) Is

func (*Warning) Is(target error) bool

Is recognizes every Warning as equivalent to AnyWarning.

func (*Warning) Unwrap

func (w *Warning) Unwrap() error

Unwrap implements the Go 1.13 error handling system by allow error identity searches to continue to the underlying error.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL