diff --git a/koboldsim/cardtypes.go b/koboldsim/cardtypes.go index 41aeb96..d6108de 100644 --- a/koboldsim/cardtypes.go +++ b/koboldsim/cardtypes.go @@ -9,6 +9,14 @@ import ( var ( ErrOptionNotEnabled = errors.New("option not enabled") ErrPolicyNotEnacted = errors.New("cannot unenact policy that is not enacted") + + // ErrUnimplemented and ErrKeepMessaage are "non-errors". They are used + // as special signals that the result needs to be handled in a special way; + // VerbosePolicy uses these to decide when to use the Default instead. + // If these are returned in a context that does not know how to respond + // to them, then they're just errors. + ErrUnimplemented = errors.New("unimplemented policy element") + ErrKeepMessaage = errors.New("use the default behavior but this message") ) type Policy interface { @@ -100,6 +108,7 @@ func (s *SwitchingCard) Then(p *Player, o CardOption) error { // BasicPolicy is a straightfoward implementation of Policy. If the currently // enacted option is re-enacted, it refunds the player's action point. type BasicPolicy struct { + Desc cardsim.Message UnenactedDesc cardsim.Message EnactedDesc cardsim.Message NothingChanged cardsim.Message @@ -124,13 +133,22 @@ func (b *BasicPolicy) LastEnacted(_ int, p Policy) { // OptionText implements CardOption. func (b *BasicPolicy) OptionText(*Player) (cardsim.Message, error) { if b.currentlyEnacted { + if b.EnactedDesc == nil { + return nil, ErrUnimplemented + } return b.EnactedDesc, nil } + if b.UnenactedDesc == nil { + return nil, ErrUnimplemented + } return b.UnenactedDesc, nil } // Enact implements CardOption. func (b *BasicPolicy) Enact(p *Player) (cardsim.Message, error) { + if b.Do == nil { + return nil, ErrUnimplemented + } if b.currentlyEnacted { p.ActionsRemaining++ if b.NothingChanged == nil { @@ -138,10 +156,10 @@ func (b *BasicPolicy) Enact(p *Player) (cardsim.Message, error) { } return b.NothingChanged, nil } - if b.CanDo != nil && !b.CanDo(p) { - return nil, ErrOptionNotEnabled + if b.Enabled(p) { + return b.Do(p) } - return b.Do(p) + return nil, ErrOptionNotEnabled } // Unenact implements Policy. @@ -149,6 +167,9 @@ func (b *BasicPolicy) Unenact(p *Player) error { if !b.currentlyEnacted { return ErrPolicyNotEnacted } + if b.Undo == nil { + return ErrUnimplemented + } return b.Undo(p) } @@ -158,7 +179,7 @@ func (b *BasicPolicy) Enabled(p *Player) bool { return true } if b.CanDo == nil { - b.CanDo = YesWeCan + panic(ErrUnimplemented) } return b.CanDo(p) } @@ -170,50 +191,154 @@ func (b *BasicPolicy) Is(p Policy) bool { return false } -// A DescResuilt is descriptive text for an option and the text result of -// enacting that option when it was described this way. -type DescResult struct { - Desc cardsim.Message - Result cardsim.Message -} - -// A VerbosePolicy is an extension to a BasicPolicy. It emits the BasicPolicy's -// `UnenactedDesc` and the message returned from Do only when no policy has ever -// been enacted for this card; otherwise, it looks up the description and result -// from a slice using the index of the last policy selected. +// A VerbosePolicy is a group of related policies pretending to all be the same +// policy. Which policy is used is determined by what the previous policy for +// the card was (as reported via a call to LastEnacted): +// +// * If no policy has yet been enacted, use FirstTime. +// * If a policy has been enacted, use the Policy at the slot in Variants +// that corresponds to the slot (on the Card) of the currently-enacted policy. +// * If the policy retrieved in this way returns ErrUnimplemented, throw away +// its response and use Default instead. For Enabled, which does not have +// an error component to its return value, look for ErrUnimplemented as the +// argument to a Panic call, instead. +// * If the policy retrieved in this way returns ErrKeepMessage when Enact +// is called, it calls Default for the side effects but ignores its message, +// retaining the message from the original call. This is to avoid having to +// repeat the same Enact function except with different text each time. +// OptionText does this too, even though OptionText doesn't have side effects, +// so the same helper function can create a "constant message" callback +// that works the same for both fields so someone implementing a card won't +// accidentally fail to enact their policy's effects by using the wrong one +// in the wrong slot. type VerbosePolicy struct { - *BasicPolicy - lastIdx int - Content []DescResult + Default Policy + FirstTime Policy + lastIdx int + lastWasMe bool + lastEnacted Policy + Variants []Policy } func (v *VerbosePolicy) LastEnacted(i int, p Policy) { v.lastIdx = i - v.BasicPolicy.currentlyEnacted = v.Is(p) -} + v.lastEnacted = p + v.lastWasMe = v.Is(p) -func (v *VerbosePolicy) OptionText(*Player) (cardsim.Message, error) { - if v.lastIdx < 0 { - return v.BasicPolicy.UnenactedDesc, nil + // make sure we can just assume that there is a policy in this slot, + // inserting the default if there is none. + v.fillDefaults() + + // Tell the potential candidate policy about this, too. Two special cases: + // * first time -- use first-time policy + // * lastWasMe -- tell the polcy that the last encated policy was itself, + // since it doesn't know it's wrapped in another policy and would not + // recognize itself as v + if i < 0 { + v.FirstTime.LastEnacted(i, p) // p should be nil here... + } else if v.lastWasMe { + v.Variants[i].LastEnacted(i, v.Variants[i]) + } else { + v.Variants[i].LastEnacted(i, p) + } + + // In case we need it, also prepare the Default for use. + if v.lastWasMe { + v.Default.LastEnacted(i, v.Default) + } else { + v.Default.LastEnacted(i, p) } - return v.Content[v.lastIdx].Desc, nil } -func (v *VerbosePolicy) Enact(p *Player) (cardsim.Message, error) { - msg, err := v.BasicPolicy.Enact(p) - if v.lastIdx >= 0 { - msg = v.Content[v.lastIdx].Result +func (v *VerbosePolicy) fillDefaults() { + if v.FirstTime == nil { + v.FirstTime = v.Default + } + for len(v.Variants) <= v.lastIdx { + v.Variants = append(v.Variants, v.Default) + } + if v.Variants[v.lastIdx] == nil { + v.Variants[v.lastIdx] = v.Default + } +} + +func (v *VerbosePolicy) OptionText(p *Player) (cardsim.Message, error) { + var msg cardsim.Message + var err error + if v.lastIdx < 0 { + msg, err = v.FirstTime.OptionText(p) + } else { + msg, err = v.Variants[v.lastIdx].OptionText(p) + } + if errors.Is(err, ErrUnimplemented) { + msg, err = v.Default.OptionText(p) } return msg, err } +func (v *VerbosePolicy) Enact(p *Player) (cardsim.Message, error) { + var msg cardsim.Message + var err error + if v.lastIdx < 0 { + msg, err = v.FirstTime.Enact(p) + } else { + msg, err = v.Variants[v.lastIdx].Enact(p) + } + if errors.Is(err, ErrUnimplemented) { + msg, err = v.Default.Enact() + } + return msg, err +} + +func (v *VerbosePolicy) Unenact(p *Player) error { + if !v.lastWasMe { + return ErrPolicyNotEnacted + } + var err error + if v.lastIdx < 0 { + err = v.FirstTime.Unenact(p) + } else { + err = v.Variants[v.lastIdx].Unenact(p) + } + if errors.Is(err, ErrUnimplemented) { + err = v.Default.Unenact(p) + } + return err +} + func (v *VerbosePolicy) Is(p Policy) bool { if o, ok := p.(*VerbosePolicy); ok { - return o == p + return o == v } return false } +func (v *VerbosePolicy) Enabled(p *Player) (result bool) { + // oops, enablement isn't designed to error out. so we have to use + // panic/recover for this. + defer func() { + if x := recover(); x != nil { + if e, ok := x.(error); ok { + if errors.Is(e, ErrUnimplemented) { + // Recover and use the Default to cover for the missing + // Enabled method. + result = v.Default.Enabled(p) + return + } + } + // Whatever we caught, it's not something we're actually ready to recover from. + panic(x) + } + }() + + if v.lastIdx < 0 { + result = v.FirstTime.Enabled(p) + return + } + result = v.Variants[v.lastIdx].Enabled(p) + return +} + // ShuffleIntoBottomHalf is a common "what to do with the card after?" behavior. func ShuffleIntoBottomHalf(c Card, p *Player, _ CardOption) error { p.Deck.InsertRandomBottom(0.5, c)