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 contains the name of the card, displayed as its title in the // action selection menu and in the card detail page itself. Name cardsim.Message // Desc contains the event description for the card, displayed in the // card detail page. Desc cardsim.Message // IsUrgent marks a card as urgent. If the player has any urgent cards // in hand, they cannot act on any non-urgent cards (or permanent actions // not marked as urgent). IsUrgent bool // After is invoked after the player has chosen to act on this card and // the chosen option has been fully enacted. If the card should be returned // to the deck, After is responsible for doing this! // // If After is not provided, ShuffleIntoBottomHalf is used as a fallback. // // The first argument to After is the card itself. This will be type // *SwitchingCard. It's represented as Card so general "After" functions // that can be used with multiple card types (for example, // ShuffleIntoBottomHalf) can be trivially implemented. // // If the card cannot be drawn into the hand because it has an IsValid // check and the check fails, After is invoked with a nil CardOption. // ShuffleIntoBottomHalf works just fine with a nil argument here. // (It doesn't care about the CardOption at all.) After func(Card, *Player, CardOption) error // IsValid is used to check whether the card can be drawn into the hand. // If it cannot, After is immediately invoked (whenever the card was // being drawn, which is probably the "draw" stage of the turn but can // happen any time if something else causes the player to draw cards) with // a nil CardOption (because no option was selected) and the SwitchingCard // does not invoke any option and does not change its active policy. // // The first argument to IsValid is the card itself. This will be type // *SwitchingCard. It's presented via the Card interface to support // general validity functions that could be used with arbitrary kinds of Card. IsValid func(Card, *Player) bool // Policies contains the options the player may choose between. Policy is // a more specific type than CardOption; a Policy can be un-enacted. // Unenactment of the previous policy before selecting a new one is the // core feature of SwitchingCard. Policies []Policy // lastPolicy stores the last policy selected for this card. It's used // extensively by SwitchingCard's logic. 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(p *Player) bool { if s.IsValid != nil && !s.IsValid(s, p) { err := s.Then(p, nil) p.ReportError(err) // can't do anything with the error right now return false } 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() } // CurrentlyEnacted returns the currently enacted Policy, if any. func (s *SwitchingCard) CurrentlyEnacted() Policy { return s.lastPolicy } // 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 } // TablePolicy is a Policy where all numerical changes are defined by // adding a constant to some set of fields (defined by `EffectsTable“) // and subtracting it back out when de-enacting. If the currently // enacted option is re-enacted, it refunds the player's action point. type TablePolicy struct { Desc cardsim.Message UnenactedDesc cardsim.Message EnactedDesc cardsim.Message NothingChanged cardsim.Message EffectsTable map[FieldLabel]float64 EnactionDesc cardsim.Message CanDo func(*TablePolicy, *Player) bool CurrentlyEnacted bool LastEnactedPolicy Policy LastEnactedIdx int } func YesWeAlsoCan(*TablePolicy, *Player) bool { return true } // LastEnacted notifies t about the last-enacted policy in its group. It updates // t.currentlyEnacted accordingly. func (t *TablePolicy) LastEnacted(i int, p Policy) { t.LastEnactedPolicy = p t.LastEnactedIdx = i t.CurrentlyEnacted = t.Is(p) } // OptionText implements CardOption. func (t *TablePolicy) OptionText(*Player) (cardsim.Message, error) { if t.CurrentlyEnacted { if t.EnactedDesc == nil { return nil, ErrUnimplemented } return t.EnactedDesc, nil } if t.UnenactedDesc == nil { return nil, ErrUnimplemented } return t.UnenactedDesc, nil } // Enact implements CardOption. func (t *TablePolicy) Enact(p *Player) (cardsim.Message, error) { if t.EffectsTable == nil { return nil, ErrUnimplemented } if t.CurrentlyEnacted { p.ActionsRemaining++ if t.NothingChanged == nil { t.NothingChanged = cardsim.MsgStr("You continue your current approach.") } return t.NothingChanged, nil } var errs cardsim.ErrorCollector for label, amount := range t.EffectsTable { errs.Add(p.Stats.Add(label, amount)) } return t.EnactionDesc, errs.Emit() } // Unenact implements Policy. func (t *TablePolicy) Unenact(p *Player) error { if !t.CurrentlyEnacted { return ErrPolicyNotEnacted } var errs cardsim.ErrorCollector for label, amount := range t.EffectsTable { errs.Add(p.Stats.Add(label, -amount)) } return errs.Emit() } // Enabled implements CardOption. func (t *TablePolicy) Enabled(p *Player) bool { if t.CurrentlyEnacted { return true } if t.CanDo == nil { panic(ErrUnimplemented) } return t.CanDo(t, p) } func (t *TablePolicy) Is(p Policy) bool { if o, ok := p.(*TablePolicy); ok { return o == t } 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 } }