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:
		| @@ -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) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user