From 8a2664c305ebc2c7a0fe5c272994441560cc83fb Mon Sep 17 00:00:00 2001 From: Kistaro Windrider Date: Mon, 27 Mar 2023 00:08:56 -0700 Subject: [PATCH] Delay rules updates during rule execution. Any RuleCollection change while the rule collection is running a turn is now delayed until all rules are evaluated. This gives consistent semantics for when rule changes invoked my rules themselves are applied. --- cardsim/rules.go | 60 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 5 deletions(-) diff --git a/cardsim/rules.go b/cardsim/rules.go index 6e0af2a..9b9953a 100644 --- a/cardsim/rules.go +++ b/cardsim/rules.go @@ -44,6 +44,9 @@ var ( // 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 @@ -84,6 +87,10 @@ type RuleCollection[C StatsCollection] struct { byLabel map[string][]RuleID nextID RuleID steps []int + + rulesRunning bool + insertLater []*keyedRule[C] + deleteLater []RuleID } // NewRuleCollection initializes an empty RuleCollection. @@ -101,20 +108,27 @@ func (r *RuleCollection[C]) Insert(rule Rule[C]) RuleID { id := r.nextID r.nextID++ k := &keyedRule[C]{id, rule} - r.rules[id] = k + 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 s == nil { r.steps = nil } - s = append(s, id) + s = append(s, k.id) r.byStep[k.Step()] = s lbl := r.byLabel[k.Label()] - lbl = append(lbl, id) + lbl = append(lbl, k.id) r.byLabel[k.Label()] = lbl - - return id } // RemoveID removes the rule with the given ID from the collection, if present. @@ -199,6 +213,10 @@ func (r *RuleCollection[C]) RemoveUniqueLabel(label string) (bool, error) { 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] @@ -213,6 +231,10 @@ func (r *RuleCollection[C]) RemoveUniqueLabel(label string) (bool, error) { // 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 @@ -232,6 +254,10 @@ func (r *RuleCollection[C]) RemoveUniqueStep(step int) (bool, error) { 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] @@ -249,6 +275,10 @@ func (r *RuleCollection[C]) RemoveAllStep(step int) int { 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 { @@ -265,6 +295,13 @@ func (r *RuleCollection[C]) RemoveAllStep(step int) int { // 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. @@ -324,3 +361,16 @@ func (r *RuleCollection[C]) Run(p *Player[C]) error { } return errs.Emit() } + +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) + } +}