package cardsim import ( "errors" "fmt" "sort" "github.com/kr/pretty" ) // A Rule implements an operation run on every game turn. // // Rule[C] is a generic interface. Like any other generic type, it describes a // family of related types: each different kind of StatsCollection that Rule // could pertain to is the basis of a distinct type of Rule. // // When implementing a generic interface, you do not need to implement a // generic type. In the case of Rule, you are likely to be writing rules for a // specific simulation. That simulation will have some associated // StatsCollection type. The rules you write will only need to implement the // variation of Rule that pertains specifically to that type. // // For example, if your `StatsCollection` type is `KoboldMineData`, then rules // for the simulation referring to it would implement `Rule[KoboldMineData]` // only. So the `Enact` function you implment would take an argument of type // `*Player[KoboldMineData]`, not some undefined type `C` that could be any // StatsCollection. Since it takes a `*Player[KoboldMineData]` as an argument, // you then know that the player's `Stats` field is not just any // StatsCollection, it is KoboldMineData specifically. The compiler won't // require you to convert from "some `StatsCollection`" to "`KoboldMineData` // specifically" when using the `Player[KoboldMineData].Stats` field, // because the type of that field is already `KoboldMineData`. type Rule[C StatsCollection] interface { // Label is an internal name the rule can be recognized by. // Some things may be easier if it is unique, but it does not have to be. // Label must be constant for the life of the Rule, including after the // Rule is removed from the collection (it is called as part of the // removal process). Label() string // Step returns which numerical step this rule should be executed on. // It is somewhat arbitrary. Rules are enacted in order from lowest step // to highest step; ties are broken randomly every turn. Step must be // constant for the life of the Rule, including after the Rule is removed // from the collection (it is called during removal). Step() int // Enact operates the rule. Some special errors are recognized and change // the flow of the turn: // // * ErrHaltTurn skips all further rules (including rules on the same step) // and immediately proceeds to the next turn. // * ErrDeleteRule removes the rule from the collection. It will not be // executed on future turns unless it is reinserted into the active // ruleset (by some other rule, option, etc.) // * ErrHaltDelete acts like both ErrHaltTurn and ErrDeleteRule. // * Any Warning is collected and displayed at the end of the turn. // * Any other error crashes the game. Enact(*Player[C]) error } var ( ErrHaltTurn = errors.New("turn halted") ErrDeleteRule = errors.New("delete this rule") ErrHaltDelete = fmt.Errorf("%w; %w", ErrHaltTurn, ErrDeleteRule) // ErrNotUnique matches errors from functions attempting to identify // a single rule by an identifier that is not unique. ErrNotUnique = errors.New("not unique") // ErrAlreadyRunning means you tried to run the rules engine from inside a rule. ErrAlreadyRunning = errors.New("cannot run a turn while running a turn") ) // RuleFunc implements a Rule represented by a single function. It is the most // common type of Rule. (You'll probably only want to implement a full Rule // type if the rule needs to have fields of its own or be identifiable by // something other than its Name.) type RuleFunc[C StatsCollection] struct { Name string Seq int F func(*Player[C]) error } func (r *RuleFunc[C]) Label() string { return r.Name } func (r *RuleFunc[C]) Step() int { return r.Seq } func (r *RuleFunc[C]) Enact(p *Player[C]) error { return r.F(p) } // RuleID is a unique opaque ID for a rule inserted in a collection. type RuleID uint64 // keyedRule attaches a unique ID to each Rule. type keyedRule[C StatsCollection] struct { id RuleID Rule[C] } // RuleCollection stores, organizes, and runs all rules currently active for a Player. type RuleCollection[C StatsCollection] struct { rules map[RuleID]*keyedRule[C] byStep map[int][]RuleID byLabel map[string][]RuleID nextID RuleID steps []int rulesRunning bool insertLater []*keyedRule[C] deleteLater []RuleID } // NewRuleCollection initializes an empty RuleCollection. func NewRuleCollection[C StatsCollection]() *RuleCollection[C] { return &RuleCollection[C]{ rules: make(map[RuleID]*keyedRule[C]), byStep: make(map[int][]RuleID), byLabel: make(map[string][]RuleID), nextID: 1, } } // Insert adds a Rule to a RuleCollection, returning the ID it assigned to the rule. func (r *RuleCollection[C]) Insert(rule Rule[C]) RuleID { id := r.nextID r.nextID++ k := &keyedRule[C]{id, rule} if r.rulesRunning { r.insertLater = append(r.insertLater, k) return id } r.performInsert(k) return id } func (r *RuleCollection[C]) performInsert(k *keyedRule[C]) { r.rules[k.id] = k s := r.byStep[k.Step()] if len(s) == 0 { r.steps = nil } s = append(s, k.id) r.byStep[k.Step()] = s lbl := r.byLabel[k.Label()] lbl = append(lbl, k.id) r.byLabel[k.Label()] = lbl } // RemoveID removes the rule with the given ID from the collection, if present. // It returns whether it found and removed anything. func (r *RuleCollection[C]) RemoveID(id RuleID) bool { rule := r.rules[id] if rule == nil { return false } if rule.id != id { panic(fmt.Errorf("rule stored in slot %d had internal ID %d", id, rule.id)) } delete(r.rules, id) r.removeFromLabel(rule) r.removeFromStep(rule) return true } // removeFromStep removes a specific ID from r.byStep, if present. // It's fine for this rule (or this entire step) to be gone already. func (r *RuleCollection[C]) removeFromStep(rule *keyedRule[C]) { s := r.byStep[rule.Step()] for i, v := range s { if v == rule.id { // special case: only item? if len(s) == 1 { s = nil delete(r.byStep, rule.Step()) r.steps = nil break } end := len(s) - 1 if i != end { s[i], s[end] = s[end], s[i] } s = s[:end] break } } if s != nil { r.byStep[rule.Step()] = s } } // removeFromLabel removes a specific ID from r.byLabel, if present. // It's fine for this rule (or this entire label) to be gone already. func (r *RuleCollection[C]) removeFromLabel(rule *keyedRule[C]) { lbl := r.byLabel[rule.Label()] for i, v := range lbl { if v == rule.id { // special case: only item? if len(lbl) == 1 { lbl = nil delete(r.byLabel, rule.Label()) break } end := len(lbl) - 1 if i != end { lbl[i], lbl[end] = lbl[end], lbl[i] } lbl = lbl[:end] break } } if lbl != nil { r.byLabel[rule.Label()] = lbl } } // RemoveUniqueLabel removes the sole rule with a specific label. If there are // no rules with that label, this returns false and nil. If there is exactly // one rule with that label, it is removed and this returns true and nil. // If there are multiple rules with this label, this returns false and // an error like ErrNotUnique. func (r *RuleCollection[C]) RemoveUniqueLabel(label string) (bool, error) { target := r.byLabel[label] if len(target) == 0 { return false, nil } if len(target) != 1 { return false, fmt.Errorf("%w: %q", ErrNotUnique, label) } if r.rulesRunning { r.deleteLater = append(r.deleteLater, target[0]) return true, nil } id := target[0] rule := r.rules[id] delete(r.rules, id) delete(r.byLabel, label) r.removeFromStep(rule) return true, nil } // RemoveAllLabel removes every rule with a specific label. It returns how many // rules were thus removed. func (r *RuleCollection[C]) RemoveAllLabel(label string) int { target := r.byLabel[label] if r.rulesRunning { r.deleteLater = append(r.deleteLater, target...) return len(target) } delete(r.byLabel, label) for _, t := range target { // RemoveID works fine when the label is already completely gone; it // calls into removeFromLabel, which is a no-op in this case. r.RemoveID(t) } return len(target) } // RemoveUniqueStep removes the sole rule on a specific step. Its semantics // are equivalent to RemoveUniqueLabel. func (r *RuleCollection[C]) RemoveUniqueStep(step int) (bool, error) { target := r.byStep[step] if len(target) == 0 { return false, nil } if len(target) != 1 { return false, fmt.Errorf("%w: %d", ErrNotUnique, step) } if r.rulesRunning { r.deleteLater = append(r.deleteLater, target[0]) return true, nil } id := target[0] rule := r.rules[id] delete(r.rules, id) delete(r.byStep, step) r.removeFromLabel(rule) return true, nil } // RemoveAllStep removes every rule on a specific step. It returns how many // rules were thus removed. func (r *RuleCollection[C]) RemoveAllStep(step int) int { target := r.byStep[step] if target == nil { return 0 } if r.rulesRunning { r.deleteLater = append(r.deleteLater, target...) return len(target) } delete(r.byStep, step) r.steps = nil for _, t := range target { // RemoveID works fine when the label is already completely gone; it // calls into removeFromLabel, which is a no-op in this case. r.RemoveID(t) } return len(target) } // Run runs all rules in the collection against the specified Player, in // step order, with ties broken randomly. // // It stops early if it observes a failure other than the errors described in // Rule.Enact as having special meaning to the rules engine. func (r *RuleCollection[C]) Run(p *Player[C]) error { if r.rulesRunning { return ErrAlreadyRunning } r.rulesRunning = true defer r.applyDelayedUpdates() defer func() { r.rulesRunning = false }() steps := r.steps if steps == nil { // Step set changed, recalculate. steps = make([]int, 0, len(r.byStep)) for step := range r.byStep { steps = append(steps, step) } sort.Ints(steps) r.steps = steps } p.Debug(2, Msgf("Executing steps: %v", steps)) var errs ErrorCollector for _, step := range steps { stepRules := r.byStep[step] p.Debug(3, Msgf("Executing step %d; length %d", step, len(stepRules))) ShuffleAll(stepRules, p.Rand) var remove []RuleID halt := false for _, id := range stepRules { rule := r.rules[id] p.Debug(4, Msgf("Executing rule %x (labeled %q)", id, rule.Label())) err := rule.Enact(p) if err != nil { p.Debug(2, Msgf("Rule %x (%q): error: %v", id, rule.Label(), err)) ignore := false if errors.Is(err, ErrDeleteRule) { remove = append(remove, id) ignore = true } if errors.Is(err, ErrHaltTurn) { halt = true ignore = true } if !ignore { errs.Add(err) if !errors.Is(err, AnyWarning) { halt = true } } } if halt { break } } // Remove rules flagged for removal. We're done iterating this set of // steps so removing the rule from the step collection won't mess up // iteration, and we're iterating off a local reference to the list of // steps so we also won't mess up iteration if we drop the list because // we removed the last rule from this step. for _, id := range remove { if !r.RemoveID(id) { panic(fmt.Errorf("can't find rule %d after it asked to remove itself", id)) } } if halt { ret := errs.Emit() p.Debug(2, Msgf("Rules stopping early. Result: %v", ret)) return ret } } ret := errs.Emit() p.Debug(2, Msgf("Rules complete. Result: %v", ret)) return ret } func (r *RuleCollection[C]) applyDelayedUpdates() { insert := r.insertLater r.insertLater = nil remove := r.deleteLater r.deleteLater = nil for _, k := range insert { r.performInsert(k) } for _, id := range remove { r.RemoveID(id) } } // RuleDumper is an InfoPanel[C] that dumps all rules in P. type RuleDumper[C StatsCollection] struct{} func (RuleDumper[C]) Title(p *Player[C]) Message { return MsgStr("Rule Dumper") } func (RuleDumper[C]) Info(p *Player[C]) ([]Message, error) { return []Message{Msgf("%# v", pretty.Formatter(p.Rules))}, nil }