CardSimEngine/cardsim/player.go

258 lines
9.4 KiB
Go

package cardsim
import (
"errors"
"math/rand"
)
var (
ErrUncooperativeCards = errors.New("a milion cards refused to join the hand")
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.
PermanentActions []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
)
// 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:", 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()
}
// 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 {
failureLimit := 1000000
for failureLimit > 0 && p.Deck.Len() > 0 && len(p.Hand) < p.HandLimit {
c := p.Deck.Draw()
if c.Drawn(p) {
p.Hand = append(p.Hand, c)
} else {
failureLimit--
}
}
if len(p.Hand) >= p.HandLimit {
return nil
}
if failureLimit <= 0 {
return ErrUncooperativeCards
}
return WarningTooFewCards
}