Compare commits
No commits in common. "45e1eeebf715dee13d07c95373f6d56b49104b4b" and "a6b2c92f86bbded5c5ac0e6ea01af527bfd6fa03" have entirely different histories.
45e1eeebf7
...
a6b2c92f86
@ -52,12 +52,6 @@ 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 {
|
||||||
@ -150,11 +144,8 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,12 +157,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.
|
||||||
@ -190,16 +181,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.
|
||||||
|
@ -25,34 +25,3 @@ 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
|
|
||||||
}
|
|
||||||
|
@ -1,20 +1,6 @@
|
|||||||
package cardsim
|
package cardsim
|
||||||
|
|
||||||
import (
|
import "math/rand"
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
)
|
|
||||||
|
|
||||||
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")
|
|
||||||
|
|
||||||
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.
|
||||||
@ -97,10 +83,7 @@ type Player[C StatsCollection] struct {
|
|||||||
ActionsPerTurn int
|
ActionsPerTurn int
|
||||||
ActionsRemaining int
|
ActionsRemaining int
|
||||||
|
|
||||||
// PermanentActions are an "extra hand" of cards that are not discarded when
|
// PermanentActions are an "extra hand" of cards that are not discarded when used.
|
||||||
// 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]
|
PermanentActions []Card[C]
|
||||||
|
|
||||||
// InfoPanels lists informational views available to the player. The Prompt
|
// InfoPanels lists informational views available to the player. The Prompt
|
||||||
@ -155,236 +138,3 @@ 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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, this returns nil and ErrInvalidCard/ErrInvalidChoice. Otherwise, this returns
|
|
||||||
// the result of enacting the card. If enacting the card causes an 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", ErrInvalidCard, choiceIdx, cardIdx, len(options)))
|
|
||||||
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, this returns nil and ErrInvalidCard/ErrInvalidChoice. Otherwise, this returns
|
|
||||||
// the result of enacting the permanent action. If enacting the card causes an error,
|
|
||||||
// the State becomes GameCrashed.
|
|
||||||
func (p *Player[C]) EnactPermanentActionUnchecked(actionIdx, choiceIdx int) (Message, error) {
|
|
||||||
if actionIdx < 0 || actionIdx >= len(p.PermanentActions) {
|
|
||||||
return nil, fmt.Errorf("%w: no action #%d when %d permanent actions exist", ErrInvalidCard, actionIdx, len(p.PermanentActions))
|
|
||||||
}
|
|
||||||
card := p.PermanentActions[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 permanent action #%d with %d options", ErrInvalidChoice, choiceIdx, actionIdx, len(options)))
|
|
||||||
return nil, errs.Emit()
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user