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.
This commit is contained in:
Kistaro Windrider 2023-03-27 00:08:56 -07:00
parent 45bbfe4e8f
commit 8a2664c305
Signed by: kistaro
SSH Key Fingerprint: SHA256:TBE2ynfmJqsAf0CP6gsflA0q5X5wD5fVKWPsZ7eVUg8

View File

@ -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)
}
}