Kistaro Windrider
8a2664c305
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.
377 lines
9.8 KiB
Go
377 lines
9.8 KiB
Go
package cardsim
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
)
|
|
|
|
// A Rule implements an operation run on every game turn.
|
|
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 s == nil {
|
|
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
|
|
}
|
|
|
|
var errs ErrorCollector
|
|
for _, step := range steps {
|
|
stepRules := r.byStep[step]
|
|
p.Rand.Shuffle(len(stepRules), func(i, j int) {
|
|
stepRules[i], stepRules[j] = stepRules[j], stepRules[i]
|
|
})
|
|
var remove []RuleID
|
|
halt := false
|
|
for _, id := range stepRules {
|
|
rule := r.rules[id]
|
|
err := rule.Enact(p)
|
|
if err != nil {
|
|
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 {
|
|
return errs.Emit()
|
|
}
|
|
}
|
|
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)
|
|
}
|
|
}
|