Initial commit.
No tests. Not complete.
This commit is contained in:
parent
d820204e7f
commit
45bbfe4e8f
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
9
.idea/cardSimEngine.iml
generated
Normal file
9
.idea/cardSimEngine.iml
generated
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="Go" enabled="true" />
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/cardSimEngine.iml" filepath="$PROJECT_DIR$/.idea/cardSimEngine.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
93
cardsim/card.go
Normal file
93
cardsim/card.go
Normal file
@ -0,0 +1,93 @@
|
||||
package cardsim
|
||||
|
||||
// A Card represents an option available to the player. Its methods may be
|
||||
// called many times per turn as the player considers their options.
|
||||
type Card[C StatsCollection] interface {
|
||||
// Title is the short name of the card displayed in the hand
|
||||
// and at the top of the card output. It receives the current
|
||||
// player as an argument. If it returns an error that is not
|
||||
// a warning, the game crashes.
|
||||
Title(p *Player[C]) (Message, error)
|
||||
|
||||
// EventText returns the text to display on the card. If it returns an
|
||||
// error that is not a warning, the game crashes.
|
||||
EventText(p *Player[C]) (Message, error)
|
||||
|
||||
// Options returns the possible actions the player can take for this card.
|
||||
// There must be at least one option.
|
||||
Options(p *Player[C]) ([]CardOption[C], error)
|
||||
}
|
||||
|
||||
// A CardOption represents a choice a player could make for some card.
|
||||
type CardOption[C StatsCollection] interface {
|
||||
// OptionText returns the text displayed for this option. It may be called
|
||||
// many times within a turn as the player considers their options. If it
|
||||
// returns an error that is not a warning, the game crashes.
|
||||
OptionText(p *Player[C]) (Message, error)
|
||||
|
||||
// Enact is called exactly once if the player chooses the option. It is
|
||||
// expected to update values in `p`. It returns the text displayed to the
|
||||
// player as a result of their action. If it returns an error that is not
|
||||
// a warning, the game crashes.
|
||||
//
|
||||
// After an option is enacted, the card is deleted. If a card should be
|
||||
// repeatable, Enact must return it to the deck (on every option).
|
||||
Enact(p *Player[C]) (Message, error)
|
||||
}
|
||||
|
||||
// A BasicCard is a Card with fixed title, text, and options.
|
||||
type BasicCard[C StatsCollection] struct {
|
||||
CardTitle Message
|
||||
CardText Message
|
||||
CardOptions []CardOption[C]
|
||||
}
|
||||
|
||||
func (b *BasicCard[C]) Title(p *Player[C]) (Message, error) {
|
||||
return b.CardTitle, nil
|
||||
}
|
||||
|
||||
func (b *BasicCard[C]) EventText(p *Player[C]) (Message, error) {
|
||||
return b.CardText, nil
|
||||
}
|
||||
|
||||
func (b *BasicCard[C]) Options(_ *Player[C]) ([]CardOption[C], error) {
|
||||
return b.CardOptions, nil
|
||||
}
|
||||
|
||||
// A BasicOption is a CardOption with fixed text, effects, and output.
|
||||
type BasicOption[C StatsCollection] struct {
|
||||
Text Message
|
||||
Effect func(*Player[C]) error
|
||||
Output Message
|
||||
}
|
||||
|
||||
// OptionText implements CardOption.
|
||||
func (b *BasicOption[C]) OptionText(p *Player[C]) (Message, error) {
|
||||
return b.Text, nil
|
||||
}
|
||||
|
||||
// Enact implements CardOption.
|
||||
func (b *BasicOption[C]) Enact(p *Player[C]) (Message, error) {
|
||||
return b.Output, b.Effect(p)
|
||||
}
|
||||
|
||||
// OptionFunc attaches a fixed prompt to an Enact-like function. Unlike
|
||||
// BasicOption, the enactment function provided to OptionFunc returns
|
||||
// the text that should be output as a result of the action, so it is
|
||||
// possible to dynamically generate this text.
|
||||
func OptionFunc[C StatsCollection](text Message, f func(*Player[C]) (Message, error)) CardOption[C] {
|
||||
return &optionFunc[C]{text, f}
|
||||
}
|
||||
|
||||
type optionFunc[C StatsCollection] struct {
|
||||
text Message
|
||||
f func(*Player[C]) (Message, error)
|
||||
}
|
||||
|
||||
func (o *optionFunc[C]) OptionText(p *Player[C]) (Message, error) {
|
||||
return o.text, nil
|
||||
}
|
||||
|
||||
func (o *optionFunc[C]) Enact(p *Player[C]) (Message, error) {
|
||||
return o.f(p)
|
||||
}
|
2
cardsim/doc.go
Normal file
2
cardsim/doc.go
Normal file
@ -0,0 +1,2 @@
|
||||
// Package cardsim is a general engine for NationStates-like simulations.
|
||||
package cardsim
|
221
cardsim/errors.go
Normal file
221
cardsim/errors.go
Normal file
@ -0,0 +1,221 @@
|
||||
package cardsim
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Warning is an error that does not crash the game, but is displayed immediately.
|
||||
type Warning struct {
|
||||
E error
|
||||
}
|
||||
|
||||
// AnyWarning is a special Warning, not intended to be returned from anything,
|
||||
// which every Warning recognizes that they "are" -- errors.Is(w, AnyWarning)
|
||||
// returns true when w is a Warning.
|
||||
var AnyWarning = &Warning{
|
||||
E: errors.New("unspecified warning"),
|
||||
}
|
||||
|
||||
// Error implements the error interface. A warning's error message is the
|
||||
// message of its underlying error, unmodified.
|
||||
func (w *Warning) Error() string {
|
||||
return w.E.Error()
|
||||
}
|
||||
|
||||
// Unwrap implements the Go 1.13 error handling system by allow error identity
|
||||
// searches to continue to the underlying error.
|
||||
func (w *Warning) Unwrap() error {
|
||||
return w.E
|
||||
}
|
||||
|
||||
// Is recognizes every Warning as equivalent to AnyWarning.
|
||||
func (*Warning) Is(target error) bool {
|
||||
w, ok := target.(*Warning)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return w == AnyWarning
|
||||
}
|
||||
|
||||
// Warningf calls fmt.Errorf with the provided arguments, wraps the error
|
||||
// created as a Warning, and returns it.
|
||||
func Warningf(f string, args ...any) *Warning {
|
||||
return &Warning{fmt.Errorf(f, args...)}
|
||||
}
|
||||
|
||||
// A Failure is an error that is definitely not a Warning. If a Warning is
|
||||
// wrapped in a Failure, it stops being a Warning.
|
||||
type Failure struct {
|
||||
e error
|
||||
}
|
||||
|
||||
// Error implements error. It returns the message of the underlying error,
|
||||
// unchanged.
|
||||
func (f Failure) Error() string {
|
||||
return f.e.Error()
|
||||
}
|
||||
|
||||
type singleUnwrappable interface {
|
||||
Unwrap() error
|
||||
}
|
||||
|
||||
type multiUnwrappable interface {
|
||||
Unwrap() []error
|
||||
}
|
||||
|
||||
// Unwrap returns a new Failure wrapping the error wrapped by the underlying
|
||||
// error, if any. If the underlying error wraps nothing, it returns nil.
|
||||
func (f Failure) Unwrap() error {
|
||||
if su, ok := f.e.(singleUnwrappable); ok {
|
||||
w := su.Unwrap()
|
||||
if w != nil {
|
||||
// Do not wrap Failure in Failure, to avoid an infinite loop.
|
||||
// If Failure already wraps Failure, this will "collapse" the
|
||||
// chain down to a single Failure.
|
||||
if f, ok := w.(Failure); ok {
|
||||
return f
|
||||
}
|
||||
return Failure{w}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if mu, ok := f.e.(multiUnwrappable); ok {
|
||||
w := mu.Unwrap()
|
||||
if len(w) == 0 {
|
||||
return nil
|
||||
}
|
||||
return aggregateFailure(w)
|
||||
}
|
||||
|
||||
// Underlying error cannot be unwrapped.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Is implements Go 1.13 error handling for a Failure. It specifically rejects
|
||||
// the notion that it could be AnyWarning, but otherwise forwards the check
|
||||
// to its underlying
|
||||
// However, it allows comparisons to other warnings to proceed as normal.
|
||||
func (f Failure) Is(target error) bool {
|
||||
if w, ok := target.(*Warning); ok {
|
||||
if w == AnyWarning {
|
||||
return false
|
||||
}
|
||||
// Strip the warning of its warning-ness and recurse.
|
||||
return errors.Is(f, w.E)
|
||||
}
|
||||
return false // let Unwrap handle the rest
|
||||
}
|
||||
|
||||
// As implements Go 1.13 error handling for a Failure. It directly unpacks its
|
||||
// contained error if possible, bypassing the "re-wrap in failure" behavior that
|
||||
// Unwrap otherwise uses.
|
||||
func (f Failure) As(target any) bool {
|
||||
// Most of this algorithm is copied from the Go standard library.
|
||||
val := reflect.ValueOf(target)
|
||||
typ := val.Type()
|
||||
if typ.Kind() != reflect.Pointer || val.IsNil() {
|
||||
return false
|
||||
}
|
||||
if reflect.TypeOf(f.e).AssignableTo(typ.Elem()) {
|
||||
val.Elem().Set(reflect.ValueOf(f.e))
|
||||
return true
|
||||
}
|
||||
|
||||
if asable, ok := f.e.(interface{ As(any) bool }); ok {
|
||||
return asable.As(target)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ErrorCollector accumulates errors as an operation progresses. The zero
|
||||
// ErrorCollector is its correct starting value. When it emits a final error:
|
||||
//
|
||||
// * If it contains exactly zero errors, it returns nil.
|
||||
// * If it contains exactly one error, it returns it.
|
||||
// * Otherwise, it returns an error that combines all the errors it collected.
|
||||
// The aggregated error is a Warning if and only if all collected errors
|
||||
// were also warnings.
|
||||
type ErrorCollector struct {
|
||||
errs []error
|
||||
hasFailure bool
|
||||
}
|
||||
|
||||
// Add inserts e into the error collection. If e is nil, this does nothing.
|
||||
func (ec *ErrorCollector) Add(e error) {
|
||||
if e == nil {
|
||||
return
|
||||
}
|
||||
if !errors.Is(e, AnyWarning) {
|
||||
ec.hasFailure = true
|
||||
}
|
||||
ec.errs = append(ec.errs, e)
|
||||
}
|
||||
|
||||
// IsEmpty reports whether ec has zero accumulated errors/warnings.
|
||||
func (ec *ErrorCollector) IsEmpty() bool {
|
||||
return len(ec.errs) == 0
|
||||
}
|
||||
|
||||
// HasFailure reports whether ec has at least one non-warning error.
|
||||
func (ec *ErrorCollector) HasFailure() bool {
|
||||
return ec.hasFailure
|
||||
}
|
||||
|
||||
// Emit returns the final error from this ErrorCollector. Do not Add
|
||||
// to this ErrorCollector after calling Emit, or the error it emitted might
|
||||
// be modified.
|
||||
//
|
||||
// If the error collector has collected no errors, this returns nil. If it has
|
||||
// collected exactly one error, it returns that error. Otherwise, it returns an
|
||||
// aggregate error, which is itself a warning if and only if all aggregated
|
||||
// errors are warnings. This may involve wrapping contained warnings, so
|
||||
// errors.Is does not erroneously represent a failure as a warning because it
|
||||
// contains a warning as a subcomponent.
|
||||
func (ec *ErrorCollector) Emit() error {
|
||||
if len(ec.errs) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(ec.errs) == 1 {
|
||||
return ec.errs[0]
|
||||
}
|
||||
if !ec.HasFailure() {
|
||||
return aggregateFailure(ec.errs)
|
||||
}
|
||||
return &aggregateError{ec.errs} // all these are recognizable failures
|
||||
}
|
||||
|
||||
// An aggregateError is a collection of errors that is itself an error.
|
||||
type aggregateError struct {
|
||||
errs []error
|
||||
}
|
||||
|
||||
// Error returns a multi-line indented error message.
|
||||
func (a *aggregateError) Error() string {
|
||||
var messages []string
|
||||
for i, e := range a.errs {
|
||||
m := fmt.Sprintf("\t[%d]:\t%s", i, strings.ReplaceAll(e.Error(), "\n", "\n\t\t"))
|
||||
messages = append(messages, m)
|
||||
}
|
||||
return fmt.Sprintf("%d errors: <\n%s\n>", len(a.errs), strings.Join(messages, "\n"))
|
||||
}
|
||||
|
||||
// Unwrap implements Go 1.13 error handling. It returns its contained errors.
|
||||
func (a *aggregateError) Unwrap() []error {
|
||||
return a.errs
|
||||
}
|
||||
|
||||
// aggregateFailure wraps every error in a slice in a Failure, cancelling any warnings.
|
||||
func aggregateFailure(errs []error) *aggregateError {
|
||||
ret := make([]error, len(errs))
|
||||
for i, e := range errs {
|
||||
if f, ok := e.(Failure); ok {
|
||||
ret[i] = f
|
||||
} else {
|
||||
ret[i] = Failure{e}
|
||||
}
|
||||
}
|
||||
return &aggregateError{ret}
|
||||
}
|
27
cardsim/messages.go
Normal file
27
cardsim/messages.go
Normal file
@ -0,0 +1,27 @@
|
||||
package cardsim
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Message is an opaque interface representing a displayable message.
|
||||
// Using an interface here allows for implementation of new message display
|
||||
// and formatting features without rewriting all existing callers.
|
||||
type Message interface {
|
||||
fmt.Stringer
|
||||
}
|
||||
|
||||
type stringMessage string
|
||||
|
||||
func (s stringMessage) String() string {
|
||||
return string(s)
|
||||
}
|
||||
|
||||
// MsgStr returns a Message representing a fixed string.
|
||||
func MsgStr(s string) Message {
|
||||
return stringMessage(s)
|
||||
}
|
||||
|
||||
// Msgf is a Sprintf-like function that produces a Message equivalent to the
|
||||
// one created by MsgStr.
|
||||
func Msgf(f string, args ...any) Message {
|
||||
return stringMessage(fmt.Sprintf(f, args...))
|
||||
}
|
16
cardsim/player.go
Normal file
16
cardsim/player.go
Normal file
@ -0,0 +1,16 @@
|
||||
package cardsim
|
||||
|
||||
import "math/rand"
|
||||
|
||||
// Player stores all gameplay state for one player.
|
||||
type Player[C StatsCollection] struct {
|
||||
Stats C
|
||||
Name string
|
||||
Deck []Card[C]
|
||||
Hand []Card[C]
|
||||
HandLimit int
|
||||
Rules *RuleCollection[C]
|
||||
Rand rand.Rand
|
||||
Turn int
|
||||
PendingMessages []Message
|
||||
}
|
326
cardsim/rules.go
Normal file
326
cardsim/rules.go
Normal file
@ -0,0 +1,326 @@
|
||||
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()
|
||||
}
|
173
cardsim/stats.go
Normal file
173
cardsim/stats.go
Normal file
@ -0,0 +1,173 @@
|
||||
package cardsim
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A StatsCollection contains stats.
|
||||
type StatsCollection interface {
|
||||
// Stats returns all the stats in this collection. It's okay for
|
||||
// these to be copies rather than pointers. Stats will be presented
|
||||
// to the player in this order.
|
||||
Stats() []Stat
|
||||
}
|
||||
|
||||
// A Stat is some value that can be printed as part of player status.
|
||||
// It may not be an actual stored value -- it might only be calculated.
|
||||
type Stat interface {
|
||||
// StatName returns the name of this stat, as displayed to the player.
|
||||
StatName() string
|
||||
// String prints the value of the stat.
|
||||
String() string // compatible with fmt.Stringer
|
||||
// Visible returns whether this stat should be displayed to the player
|
||||
// during regular gameplay. (Invisible stats are important for debugging.)
|
||||
Visible() bool
|
||||
}
|
||||
|
||||
// Stored is a generic Stat implementation that stores a stat value and name.
|
||||
// It's visible to the player.
|
||||
type Stored[T any] struct {
|
||||
// Display name of this Stat.
|
||||
Name string
|
||||
// Value of this Stat. Can be overwritten.
|
||||
Value T
|
||||
}
|
||||
|
||||
// Statname implements Stat.
|
||||
func (s Stored[T]) StatName() string {
|
||||
return s.Name
|
||||
}
|
||||
|
||||
// String implements Stat and fmt.Stringer.
|
||||
func (s Stored[T]) String() string {
|
||||
return fmt.Sprint(s.Value)
|
||||
}
|
||||
|
||||
// Visible implements Stat.
|
||||
func (Stored[T]) Visible() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Invisible is a generic Stat implementation that stores a stat value and name.
|
||||
// It's not visible to the player.
|
||||
type Invisible[T any] struct {
|
||||
// Display name of this Stat.
|
||||
Name string
|
||||
// Value of this Stat. Can be overwritten.
|
||||
Value T
|
||||
}
|
||||
|
||||
// Statname implements Stat.
|
||||
func (i Invisible[T]) StatName() string {
|
||||
return i.Name
|
||||
}
|
||||
|
||||
// String implements Stat and fmt.Stringer.
|
||||
func (i Invisible[T]) String() string {
|
||||
return fmt.Sprint(i.Value)
|
||||
}
|
||||
|
||||
// Visible implements Stat.
|
||||
func (Invisible[T]) Visible() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// StatFunc names a function as a stat visible to the player.
|
||||
func StatFunc[T any](name string, f func() T) Stat {
|
||||
return statFunc[T]{
|
||||
f: f,
|
||||
name: name,
|
||||
visible: true,
|
||||
}
|
||||
}
|
||||
|
||||
// InvisibleStatFunc names a function as a stat not visible to the player.
|
||||
func InvisibleStatFunc[T any](name string, f func() T) Stat {
|
||||
return statFunc[T]{
|
||||
f: f,
|
||||
name: name,
|
||||
visible: false,
|
||||
}
|
||||
}
|
||||
|
||||
// statFunc wraps a function as a stat.
|
||||
type statFunc[T any] struct {
|
||||
f func() T
|
||||
name string
|
||||
visible bool
|
||||
}
|
||||
|
||||
func (s statFunc[T]) StatName() string {
|
||||
return s.name
|
||||
}
|
||||
|
||||
func (s statFunc[T]) String() string {
|
||||
return fmt.Sprint(s.f())
|
||||
}
|
||||
|
||||
func (s statFunc[T]) Visible() bool {
|
||||
return s.visible
|
||||
}
|
||||
|
||||
// ExtractStats pulls all exported Stat fields (not functions) out of a struct.
|
||||
// If x cannot be resolved to a struct, it panics. It unwraps interfaces and
|
||||
// follows pointers to try to find a struct.
|
||||
func ExtractStats(x any) []Stat {
|
||||
v := reflect.ValueOf(x)
|
||||
for k := v.Kind(); k == reflect.Pointer || k == reflect.Interface; k = v.Kind() {
|
||||
v = v.Elem()
|
||||
}
|
||||
if v.Kind() != reflect.Struct {
|
||||
panic(fmt.Errorf("%T is not a struct", x))
|
||||
}
|
||||
|
||||
var ret []Stat
|
||||
lim := v.NumField()
|
||||
for i := 0; i < lim; i++ {
|
||||
f := v.Field(i)
|
||||
if !f.CanInterface() {
|
||||
continue
|
||||
}
|
||||
x := f.Interface()
|
||||
if s, ok := x.(Stat); ok {
|
||||
ret = append(ret, s)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// SortStats sorts the provided slice of stats in place. It puts all visible
|
||||
// stats before all invisible stats, then alphabetizes (case-insensitive).
|
||||
func SortStats(ss []Stat) {
|
||||
sort.Sort(statSorter(ss))
|
||||
}
|
||||
|
||||
// statSorter implements sort.Interface for []Stat.
|
||||
type statSorter []Stat
|
||||
|
||||
// Len implements sort.Interface.
|
||||
func (s statSorter) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
// Swap implements sort.Interface.
|
||||
func (s statSorter) Swap(i, j int) {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
|
||||
// Less implements sort.Interface.
|
||||
func (s statSorter) Less(i, j int) bool {
|
||||
lhs, rhs := s[i], s[j]
|
||||
if lhs.Visible() != rhs.Visible() {
|
||||
return lhs.Visible()
|
||||
}
|
||||
ln, rn := strings.ToLower(lhs.StatName()), strings.ToLower(rhs.StatName())
|
||||
if ln != rn {
|
||||
return ln < rn
|
||||
}
|
||||
// Names differ only by capitalization, if that.
|
||||
return lhs.StatName() < rhs.StatName()
|
||||
}
|
Loading…
Reference in New Issue
Block a user