Refactor VerbosePolicy.
This allows it to switch between other complete policies, with fallbacks to a default if parts aren't impelemented. Complementing it is the change to BasicPolicy, which throws ErrUnimplemented if fields are missing, which VerbosePolicy uses as a "go ask the default" sign.
This commit is contained in:
parent
8ce6539c8a
commit
a1f55c865d
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user