445 lines
12 KiB
Go
445 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
|
|
}
|
|
|
|
// 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) ([]CardOption, error) {
|
|
lastIdx := -1
|
|
for i, p := range s.Policies {
|
|
if p.Is(s.lastPolicy) {
|
|
lastIdx = i
|
|
break
|
|
}
|
|
}
|
|
ret := make([]CardOption, len(s.Policies))
|
|
for i, p := range s.Policies {
|
|
p.LastEnacted(lastIdx, s.lastPolicy)
|
|
ret[i] = p
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
// Then implements Card.
|
|
func (s *SwitchingCard) Then(p *Player, o CardOption) error {
|
|
newPolicy := o.(Policy)
|
|
var errs cardsim.ErrorCollector
|
|
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))
|
|
}
|
|
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(*Player) bool
|
|
|
|
currentlyEnacted bool
|
|
}
|
|
|
|
// YesWeCan returns true. It's the default value for BasicPolicy.CanDo / BasicPolicy.CanUndo.
|
|
func YesWeCan(*Player) bool {
|
|
return true
|
|
}
|
|
|
|
// LastEnacted notifies b about the last-enacted policy in its group. It updates
|
|
// b.currentlyEnacted accordingly.
|
|
func (b *BasicPolicy) LastEnacted(_ int, p Policy) {
|
|
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
|
|
}
|
|
if b.Enabled(p) {
|
|
return b.Do(p)
|
|
}
|
|
return nil, ErrOptionNotEnabled
|
|
}
|
|
|
|
// 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(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.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
|
|
}
|
|
}
|