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") ) 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 !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 { 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 and returns itself. // // It also installs placeholders if CanDo or CanUndo is unspecified. func (b *BasicPolicy) LastEnacted(_ int, p Policy) { b.currentlyEnacted = false if o, ok := p.(*BasicPolicy); ok && o == b { b.currentlyEnacted = true } if b.CanDo == nil { b.CanDo = YesWeCan } } // OptionText implements CardOption. func (b *BasicPolicy) OptionText(*Player) (cardsim.Message, error) { if b.currentlyEnacted { return b.EnactedDesc, nil } return b.UnenactedDesc, nil } // Enact implements CardOption. func (b *BasicPolicy) Enact(p *Player) (cardsim.Message, error) { if b.currentlyEnacted { p.ActionsRemaining++ if b.NothingChanged == nil { b.NothingChanged = cardsim.MsgStr("You continue your current approach.") } return b.NothingChanged, nil } if !b.CanDo(p) { return nil, ErrOptionNotEnabled } return b.Do(p) } // Unenact implements Policy. func (b *BasicPolicy) Unenact(p *Player) error { if !b.currentlyEnacted { return ErrPolicyNotEnacted } return b.Undo(p) } // Enabled implements CardOption. func (b *BasicPolicy) Enabled(p *Player) bool { if b.currentlyEnacted { return true } if b.CanDo == nil { b.CanDo = YesWeCan } return b.CanDo(p) } func (b *BasicPolicy) Is(p Policy) bool { if o, ok := p.(*BasicPolicy); ok { return o == b } 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. type VerbosePolicy struct { *BasicPolicy lastIdx int Content []DescResult } func (v *VerbosePolicy) LastEnacted(i int, p Policy) { v.lastIdx = i v.BasicPolicy.LastEnacted(i, p) } func (v *VerbosePolicy) OptionText(*Player) (cardsim.Message, error) { if v.lastIdx < 0 { return v.BasicPolicy.UnenactedDesc, nil } 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 } return msg, err } func (v *VerbosePolicy) Is(p Policy) bool { if o, ok := p.(*VerbosePolicy); ok { return o == p } return false } // 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 }