Compare commits

6 Commits

Author SHA1 Message Date
Rakeela
adb02ff38b Forestry tab
Mostly a commit to see if I can still commit.
2025-08-30 23:24:19 -07:00
37d3b639bf Merge branch 'Rewrite-Stats-System' of https://git.chromaticdragon.app/kistaro/KoboldSim into effects_table
# Conflicts:
#	koboldsim/cards.go
#	koboldsim/stats.go

cards.go -- Rakeela fixed a card differently from how I did
stats.go -- expected; Rakeela added to the big switch statement I got rid of
2024-09-29 12:51:39 -07:00
a237fa81bf pull try out of Add
deferring a recovery handler is normal in Go but opaque. Pulling out a helper for this (`try`) makes it more obvious that this is just another "if this operation fails, return error" case.
2024-09-29 12:41:49 -07:00
68cab7d2be Remove stale "Add case" snippet
The reflection-driven implementation of Add no longer contains a huge boilerplate `switch` statement so we no longer need a snippet for the boilerplate.
2024-09-29 12:20:53 -07:00
2d16f97314 Remiplement Add to use reflection.
Now it matters whether `FieldLabel`s are spelled correctly, but in return, we don't have to write and maintain that obnoxious `switch` statement.
2024-09-29 12:19:07 -07:00
8a28f38d4d Finish converting second option of Pan-Tribal Festival of Bureaucracy. 2024-09-29 12:18:12 -07:00
5 changed files with 110 additions and 159 deletions

View File

@@ -15,13 +15,4 @@
// ],
// "description": "Log output to console"
// }
"KoboldMine Add case": {
"scope": "go",
"prefix": "case",
"body": [
"case ${1:field}:",
"\tk.${1:field} += amount",
"\treturn nil",
],
}
}

View File

