Kistaro Windrider
d434e50897
If no After function is provided for a SwitchingCard, it uses ShuffleIntoBottomHalf. This also modifies Then to be callable with a nil `o`, which it recognizes as "nothing happened but we need cleanup anyway", in preparation for the incoming "make cards sometimes not drawable" feature.
459 lines
12 KiB
Go
459 lines
12 KiB
Go
package koboldsim
|
|
|
|
import (
|
|
"errors"
|
|
|
|
"git.chromaticdragon.app/kistaro/CardSimEngine/cardsim"
|
|
)
|
|
|
|
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")
|
|
ErrKeepMessage = errors.New("use the default behavior but this message")
|
|
)
|
|
|
|
type Policy interface {
|
|
cardsim.CardOption[*KoboldMine]
|
|
// Unenact reverses the previous enactment of this policy.
|
|
Unenact(p *Player) error
|
|
|
|
// LastEnacted informs this Policy which choice was last enacted and
|
|
// which index it is under; this allows it to prepare to describe
|
|
// itself differently depending on the last selected policy. If no
|
|
// option has ever been chosen for this card, the index is -1.
|
|
//
|
|
// The Card that would present this Policy as an option must use this,
|
|
// and provide the active policy that this is a candidate to replace,
|
|
LastEnacted(int, Policy)
|
|
|
|
// Is returns whether this policy is this other policy. This is a strict
|
|
// identity equality check, don't do anything clever here.
|
|
Is(Policy) bool
|
|
}
|
|
|
|
// A SwitchingCard is an issue card that remembers which option was selected
|
|
// previously, and undoes it before doing a different one. It is always valid
|
|
// to draw.
|
|
type SwitchingCard struct {
|
|
Name cardsim.Message
|
|
Desc cardsim.Message
|
|
IsUrgent bool
|
|
After func(Card, *Player, CardOption) error
|
|
Policies []Policy
|
|
lastPolicy Policy
|
|
|
|
// ShowUnavailable controls whether options for which Enabled() = false
|
|
// should be presented to the player at all. Indexes for "last enacted"
|
|
// still refer to the original list, not the shortened one.
|
|
ShowUnavailable bool
|
|
}
|
|
|
|
// Title implements Card.
|
|
func (s *SwitchingCard) Title(*Player) cardsim.Message {
|
|
return s.Name
|
|
}
|
|
|
|
// Urgent implements Card.
|
|
func (s *SwitchingCard) Urgent(*Player) bool {
|
|
return s.IsUrgent
|
|
}
|
|
|
|
// Drawn implements Card.
|
|
func (s *SwitchingCard) Drawn(*Player) bool {
|
|
return true
|
|
}
|
|
|
|
// EventText implements Card.
|
|
func (s *SwitchingCard) EventText(*Player) (cardsim.Message, error) {
|
|
return s.Desc, nil
|
|
}
|
|
|
|
// Options implements Card.
|
|
func (s *SwitchingCard) Options(player *Player) ([]CardOption, error) {
|
|
lastIdx := -1
|
|
for i, p := range s.Policies {
|
|
if p.Is(s.lastPolicy) {
|
|
lastIdx = i
|
|
break
|
|
}
|
|
}
|
|
ret := make([]CardOption, 0, len(s.Policies))
|
|
for _, p := range s.Policies {
|
|
p.LastEnacted(lastIdx, s.lastPolicy)
|
|
if s.ShowUnavailable || p.Enabled(player) {
|
|
ret = append(ret, p)
|
|
}
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
// Then implements Card.
|
|
func (s *SwitchingCard) Then(p *Player, o CardOption) error {
|
|
var errs cardsim.ErrorCollector
|
|
if o != nil {
|
|
newPolicy := o.(Policy)
|
|
if s.lastPolicy != nil && !newPolicy.Is(s.lastPolicy) {
|
|
err := s.lastPolicy.Unenact(p)
|
|
if cardsim.IsSeriousError(err) {
|
|
return err
|
|
}
|
|
errs.Add(err)
|
|
}
|
|
s.lastPolicy = o.(Policy)
|
|
}
|
|
if s.After != nil {
|
|
errs.Add(s.After(s, p, o))
|
|
} else {
|
|
// Fallback: Shuffle the card back into the bottom half of the deck.
|
|
errs.Add(ShuffleIntoBottomHalf(s, p, o))
|
|
|
|
}
|
|
return errs.Emit()
|
|
}
|
|
|
|
// 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
|
|
Do func(*Player) (cardsim.Message, error)
|
|
Undo func(*Player) error
|
|
CanDo func(*BasicPolicy, *Player) bool
|
|
|
|
CurrentlyEnacted bool
|
|
LastEnactedPolicy Policy
|
|
LastEnactedIdx int
|
|
}
|
|
|
|
// YesWeCan returns true. It's the default value for BasicPolicy.CanDo / BasicPolicy.CanUndo.
|
|
func YesWeCan(*BasicPolicy, *Player) bool {
|
|
return true
|
|
}
|
|
|
|
// LastEnacted notifies b about the last-enacted policy in its group. It updates
|
|
// b.currentlyEnacted accordingly.
|
|
func (b *BasicPolicy) LastEnacted(i int, p Policy) {
|
|
b.LastEnactedPolicy = p
|
|
b.LastEnactedIdx = i
|
|
b.CurrentlyEnacted = b.Is(p)
|
|
}
|
|
|
|
// 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 {
|
|
b.NothingChanged = cardsim.MsgStr("You continue your current approach.")
|
|
}
|
|
return b.NothingChanged, nil
|
|
}
|
|
return b.Do(p)
|
|
}
|
|
|
|
// Unenact implements Policy.
|
|
func (b *BasicPolicy) Unenact(p *Player) error {
|
|
if !b.CurrentlyEnacted {
|
|
return ErrPolicyNotEnacted
|
|
}
|
|
if b.Undo == nil {
|
|
return ErrUnimplemented
|
|
}
|
|
return b.Undo(p)
|
|
}
|
|
|
|
// Enabled implements CardOption.
|
|
func (b *BasicPolicy) Enabled(p *Player) bool {
|
|
if b.CurrentlyEnacted {
|
|
return true
|
|
}
|
|
if b.CanDo == nil {
|
|
panic(ErrUnimplemented)
|
|
}
|
|
return b.CanDo(b, p)
|
|
}
|
|
|
|
func (b *BasicPolicy) Is(p Policy) bool {
|
|
if o, ok := p.(*BasicPolicy); ok {
|
|
return o == b
|
|
}
|
|
return false
|
|
}
|
|
|
|
// 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 {
|
|
Default Policy
|
|
FirstTime Policy
|
|
lastIdx int
|
|
lastWasMe bool
|
|
lastEnacted Policy
|
|
Variants []Policy
|
|
}
|
|
|
|
func (v *VerbosePolicy) LastEnacted(i int, p Policy) {
|
|
v.lastIdx = i
|
|
v.lastEnacted = p
|
|
v.lastWasMe = v.Is(p)
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
|
|
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.lastIdx >= 0 && 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, ErrKeepMessage) {
|
|
_, err = v.Default.OptionText(p)
|
|
} else 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, ErrKeepMessage) {
|
|
_, err = v.Default.Enact(p)
|
|
} else if errors.Is(err, ErrUnimplemented) {
|
|
msg, err = v.Default.Enact(p)
|
|
}
|
|
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 == 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
|
|
}
|
|
|
|
// FuncPolicy implements Policy by calling specified functions. If they're
|
|
// missing, it returns ErrUnimplemented. It handles Is itself. It also tracks
|
|
// LastEnacted data (last index, which policy, is policy self) itself.
|
|
type FuncPolicy struct {
|
|
OptionTextFunc func(*Player) (cardsim.Message, error)
|
|
EnactFunc func(*Player) (cardsim.Message, error)
|
|
EnabledFunc func(*Player) bool
|
|
UnenactFunc func(*Player) error
|
|
|
|
// These three fields are assigned by LastEnacted and typically should not
|
|
// be set in the initializer.
|
|
LastEnactedIdx int
|
|
LastEnactedPolicy Policy
|
|
WasEnactedLast bool
|
|
}
|
|
|
|
func (f *FuncPolicy) OptionText(p *Player) (cardsim.Message, error) {
|
|
if f.OptionTextFunc == nil {
|
|
return nil, ErrUnimplemented
|
|
}
|
|
return f.OptionTextFunc(p)
|
|
}
|
|
|
|
func (f *FuncPolicy) Enact(p *Player) (cardsim.Message, error) {
|
|
if f.EnactFunc == nil {
|
|
return nil, ErrUnimplemented
|
|
}
|
|
return f.EnactFunc(p)
|
|
}
|
|
|
|
func (f *FuncPolicy) Enabled(p *Player) bool {
|
|
if f.EnabledFunc == nil {
|
|
panic(ErrUnimplemented)
|
|
}
|
|
return f.EnabledFunc(p)
|
|
}
|
|
|
|
func (f *FuncPolicy) Unenact(p *Player) error {
|
|
if f.UnenactFunc == nil {
|
|
return ErrUnimplemented
|
|
}
|
|
return f.UnenactFunc(p)
|
|
}
|
|
|
|
func (f *FuncPolicy) LastEnacted(i int, p Policy) {
|
|
f.LastEnactedIdx = i
|
|
f.LastEnactedPolicy = p
|
|
f.WasEnactedLast = f.Is(p)
|
|
}
|
|
|
|
func (f *FuncPolicy) Is(p Policy) bool {
|
|
fp, ok := p.(*FuncPolicy)
|
|
return ok && (f == fp)
|
|
}
|
|
|
|
// A DisabledPolicy is never enabled.
|
|
type DisabledPolicy struct {
|
|
Msg cardsim.Message
|
|
}
|
|
|
|
func (d *DisabledPolicy) OptionText(p *Player) (cardsim.Message, error) {
|
|
return d.Msg, nil
|
|
}
|
|
|
|
func (d *DisabledPolicy) Enact(*Player) (cardsim.Message, error) {
|
|
return nil, ErrOptionNotEnabled
|
|
}
|
|
|
|
func (d *DisabledPolicy) Enabled(*Player) bool {
|
|
return false
|
|
}
|
|
|
|
func (d *DisabledPolicy) Unenact(*Player) error {
|
|
return ErrPolicyNotEnacted
|
|
}
|
|
|
|
func (d *DisabledPolicy) LastEnacted(int, Policy) {}
|
|
|
|
func (d *DisabledPolicy) Is(p Policy) bool {
|
|
dp, ok := p.(*DisabledPolicy)
|
|
return ok && (dp == d)
|
|
}
|
|
|
|
// 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)
|
|
return nil
|
|
}
|
|
|
|
// OverrideDefaultMsg returns a closure that returns the provided message and
|
|
// ErrKeepMessage. This can be used in a FuncPolicy to tell a VerbosePolicy
|
|
// to use its Default but keep the message written here, to avoid writing
|
|
// repetitive Enact funcs (and, for that matter, OptionText funcs, even though
|
|
// the underlying Default call should be unnecessary).
|
|
func OverrideMsg(m cardsim.Message) func(*Player) (cardsim.Message, error) {
|
|
return func(p *Player) (cardsim.Message, error) {
|
|
return m, ErrKeepMessage
|
|
}
|
|
}
|