diff --git a/cardsim/card.go b/cardsim/card.go index cd51389..5d3cc55 100644 --- a/cardsim/card.go +++ b/cardsim/card.go @@ -52,6 +52,9 @@ type CardOption[C StatsCollection] interface { // After an option is enacted, the card is deleted. If a card should be // repeatable, Enact must return it to the deck (on every option). Enact(p *Player[C]) (Message, error) + + // Enabled returns whether this option can curently be enacted. + Enabled(p *Player[C]) bool } // A BasicCard is a Card with fixed title, text, options, and optional @@ -64,22 +67,27 @@ type BasicCard[C StatsCollection] struct { AfterOption func(p *Player[C], option CardOption[C]) error } -func (b *BasicCard[C]) Title(p *Player[C]) (Message, error) { +// Title implements Card. +func (b *BasicCard[C]) Title(_ *Player[C]) (Message, error) { return b.CardTitle, nil } +// Urgent implements Card. func (b *BasicCard[C]) Urgent(_ *Player[C]) bool { return b.IsUrgent } -func (b *BasicCard[C]) EventText(p *Player[C]) (Message, error) { +// EventText implements Card. +func (b *BasicCard[C]) EventText(_ *Player[C]) (Message, error) { return b.CardText, nil } +// Options implements Card. func (b *BasicCard[C]) Options(_ *Player[C]) ([]CardOption[C], error) { return b.CardOptions, nil } +// Then implements Card. func (b *BasicCard[C]) Then(p *Player[C], option CardOption[C]) error { if b.AfterOption == nil { return nil @@ -87,11 +95,13 @@ func (b *BasicCard[C]) Then(p *Player[C], option CardOption[C]) error { return b.AfterOption(p, option) } -func (b *BasicCard[C]) Drawn(p *Player[C]) bool { +// Drawn implements Card. +func (b *BasicCard[C]) Drawn(_ *Player[C]) bool { return true } // A BasicOption is a CardOption with fixed text, effects, and output. +// It's always enabled. type BasicOption[C StatsCollection] struct { Text Message Effect func(*Player[C]) error @@ -108,6 +118,11 @@ func (b *BasicOption[C]) Enact(p *Player[C]) (Message, error) { return b.Output, b.Effect(p) } +// Enabled implements CardOption. +func (b *BasicOption[C]) Enabled(p *Player[C]) bool { + return true +} + // OptionFunc attaches a fixed prompt to an Enact-like function. Unlike // BasicOption, the enactment function provided to OptionFunc returns // the text that should be output as a result of the action, so it is @@ -121,10 +136,17 @@ type optionFunc[C StatsCollection] struct { f func(*Player[C]) (Message, error) } +// OptionText implements CardOption. func (o *optionFunc[C]) OptionText(p *Player[C]) (Message, error) { return o.text, nil } +// Enact implements CardOption. func (o *optionFunc[C]) Enact(p *Player[C]) (Message, error) { return o.f(p) } + +// Enabled implements CardOption. +func (o *optionFunc[C]) Enabled(p *Player[C]) bool { + return true +} diff --git a/cardsim/player.go b/cardsim/player.go index 2758ec8..a0eeaac 100644 --- a/cardsim/player.go +++ b/cardsim/player.go @@ -277,9 +277,10 @@ func (p *Player[C]) HasUrgentCards() bool { // 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. +// 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)) @@ -293,7 +294,13 @@ func (p *Player[C]) EnactCardUnchecked(cardIdx, choiceIdx int) (Message, error) } 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))) + 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() } @@ -335,9 +342,10 @@ func (p *Player[C]) EnactCard(cardIdx, choiceIdx int) (Message, error) { // 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. +// 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) { 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)) @@ -354,17 +362,22 @@ func (p *Player[C]) EnactPermanentActionUnchecked(actionIdx, choiceIdx int) (Mes errs.Add(fmt.Errorf("%w: no option #%d on permanent 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 permanent action #%d is not enabled", ErrInvalidChoice, choiceIdx, actionIdx)) + return nil, errs.Emit() + } p.ActionsRemaining-- - ret, err := options[choiceIdx].Enact(p) + ret, err := chosen.Enact(p) errs.Add(err) if IsSeriousError(err) { p.State = GameCrashed return ret, errs.Emit() } - err = card.Then(p, options[choiceIdx]) + err = card.Then(p, chosen) errs.Add(err) if IsSeriousError(err) { p.State = GameCrashed