327 lines
8.7 KiB
Go
327 lines
8.7 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")
|
||
|
)
|
||
|
|
||
|
// 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()
|
||
|
}
|