2023-03-27 06:40:44 +00:00
|
|
|
package cardsim
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"sort"
|
|
|
|
)
|
|
|
|
|
|
|
|
// A Rule implements an operation run on every game turn.
|
2023-04-02 00:56:31 +00:00
|
|
|
//
|
|
|
|
// 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`.
|
2023-03-27 06:40:44 +00:00
|
|
|
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")
|
2023-03-27 07:08:56 +00:00
|
|
|
|
|
|
|
// ErrAlreadyRunning means you tried to run the rules engine from inside a rule.
|
|
|
|
ErrAlreadyRunning = errors.New("cannot run a turn while running a turn")
|
2023-03-27 06:40:44 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// 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
|
2023-03-27 07:08:56 +00:00
|
|
|
|
|
|
|
rulesRunning bool
|
|
|
|
insertLater []*keyedRule[C]
|
|
|
|
deleteLater []RuleID
|
2023-03-27 06:40:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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}
|
2023-03-27 07:08:56 +00:00
|
|
|
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
|
2023-03-27 06:40:44 +00:00
|
|
|
|
|
|
|
s := r.byStep[k.Step()]
|
|
|
|
if s == nil {
|
|
|
|
r.steps = nil
|
|
|
|
}
|
2023-03-27 07:08:56 +00:00
|
|
|
s = append(s, k.id)
|
2023-03-27 06:40:44 +00:00
|
|
|
r.byStep[k.Step()] = s
|
|
|
|
|
|
|
|
lbl := r.byLabel[k.Label()]
|
2023-03-27 07:08:56 +00:00
|
|
|
lbl = append(lbl, k.id)
|
2023-03-27 06:40:44 +00:00
|
|
|
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)
|
|
|
|
}
|
2023-03-27 07:08:56 +00:00
|
|
|
if r.rulesRunning {
|
|
|
|
r.deleteLater = append(r.deleteLater, target[0])
|
|
|
|
return true, nil
|
|
|
|
}
|
2023-03-27 06:40:44 +00:00
|
|
|
|
|
|
|
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]
|
2023-03-27 07:08:56 +00:00
|
|
|
if r.rulesRunning {
|
|
|
|
r.deleteLater = append(r.deleteLater, target...)
|
|
|
|
return len(target)
|
|
|
|
}
|
2023-03-27 06:40:44 +00:00
|
|
|
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)
|
|
|
|
}
|
2023-03-27 07:08:56 +00:00
|
|
|
if r.rulesRunning {
|
|
|
|
r.deleteLater = append(r.deleteLater, target[0])
|
|
|
|
return true, nil
|
|
|
|
}
|
2023-03-27 06:40:44 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
2023-03-27 07:08:56 +00:00
|
|
|
if r.rulesRunning {
|
|
|
|
r.deleteLater = append(r.deleteLater, target...)
|
|
|
|
return len(target)
|
|
|
|
}
|
2023-03-27 06:40:44 +00:00
|
|
|
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 {
|
2023-03-27 07:08:56 +00:00
|
|
|
if r.rulesRunning {
|
|
|
|
return ErrAlreadyRunning
|
|
|
|
}
|
|
|
|
r.rulesRunning = true
|
|
|
|
defer r.applyDelayedUpdates()
|
|
|
|
defer func() { r.rulesRunning = false }()
|
|
|
|
|
2023-03-27 06:40:44 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-04-03 02:33:44 +00:00
|
|
|
p.Debug(2, Msgf("Executing steps: %v", steps))
|
|
|
|
|
2023-03-27 06:40:44 +00:00
|
|
|
var errs ErrorCollector
|
|
|
|
for _, step := range steps {
|
|
|
|
stepRules := r.byStep[step]
|
2023-04-03 02:33:44 +00:00
|
|
|
p.Debug(3, Msgf("Executing step %d; length %d", step, len(stepRules)))
|
|
|
|
ShuffleAll(stepRules, p.Rand)
|
2023-03-27 06:40:44 +00:00
|
|
|
var remove []RuleID
|
|
|
|
halt := false
|
|
|
|
for _, id := range stepRules {
|
|
|
|
rule := r.rules[id]
|
2023-04-03 02:33:44 +00:00
|
|
|
p.Debug(4, Msgf("Executing rule %x (labeled %q)", id, rule.Label()))
|
2023-03-27 06:40:44 +00:00
|
|
|
err := rule.Enact(p)
|
|
|
|
if err != nil {
|
2023-04-03 02:33:44 +00:00
|
|
|
p.Debug(2, Msgf("Rule %x (%q): error: %v", id, rule.Label(), err))
|
2023-03-27 06:40:44 +00:00
|
|
|
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 {
|
2023-04-03 02:33:44 +00:00
|
|
|
ret := errs.Emit()
|
|
|
|
p.Debug(2, Msgf("Rules stopping early. Result: %v", ret))
|
|
|
|
return ret
|
2023-03-27 06:40:44 +00:00
|
|
|
}
|
|
|
|
}
|
2023-04-03 02:33:44 +00:00
|
|
|
ret := errs.Emit()
|
|
|
|
p.Debug(2, Msgf("Rules complete. Result: %v", ret))
|
|
|
|
return ret
|
2023-03-27 06:40:44 +00:00
|
|
|
}
|
2023-03-27 07:08:56 +00:00
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|