From 45e1eeebf715dee13d07c95373f6d56b49104b4b Mon Sep 17 00:00:00 2001 From: Kistaro Windrider Date: Sat, 1 Apr 2023 21:18:48 -0700 Subject: [PATCH] Play cards. --- cardsim/player.go | 137 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 135 insertions(+), 2 deletions(-) diff --git a/cardsim/player.go b/cardsim/player.go index 79a3a20..2758ec8 100644 --- a/cardsim/player.go +++ b/cardsim/player.go @@ -2,11 +2,16 @@ package cardsim import ( "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") ) @@ -92,7 +97,10 @@ type Player[C StatsCollection] struct { ActionsPerTurn int ActionsRemaining int - // PermanentActions are an "extra hand" of cards that are not discarded when used. + // 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] // InfoPanels lists informational views available to the player. The Prompt @@ -197,7 +205,7 @@ func (p *Player[C]) Simulate() error { if p.DebugLevel > 0 && !errs.IsEmpty() { p.ChapterBreak() - p.TemporaryMessages = append(p.TemporaryMessages, Msgf("%d errors and warnings:", len(errs.Errs))) + p.TemporaryMessages = append(p.TemporaryMessages, Msgf("%d ERRORS AND WARNINGS:", len(errs.Errs))) for i, e := range errs.Errs { yikes := " " if IsSeriousError(e) { @@ -255,3 +263,128 @@ func (p *Player[C]) FillHand() error { 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) +}