CardSimEngine/cardsim/rules.go

418 lines
12 KiB
Go

package cardsim
import (
"errors"
"fmt"
"sort"
"github.com/kr/pretty"
)
// A Rule implements an operation run on every game turn.
//
// Rule[C] is a generic interface. Like any other generic type, it describes a
// family of related types: each different kind of StatsCollection that Rule
// could pertain to is the basis of a distinct type of Rule.
//
// When implementing a generic interface, you do not need to implement a
// generic type. In the case of Rule, you are likely to be writing rules for a
// specific simulation. That simulation will have some associated
// StatsCollection type. The rules you write will only need to implement the
// variation of Rule that pertains specifically to that type.
//
// For example, if your `StatsCollection` type is `KoboldMineData`, then rules
// for the simulation referring to it would implement `Rule[KoboldMineData]`
// only. So the `Enact` function you implment would take an argument of type
// `*Player[KoboldMineData]`, not some undefined type `C` that could be any
// StatsCollection. Since it takes a `*Player[KoboldMineData]` as an argument,
// you then know that the player's `Stats` field is not just any
// StatsCollection, it is KoboldMineData specifically. The compiler won't
// require you to convert from "some `StatsCollection`" to "`KoboldMineData`
// specifically" when using the `Player[KoboldMineData].Stats` field,
// because the type of that field is already `KoboldMineData`.
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 len(s) == 0 {
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
}
p.Debug(2, Msgf("Executing steps: %v", steps))
var errs ErrorCollector
for _, step := range steps {
stepRules := r.byStep[step]
p.Debug(3, Msgf("Executing step %d; length %d", step, len(stepRules)))
ShuffleAll(stepRules, p.Rand)
var remove []RuleID
halt := false
for _, id := range stepRules {
rule := r.rules[id]
p.Debug(4, Msgf("Executing rule %x (labeled %q)", id, rule.Label()))
err := rule.Enact(p)
if err != nil {
p.Debug(2, Msgf("Rule %x (%q): error: %v", id, rule.Label(), err))
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 {
ret := errs.Emit()
p.Debug(2, Msgf("Rules stopping early. Result: %v", ret))
return ret
}
}
ret := errs.Emit()
p.Debug(2, Msgf("Rules complete. Result: %v", ret))
return ret
}
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)
}
}
// RuleDumper is an InfoPanel[C] that dumps all rules in P.
type RuleDumper[C StatsCollection] struct{}
func (RuleDumper[C]) Title(p *Player[C]) Message {
return MsgStr("Rule Dumper")
}
func (RuleDumper[C]) Info(p *Player[C]) ([]Message, error) {
return []Message{Msgf("%# v", pretty.Formatter(p.Rules))}, nil
}