Kistaro Windrider
54711b36a8
Includes some refactoring work to pull out common code and express the idea of "wait, which panel, exactly?".
541 lines
20 KiB
Go
541 lines
20 KiB
Go
package cardsim
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"math/rand"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
ErrUncooperativeCards = errors.New("a milion cards refused to join the hand")
|
|
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 = errors.New("no actions can be taken")
|
|
)
|
|
|
|
// 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]`.
|
|
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.
|
|
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
|
|
}
|
|
|
|
// GameState represents various states a player's Game can be in.
|
|
type GameState int
|
|
|
|
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
|
|
)
|
|
|
|
// 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 InitPlayer[C StatsCollection](stats C) *Player[C] {
|
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
return &Player[C]{
|
|
Stats: stats,
|
|
Rand: r,
|
|
Deck: &Deck[C]{rand: r},
|
|
HandLimit: 1,
|
|
ActionsPerTurn: 1,
|
|
Rules: NewRuleCollection[C](),
|
|
}
|
|
}
|
|
|
|
// Over returns whether this state represents a game that is over.
|
|
func (g GameState) Over() bool {
|
|
return g == GameLost || g == GameWon || g == GameCrashed || g == GameStalled
|
|
}
|
|
|
|
// 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 (p *Player[C]) ChapterBreak() {
|
|
for i := len(p.TemporaryMessages) - 1; i >= 0; i-- {
|
|
m := p.TemporaryMessages[i]
|
|
if IsSpecialMessage(m, ChapterBreak) {
|
|
return
|
|
}
|
|
if p.TemporaryMessages[i] != nil {
|
|
p.TemporaryMessages = append(p.TemporaryMessages, ChapterBreak)
|
|
return
|
|
}
|
|
}
|
|
// No non-nil messages -- nothing to do.
|
|
}
|
|
|
|
// 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 (p *Player[C]) SectionBreak() {
|
|
for i := len(p.TemporaryMessages) - 1; i >= 0; i-- {
|
|
m := p.TemporaryMessages[i]
|
|
if IsSpecialMessage(m, ChapterBreak) || IsSpecialMessage(m, SectionBreak) {
|
|
return
|
|
}
|
|
if m != nil {
|
|
p.TemporaryMessages = append(p.TemporaryMessages, SectionBreak)
|
|
return
|
|
}
|
|
}
|
|
// No non-nil messages -- nothing to do.
|
|
}
|
|
|
|
// 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 (p *Player[C]) Simulate() error {
|
|
var errs ErrorCollector
|
|
p.TemporaryMessages = nil
|
|
p.TemporaryPanels = nil
|
|
errs.Add(p.Rules.Run(p))
|
|
errs.Add(p.StartNextTurn())
|
|
|
|
if errs.HasFailure() {
|
|
p.State = GameCrashed
|
|
}
|
|
|
|
if p.DebugLevel > 0 && !errs.IsEmpty() {
|
|
p.ChapterBreak()
|
|
p.TemporaryMessages = append(p.TemporaryMessages, Msgf("%d ERRORS AND WARNINGS while simulating turn:", len(errs.Errs)))
|
|
for i, e := range errs.Errs {
|
|
yikes := " "
|
|
if IsSeriousError(e) {
|
|
yikes = "!"
|
|
}
|
|
p.TemporaryMessages = append(p.TemporaryMessages, Msgf("%s[%d]:\t%v", yikes, i, e))
|
|
}
|
|
}
|
|
return errs.Emit()
|
|
}
|
|
|
|
// 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.)
|
|
func (p *Player[C]) StartNextTurn() error {
|
|
var errs ErrorCollector
|
|
p.TurnNumber++
|
|
p.ActionsRemaining = p.ActionsPerTurn
|
|
errs.Add(p.FillHand())
|
|
if len(p.Hand)+len(p.PermanentActions) == 0 {
|
|
p.State = GameStalled
|
|
errs.Add(Warningf("%w: no cards in hand, no permanent actions", WarningStalemate))
|
|
}
|
|
if p.ActionsRemaining == 0 {
|
|
p.State = GameStalled
|
|
errs.Add(Warningf("%w: 0 actions available in the turn", WarningStalemate))
|
|
}
|
|
return errs.Emit()
|
|
}
|
|
|
|
// 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 crashes with
|
|
// ErrUncooperativeCards. If the deck does not have enough cards, this
|
|
// returns WarningTooFewCards.
|
|
func (p *Player[C]) Draw() error {
|
|
for attempts := 0; attempts < 1000000; attempts++ {
|
|
if p.Deck.Len() == 0 {
|
|
return WarningTooFewCards
|
|
}
|
|
c := p.Deck.Draw()
|
|
if c.Drawn(p) {
|
|
p.Hand = append(p.Hand, c)
|
|
return nil
|
|
}
|
|
}
|
|
return ErrUncooperativeCards
|
|
}
|
|
|
|
// 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 crashes
|
|
// with ErrUncooperativeCards. If the deck does not have enough cards, this
|
|
// returns WarningTooFewCards.
|
|
func (p *Player[C]) FillHand() error {
|
|
var lastErr error
|
|
for p.Deck.Len() > 0 && len(p.Hand) < p.HandLimit {
|
|
lastErr = p.Draw()
|
|
if IsSeriousError(lastErr) {
|
|
return lastErr
|
|
}
|
|
}
|
|
|
|
if len(p.Hand) >= p.HandLimit {
|
|
return nil
|
|
}
|
|
|
|
return WarningTooFewCards
|
|
}
|
|
|
|
// HasUrgentCards returns whether any cards in the Hand think they are Urgent.
|
|
func (p *Player[C]) HasUrgentCards() bool {
|
|
for _, c := range p.Hand {
|
|
if c.Urgent(p) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// 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 EnactableType int
|
|
|
|
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
|
|
)
|
|
|
|
// 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 (p *Player[C]) EnactCardUnchecked(cardIdx, choiceIdx int) (Message, error) {
|
|
if cardIdx < 0 || cardIdx >= len(p.Hand) {
|
|
return nil, fmt.Errorf("%w: no card #%d when %d cards in hand", ErrInvalidCard, cardIdx, len(p.Hand))
|
|
}
|
|
card := p.Hand[cardIdx]
|
|
var errs ErrorCollector
|
|
options, err := card.Options(p)
|
|
if IsSeriousError(err) {
|
|
p.State = GameCrashed
|
|
return nil, err
|
|
}
|
|
errs.Add(err)
|
|
if choiceIdx < 0 || choiceIdx > len(options) {
|
|
errs.Add(fmt.Errorf("%w: no option #%d on card #%d with %d options", ErrInvalidChoice, choiceIdx, cardIdx, len(options)))
|
|
return nil, errs.Emit()
|
|
}
|
|
|
|
chosen := options[choiceIdx]
|
|
if !chosen.Enabled(p) {
|
|
errs.Add(fmt.Errorf("%w: option %d on card %d was not enabled", ErrInvalidChoice, choiceIdx, cardIdx))
|
|
return nil, errs.Emit()
|
|
}
|
|
|
|
p.Hand = DeleteFrom(p.Hand, cardIdx)
|
|
p.ActionsRemaining--
|
|
|
|
ret, err := options[choiceIdx].Enact(p)
|
|
errs.Add(err)
|
|
if IsSeriousError(err) {
|
|
p.State = GameCrashed
|
|
return ret, errs.Emit()
|
|
}
|
|
|
|
err = card.Then(p, options[choiceIdx])
|
|
errs.Add(err)
|
|
if IsSeriousError(err) {
|
|
p.State = GameCrashed
|
|
}
|
|
return ret, errs.Emit()
|
|
}
|
|
|
|
// 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 (p *Player[C]) EnactCard(cardIdx, choiceIdx int) (Message, error) {
|
|
if p.ActionsRemaining <= 0 {
|
|
return nil, ErrNoActions
|
|
}
|
|
if cardIdx < 0 || cardIdx >= len(p.Hand) {
|
|
return nil, fmt.Errorf("%w: no card #%d when %d cards in hand", ErrInvalidCard, cardIdx, len(p.Hand))
|
|
}
|
|
if !p.Hand[cardIdx].Urgent(p) && p.HasUrgentCards() {
|
|
return nil, ErrNotUrgent
|
|
}
|
|
return p.EnactCardUnchecked(cardIdx, choiceIdx)
|
|
}
|
|
|
|
// 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 (p *Player[C]) EnactPermanentActionUnchecked(actionIdx, choiceIdx int) (Message, error) {
|
|
return p.enactActionUnchecked(p.PermanentActions, actionIdx, choiceIdx)
|
|
}
|
|
|
|
// 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 (p *Player[C]) EnactDebugActionUnchecked(actionIdx, choiceIdx int) (Message, error) {
|
|
if p.DebugLevel < 1 {
|
|
return nil, ErrNotDebugging
|
|
}
|
|
return p.enactActionUnchecked(p.DebugActions, actionIdx, choiceIdx)
|
|
}
|
|
|
|
// enactActionUnchecked implements EnactPermanentActionUnchecked and EnactDebugActionUnchecked.
|
|
func (p *Player[C]) enactActionUnchecked(actionSource []Card[C], actionIdx, choiceIdx int) (Message, error) {
|
|
if actionIdx < 0 || actionIdx >= len(actionSource) {
|
|
return nil, fmt.Errorf("%w: no action #%d when %d actions exist", ErrInvalidCard, actionIdx, len(actionSource))
|
|
}
|
|
card := actionSource[actionIdx]
|
|
var errs ErrorCollector
|
|
options, err := card.Options(p)
|
|
if IsSeriousError(err) {
|
|
p.State = GameCrashed
|
|
return nil, err
|
|
}
|
|
errs.Add(err)
|
|
if choiceIdx < 0 || choiceIdx > len(options) {
|
|
errs.Add(fmt.Errorf("%w: no option #%d on action #%d with %d options", ErrInvalidChoice, choiceIdx, actionIdx, len(options)))
|
|
return nil, errs.Emit()
|
|
}
|
|
chosen := options[choiceIdx]
|
|
if !chosen.Enabled(p) {
|
|
errs.Add(fmt.Errorf("%w: option #%d on action #%d is not enabled", ErrInvalidChoice, choiceIdx, actionIdx))
|
|
return nil, errs.Emit()
|
|
}
|
|
|
|
p.ActionsRemaining--
|
|
|
|
ret, err := chosen.Enact(p)
|
|
errs.Add(err)
|
|
if IsSeriousError(err) {
|
|
p.State = GameCrashed
|
|
return ret, errs.Emit()
|
|
}
|
|
|
|
err = card.Then(p, chosen)
|
|
errs.Add(err)
|
|
if IsSeriousError(err) {
|
|
p.State = GameCrashed
|
|
}
|
|
return ret, errs.Emit()
|
|
}
|
|
|
|
// 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 (p *Player[C]) EnactPermanentAction(actionIdx, choiceIdx int) (Message, error) {
|
|
if p.ActionsRemaining <= 0 {
|
|
return nil, ErrNoActions
|
|
}
|
|
if actionIdx < 0 || actionIdx >= len(p.PermanentActions) {
|
|
return nil, fmt.Errorf("%w: no action #%d when %d permanent actions available", ErrInvalidCard, actionIdx, len(p.PermanentActions))
|
|
}
|
|
if !p.PermanentActions[actionIdx].Urgent(p) && p.HasUrgentCards() {
|
|
return nil, ErrNotUrgent
|
|
}
|
|
return p.EnactPermanentActionUnchecked(actionIdx, choiceIdx)
|
|
}
|
|
|
|
// 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 (p *Player[C]) ReportError(e error) {
|
|
if e == nil || p.DebugLevel < -1 {
|
|
return
|
|
}
|
|
if p.DebugLevel < 0 && !IsSeriousError(e) {
|
|
return
|
|
}
|
|
p.ChapterBreak()
|
|
severity := "[Warning]"
|
|
if IsSeriousError(e) {
|
|
severity = "[ERROR]"
|
|
}
|
|
p.TemporaryMessages = append(p.TemporaryMessages, Msgf("%s: %v", severity, e))
|
|
}
|
|
|
|
// CanAct returns whether the player has actions theoretically available.
|
|
func (p *Player[C]) CanAct() bool {
|
|
return p.ActionsRemaining > 0 && (len(p.Hand) > 0 || len(p.PermanentActions) > 0)
|
|
}
|
|
|
|
// Debug adds a message to the player's temporary messages if their debug level
|
|
// is at least the level specified.
|
|
func (p *Player[C]) Debug(minLevel int, msg Message) {
|
|
if p.DebugLevel < minLevel || msg == nil {
|
|
return
|
|
}
|
|
p.TemporaryMessages = append(p.TemporaryMessages, msg)
|
|
}
|
|
|
|
// Emit adds a message to the player's temporary messages.
|
|
func (p *Player[C]) Emit(msg Message) {
|
|
if msg == nil {
|
|
return
|
|
}
|
|
p.TemporaryMessages = append(p.TemporaryMessages, msg)
|
|
}
|