CardSimEngine/cardsim/rules.go

327 lines
8.7 KiB
Go
Raw Normal View History

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")
)
// 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
}
// 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}
r.rules[id] = k
s := r.byStep[k.Step()]
if s == nil {
r.steps = nil
}
s = append(s, id)
r.byStep[k.Step()] = s
lbl := r.byLabel[k.Label()]
lbl = append(lbl, id)
r.byLabel[k.Label()] = lbl
return id
}
// 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)
}
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]
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)
}
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
}
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 {
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()
}