@@ -298,79 +298,56 @@ var cards = []Card{
EffectsTable: map[FieldLabel]float64{
Construction: 2,
Welfare: 1,
Healthcare: 1,
ParksExpense: 4,
Hospitality: 1,
Madness: -1,
Madness: -2,
Cruelty: -1,
Greed: -1,
},
EnactionDesc: cardsim.MsgStr("The company of others makes cold water tolerable."),
CanDo: YesWeAlsoCan,
},
&BasicPolicy{
&TablePolicy{
UnenactedDesc: cardsim.MsgStr(`Your Minister of Finance is scandalized. "A free service? And such an expensive one to construct? No, no, no. The baths must pull their weight in the budget if they're ot be made at all. We must set an entry fee."`),
EnactedDesc: cardsim.MsgStr("[current policy] Elite bathhouses serve paying customers in your largest sites."),
Do: func(p *Player) (cardsim.Message, error) {
p.Stats.Construction += 2
p.Stats.Hospitality += 3
p.Stats.ParksExpense += 2
return cardsim.MsgStr("A dirty body is the mark of the lower classes."), nil
EffectsTable: map[FieldLabel]float64{
Construction: 2,
Hospitality: 3,
ParksExpense: 2,
Madness: -1,
Greed: 1,
},
Undo: func(p *Player) error {
p.Stats.Construction -= 2
p.Stats.Hospitality -= 3
p.Stats.ParksExpense -= 2
return nil
EnactionDesc: cardsim.MsgStr("A dirty body is the mark of the lower classes."),
CanDo: YesWeAlsoCan,
},
CanDo: YesWeCan,
},
&BasicPolicy{
&TablePolicy{
UnenactedDesc: cardsim.MsgStr(`Your head diplomat chimes in with, "If you want to make the service self-sustaining, build a bath in a shallow cave and invite the surfacers in for a fee. Of course, we'll be giving up some of our secrecy, but it'll bring in foreign exchange to power our industries."`),
EnactedDesc: cardsim.MsgStr("[current policy] Conservative kobolds are scandalized: not only has the tribe bathhouses, it has near-surface bathhouses for surfacers to visit."),
Do: func(p *Player) (cardsim.Message, error) {
p.Stats.Construction += 2
p.Stats.Hospitality += 4
p.Stats.Manufacturing += 1
p.Stats.ParksExpense += 3
p.Stats.FoodSupply += 1
p.Stats.ForeignRelExpense += 1
p.Stats.ForeignRelations += 1
p.Stats.HiddenRelPenalty -= -2
p.Stats.Secrecy -= 10
p.Stats.Cruelty -= 1
return cardsim.MsgStr("Surfacers stink less in the tribe's vicinity."), nil
EffectsTable: map[FieldLabel]float64{
Construction: 2,
Hospitality: 4,
Manufacturing: 1,
ParksExpense: 3,
FoodSupply: 1,
ForeignRelExpense: 1,
ForeignRelations: 1,
HiddenRelPenalty: -2,
Secrecy: -20,
Cruelty: -1,
},
Undo: func(p *Player) error {
p.Stats.Construction -= 2
p.Stats.Hospitality -= 3
p.Stats.Manufacturing -= 1
p.Stats.ParksExpense -= 3
p.Stats.FoodSupply -= 1
p.Stats.ForeignRelExpense -= 1
p.Stats.ForeignRelations -= 1
p.Stats.HiddenRelPenalty += 2
p.Stats.Secrecy += 10
p.Stats.Cruelty += 1
return nil
},
CanDo: YesWeCan,
EnactionDesc: cardsim.MsgStr("Surfacers stink less in the tribe's vicinity."),
CanDo: YesWeAlsoCan,
},
&VerbosePolicy{
Default: &BasicPolicy{
Default: &TablePolicy{
UnenactedDesc: cardsim.MsgStr("Bathhouses are a big project that would displace other things in the city center. You could use the space for housing and services instead."),
EnactedDesc: cardsim.MsgStr("When is the right time to embark on building public baths? You can hew close to skeptics instead."),
Do: func(p *Player) (cardsim.Message, error) {
p.Stats.BasePopulation += 5
p.Stats.Gullibility -= 1
return cardsim.MsgStr("Opponents of public bathing won a recent political contest."), nil
EffectsTable: map[FieldLabel]float64{
BasePopulation: 5,
Gullibility: -1,
},
Undo: func(p *Player) error {
p.Stats.BasePopulation -= 5
p.Stats.Gullibility += 1
return nil
},
CanDo: YesWeCan,
EnactionDesc: cardsim.MsgStr("Opponents of public bathing won a recent political contest."),
CanDo: YesWeAlsoCan,
},
},
}, // End of "A Recipe for Stewed Kobold" policies
@@ -383,9 +360,19 @@ var cards = []Card{
return p.Stats.Militarism > 0
},
Policies: []Policy{
&BasicPolicy{
&TablePolicy{
UnenactedDesc: cardsim.MsgStr(`A scavenger clad in sturdy clothes says, "We need to secure a section of forests on the surface. The surfacers strip too many of the forests bare; if we dispatch patrols ready to fight we can keep them from assarting the forest for their endlessly growing farm communities."`),
EnactedDesc: cardsim.MsgStr("[current policy] The war department maintains your meagre conquest of a stretch of forest on the surface."),
EffectsTable: map[FieldLabel]float64{
BasePopulation: -20,
Forestry: 3,
Militarism: 1,
FoodSupply: 3,
Secrecy: -10,
ForeignRelations: -2,
Madness: -1,
Gullibility: -1,
},
Do: func(p *Player) (cardsim.Message, error) {
p.Stats.BasePopulation -= 20
p.Stats.Forestry += 3
@@ -397,18 +384,8 @@ var cards = []Card{
p.Stats.Gullibility -= 1
return cardsim.MsgStr("A bunch of lumberjacks just disappeared in a forest."), nil
},
Undo: func(p *Player) error {
p.Stats.BasePopulation += 20
p.Stats.Forestry -= 3
p.Stats.Militarism -= 1
p.Stats.FoodSupply -= 5
p.Stats.Secrecy += 10
p.Stats.ForeignRelations += 2
p.Stats.Madness += 1
p.Stats.Gullibility += 1
return nil
},
CanDo: YesWeCan,
EnactionDesc: cardsim.MsgStr("A bunch of lumberjacks just disappeared in a forest."),
CanDo: YesWeAlsoCan,
},
&BasicPolicy{
UnenactedDesc: cardsim.MsgStr(`A very scrawny warrior waves a spear over their head and says, "That's not going far enough! We need to seize farmland of our own! Come on, we shouldn't have to eat cave lichen while the surfacers eat roast pig!"`),

View File

@@ -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;

View File

@@ -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 (
@@ -180,6 +186,7 @@ const (
FoodSupply FieldLabel = "FoodSupply"
ForeignRelations FieldLabel = "ForeignRelations"
ForeignRelExpense FieldLabel = "ForeignRelExpense"
Forestry FieldLabel = "Forestry"
Gadgetry FieldLabel = "Gadgetry"
Greed FieldLabel = "Greed"
Gullibility FieldLabel = "Gullibility"
@@ -199,89 +206,44 @@ const (
Welfare FieldLabel = "Welfare"
)
// 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) error {
switch which {
case Alchemy:
k.Alchemy += amount
return nil
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 ForeignRelExpense:
k.ForeignRelExpense += amount
return nil
case Gadgetry:
k.Gadgetry += amount
return nil
case Greed:
k.Greed += amount
return nil
case Gullibility:
k.Gullibility += amount
return nil
case Healthcare:
k.Healthcare += amount
return nil
case HiddenRelPenalty:
k.HiddenRelPenalty += amount
return nil
case Hospitality:
k.Hospitality += amount
return nil
case Logistics:
k.Logistics += amount
return nil
case Madness:
k.Madness += amount
return nil
case Manufacturing:
k.Manufacturing += amount
return nil
case Militarism:
k.Militarism += amount
return nil
case Mining:
k.Mining += amount
return nil
case ParksExpense:
k.ParksExpense += amount
return nil
case Publishing:
k.Publishing += amount
return nil
case Rebellion:
k.Rebellion += amount
return nil
case Scavenging:
k.Scavenging += amount
return nil
case Secrecy:
k.Secrecy += amount
return nil
case Welfare:
k.Welfare += amount
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)
}
if !f.CanFloat() {
return fmt.Errorf("cannot add %f to field %q: %w", amount, which, ErrFieldNotFloat)
}
if err := try(func() { f.SetFloat(f.Float() + amount) }); err != nil {
return fmt.Errorf("could not add %f to field %q: %w", amount, which, err)
}
return nil
}
return fmt.Errorf("cannot add %f to %q: %w", amount, which, ErrNoFieldLabel)
}

View File

@@ -1,6 +1,8 @@
package koboldsim
import (
"errors"
"fmt"
"math"
"golang.org/x/exp/constraints"
@@ -47,3 +49,23 @@ func clamp[T constraints.Ordered](a, b, c T) T {
// `a` is neither most nor least; therefore, `a` is mid
return a
}
var ErrWrappedPanic = errors.New("panic")
// try catches a panic in the provided func and demotes it to an error, if any
// panic occurs. The returned error, if any, wraps `ErrWrappedPanic`. If the
// panic argument is itself an error, it is also wrapped; otherwise, it is
// stringified into the error message using `%v`.
func try(f func()) (finalErr error) {
defer func() {
if r := recover(); r != nil {
if e, ok := r.(error); ok {
finalErr = fmt.Errorf("%w: %w", ErrWrappedPanic, e)
return
}
finalErr = fmt.Errorf("%w: %v", ErrWrappedPanic, r)
}
}()
f()
return nil
}