diff --git a/koboldsim/cardtypes.go b/koboldsim/cardtypes.go index d0663ee..c128eb8 100644 --- a/koboldsim/cardtypes.go +++ b/koboldsim/cardtypes.go @@ -9,7 +9,6 @@ import ( var ( ErrOptionNotEnabled = errors.New("option not enabled") ErrPolicyNotEnacted = errors.New("cannot unenact policy that is not enacted") - ErrNoFieldLabel = errors.New("field does not exist") // ErrUnimplemented and ErrKeepMessaage are "non-errors". They are used // as special signals that the result needs to be handled in a special way; diff --git a/koboldsim/stats.go b/koboldsim/stats.go index 1f76e73..338eaf8 100644 --- a/koboldsim/stats.go +++ b/koboldsim/stats.go @@ -1,7 +1,9 @@ package koboldsim import ( + "errors" "fmt" + "reflect" "git.chromaticdragon.app/kistaro/CardSimEngine/cardsim" ) @@ -167,6 +169,10 @@ func NewKoboldMine() *KoboldMine { } } +// FieldLabel instances are strings exactly matching the name of an +// exported field in `KoboldMine`. These are used to map field names to +// amounts to change in `TablePolicy` instances, which are then looked +// up by name (via reflection) when adding the stat. type FieldLabel string const ( @@ -188,56 +194,51 @@ const ( Secrecy FieldLabel = "Secrecy" ) -func (k *KoboldMine) Add(which FieldLabel, amount float64) error { - switch which { - case Authoritarianism: - k.Authoritarianism += amount - return nil - case BasePopulation: - k.BasePopulation += amount - return nil - case Bureaucracy: - k.Bureaucracy += amount - return nil - case Construction: - k.Construction += amount - return nil - case Cruelty: - k.Cruelty += amount - return nil - case Education: - k.Education += amount - return nil - case FoodSupply: - k.FoodSupply += amount - return nil - case ForeignRelations: - k.ForeignRelations += amount - return nil - case Gullibility: - k.Gullibility += amount - return nil - case Madness: - k.Madness += amount - return nil - case Manufacturing: - k.Manufacturing += amount - return nil - case Mining: - k.Mining += amount - return nil - case Militarism: - k.Militarism += amount - return nil - case Rebellion: - k.Rebellion += amount - return nil - case Scavenging: - k.Scavenging += amount - return nil - case Secrecy: - k.Secrecy += amount - return nil +// ErrBadFieldLabel is an "error category" for all errors where a +// FieldLabel did not correctly name a Field that could be used in Add. +var ErrBadFieldLabel = errors.New("bad field label") + +// ErrNoFieldLabel is a kind of `ErrBadFieldLabel` used when no field +// of the target has the exact name specified in the FieldLabel. Check +// spelling and capitalization. +var ErrNoFieldLabel = fmt.Errorf("%w: field does not exist", ErrBadFieldLabel) + +// ErrFieldNotFloat is a kind of `ErrBadFieldLabel` used when it is not +// possible to read and assign a float to the named field. Is this the +// name of a calculated stat (a function) rather than a base stat? +var ErrFieldNotFloat = fmt.Errorf("%w: field type is not float", ErrBadFieldLabel) + +// ErrFieldSetPanic is a kind of `ErrBadFieldLabel` used when trying to +// set the value of a field panicked. The panic message should be +// preserved in the error to diagnose the problem. If the problem is +// that the field is unexported, capitalize the name (including inside +// the KoboldMine type). Any other issue is a more complicated bug, +// because all of them that aren't already caught by ErrFieldNotFloat +// imply something very unexpected happened with `k` itself (in `Add`). +var ErrFieldSetPanic = fmt.Errorf("%w: panic when setting", ErrBadFieldLabel) + +// Use a FieldLabel to add an amount to the matching field. If no such +// field can be found, the field is not exported, or the field is not +// of type float64, this returns an error ad does not change any values. +func (k *KoboldMine) Add(which FieldLabel, amount float64) (finalErr error) { + kv := reflect.ValueOf(k).Elem() + f := kv.FieldByName(string(which)) + if !f.IsValid() { + return fmt.Errorf("cannot add %f to field %q: %w", amount, which, ErrNoFieldLabel) } - return fmt.Errorf("cannot add %f to %q: %w", amount, which, ErrNoFieldLabel) + if !f.CanFloat() { + return fmt.Errorf("cannot add %f to field %q: %w", amount, which, ErrFieldNotFloat) + } + + defer func() { + if r := recover(); r != nil { + if e, ok := r.(error); ok { + finalErr = fmt.Errorf("could not add %f to field %q: %w: %w", amount, which, ErrFieldSetPanic, e) + } else { + finalErr = fmt.Errorf("could not add %f to field %q: %w: %v", amount, which, ErrFieldSetPanic, r) + } + } + }() + f.SetFloat(f.Float() + amount) + return nil }