Implement drawing and running a turn.

This commit is contained in:
Kistaro Windrider 2023-04-01 20:52:46 -07:00
parent a6b2c92f86
commit 576a2dd69e
Signed by: kistaro
SSH Key Fingerprint: SHA256:TBE2ynfmJqsAf0CP6gsflA0q5X5wD5fVKWPsZ7eVUg8
3 changed files with 166 additions and 9 deletions

View File

@ -52,6 +52,12 @@ func Warningf(f string, args ...any) *Warning {
return &Warning{fmt.Errorf(f, args...)} return &Warning{fmt.Errorf(f, args...)}
} }
// IsSeriousError returns whether e is a non-warning error. If e is nil, this
// returns false.
func IsSeriousError(e error) bool {
return !errors.Is(e, AnyWarning)
}
// A Failure is an error that is definitely not a Warning. If a Warning is // A Failure is an error that is definitely not a Warning. If a Warning is
// wrapped in a Failure, it stops being a Warning. // wrapped in a Failure, it stops being a Warning.
type Failure struct { type Failure struct {
@ -144,8 +150,11 @@ func (f Failure) As(target any) bool {
// - Otherwise, it returns an error that combines all the errors it collected. // - 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 // The aggregated error is a Warning if and only if all collected errors
// were also warnings. // were also warnings.
//
// An ErrorCollector's Errs should not be written by anything other than Add,
// or HasFailure may get out of sync.
type ErrorCollector struct { type ErrorCollector struct {
errs []error Errs []error
hasFailure bool hasFailure bool
} }
@ -157,12 +166,12 @@ func (ec *ErrorCollector) Add(e error) {
if !errors.Is(e, AnyWarning) { if !errors.Is(e, AnyWarning) {
ec.hasFailure = true ec.hasFailure = true
} }
ec.errs = append(ec.errs, e) ec.Errs = append(ec.Errs, e)
} }
// IsEmpty reports whether ec has zero accumulated errors/warnings. // IsEmpty reports whether ec has zero accumulated errors/warnings.
func (ec *ErrorCollector) IsEmpty() bool { func (ec *ErrorCollector) IsEmpty() bool {
return len(ec.errs) == 0 return len(ec.Errs) == 0
} }
// HasFailure reports whether ec has at least one non-warning error. // HasFailure reports whether ec has at least one non-warning error.
@ -181,16 +190,16 @@ func (ec *ErrorCollector) HasFailure() bool {
// errors.Is does not erroneously represent a failure as a warning because it // errors.Is does not erroneously represent a failure as a warning because it
// contains a warning as a subcomponent. // contains a warning as a subcomponent.
func (ec *ErrorCollector) Emit() error { func (ec *ErrorCollector) Emit() error {
if len(ec.errs) == 0 { if len(ec.Errs) == 0 {
return nil return nil
} }
if len(ec.errs) == 1 { if len(ec.Errs) == 1 {
return ec.errs[0] return ec.Errs[0]
} }
if ec.HasFailure() { if ec.HasFailure() {
return aggregateFailure(ec.errs) return aggregateFailure(ec.Errs)
} }
return &aggregateError{ec.errs} // all these are recognizable warnings return &aggregateError{ec.Errs} // all these are recognizable warnings
} }
// An aggregateError is a collection of errors that is itself an error. // An aggregateError is a collection of errors that is itself an error.

View File

@ -25,3 +25,34 @@ func MsgStr(s string) Message {
func Msgf(f string, args ...any) Message { func Msgf(f string, args ...any) Message {
return stringMessage(fmt.Sprintf(f, args...)) return stringMessage(fmt.Sprintf(f, args...))
} }
// A SpecialMessage is a specific, uniquely identifiable message.
type SpecialMessage struct {
msg Message
}
// String implements Message.
func (s *SpecialMessage) String() string {
if s == nil {
return ""
}
return s.msg.String()
}
// 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.
var (
SectionBreak = &SpecialMessage{MsgStr(" -------------------------------------------------------------------- ")}
ChapterBreak = &SpecialMessage{MsgStr(" ==================================================================== ")}
)
// IsSpecialMessage returns whether a provided Message is a specific SpecialMessage.
func IsSpecialMessage(m Message, s *SpecialMessage) bool {
if m == nil {
return s == nil
}
if s2, ok := m.(*SpecialMessage); ok {
return s == s2
}
return false
}

View File

@ -1,6 +1,15 @@
package cardsim package cardsim
import "math/rand" 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. // Player stores all gameplay state for one player at a specific point in time.
// Game-specific data is stored in Stats. // Game-specific data is stored in Stats.
@ -138,3 +147,111 @@ const (
func (g GameState) Over() bool { func (g GameState) Over() bool {
return g == GameLost || g == GameWon || g == GameCrashed || g == GameStalled 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
}