Compare commits
6 Commits
39e4c94b6f
...
Rewrite-St
Author | SHA1 | Date | |
---|---|---|---|
|
adb02ff38b | ||
37d3b639bf
|
|||
a237fa81bf
|
|||
68cab7d2be
|
|||
2d16f97314
|
|||
8a28f38d4d
|
9
.vscode/koboldsimsnippets.code-snippets
vendored
9
.vscode/koboldsimsnippets.code-snippets
vendored
@@ -15,13 +15,4 @@
|
|||||||
// ],
|
// ],
|
||||||
// "description": "Log output to console"
|
// "description": "Log output to console"
|
||||||
// }
|
// }
|
||||||
"KoboldMine Add case": {
|
|
||||||
"scope": "go",
|
|
||||||
"prefix": "case",
|
|
||||||
"body": [
|
|
||||||
"case ${1:field}:",
|
|
||||||
"\tk.${1:field} += amount",
|
|
||||||
"\treturn nil",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
}
|
@@ -298,79 +298,56 @@ var cards = []Card{
|
|||||||
EffectsTable: map[FieldLabel]float64{
|
EffectsTable: map[FieldLabel]float64{
|
||||||
Construction: 2,
|
Construction: 2,
|
||||||
Welfare: 1,
|
Welfare: 1,
|
||||||
Healthcare: 1,
|
|
||||||
ParksExpense: 4,
|
ParksExpense: 4,
|
||||||
Hospitality: 1,
|
Hospitality: 1,
|
||||||
Madness: -1,
|
Madness: -2,
|
||||||
Cruelty: -1,
|
Cruelty: -1,
|
||||||
Greed: -1,
|
Greed: -1,
|
||||||
},
|
},
|
||||||
EnactionDesc: cardsim.MsgStr("The company of others makes cold water tolerable."),
|
EnactionDesc: cardsim.MsgStr("The company of others makes cold water tolerable."),
|
||||||
CanDo: YesWeAlsoCan,
|
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."`),
|
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."),
|
EnactedDesc: cardsim.MsgStr("[current policy] Elite bathhouses serve paying customers in your largest sites."),
|
||||||
Do: func(p *Player) (cardsim.Message, error) {
|
EffectsTable: map[FieldLabel]float64{
|
||||||
p.Stats.Construction += 2
|
Construction: 2,
|
||||||
p.Stats.Hospitality += 3
|
Hospitality: 3,
|
||||||
p.Stats.ParksExpense += 2
|
ParksExpense: 2,
|
||||||
return cardsim.MsgStr("A dirty body is the mark of the lower classes."), nil
|
Madness: -1,
|
||||||
|
Greed: 1,
|
||||||
},
|
},
|
||||||
Undo: func(p *Player) error {
|
EnactionDesc: cardsim.MsgStr("A dirty body is the mark of the lower classes."),
|
||||||
p.Stats.Construction -= 2
|
CanDo: YesWeAlsoCan,
|
||||||
p.Stats.Hospitality -= 3
|
|
||||||
p.Stats.ParksExpense -= 2
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
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."`),
|
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."),
|
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) {
|
EffectsTable: map[FieldLabel]float64{
|
||||||
p.Stats.Construction += 2
|
Construction: 2,
|
||||||
p.Stats.Hospitality += 4
|
Hospitality: 4,
|
||||||
p.Stats.Manufacturing += 1
|
Manufacturing: 1,
|
||||||
p.Stats.ParksExpense += 3
|
ParksExpense: 3,
|
||||||
p.Stats.FoodSupply += 1
|
FoodSupply: 1,
|
||||||
p.Stats.ForeignRelExpense += 1
|
ForeignRelExpense: 1,
|
||||||
p.Stats.ForeignRelations += 1
|
ForeignRelations: 1,
|
||||||
p.Stats.HiddenRelPenalty -= -2
|
HiddenRelPenalty: -2,
|
||||||
p.Stats.Secrecy -= 10
|
Secrecy: -20,
|
||||||
p.Stats.Cruelty -= 1
|
Cruelty: -1,
|
||||||
return cardsim.MsgStr("Surfacers stink less in the tribe's vicinity."), nil
|
|
||||||
},
|
},
|
||||||
Undo: func(p *Player) error {
|
EnactionDesc: cardsim.MsgStr("Surfacers stink less in the tribe's vicinity."),
|
||||||
p.Stats.Construction -= 2
|
CanDo: YesWeAlsoCan,
|
||||||
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,
|
|
||||||
},
|
},
|
||||||
&VerbosePolicy{
|
&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."),
|
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."),
|
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) {
|
EffectsTable: map[FieldLabel]float64{
|
||||||
p.Stats.BasePopulation += 5
|
BasePopulation: 5,
|
||||||
p.Stats.Gullibility -= 1
|
Gullibility: -1,
|
||||||
return cardsim.MsgStr("Opponents of public bathing won a recent political contest."), nil
|
|
||||||
},
|
},
|
||||||
Undo: func(p *Player) error {
|
EnactionDesc: cardsim.MsgStr("Opponents of public bathing won a recent political contest."),
|
||||||
p.Stats.BasePopulation -= 5
|
CanDo: YesWeAlsoCan,
|
||||||
p.Stats.Gullibility += 1
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
CanDo: YesWeCan,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, // End of "A Recipe for Stewed Kobold" policies
|
}, // End of "A Recipe for Stewed Kobold" policies
|
||||||
@@ -383,9 +360,19 @@ var cards = []Card{
|
|||||||
return p.Stats.Militarism > 0
|
return p.Stats.Militarism > 0
|
||||||
},
|
},
|
||||||
Policies: []Policy{
|
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."`),
|
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."),
|
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) {
|
Do: func(p *Player) (cardsim.Message, error) {
|
||||||
p.Stats.BasePopulation -= 20
|
p.Stats.BasePopulation -= 20
|
||||||
p.Stats.Forestry += 3
|
p.Stats.Forestry += 3
|
||||||
@@ -397,18 +384,8 @@ var cards = []Card{
|
|||||||
p.Stats.Gullibility -= 1
|
p.Stats.Gullibility -= 1
|
||||||
return cardsim.MsgStr("A bunch of lumberjacks just disappeared in a forest."), nil
|
return cardsim.MsgStr("A bunch of lumberjacks just disappeared in a forest."), nil
|
||||||
},
|
},
|
||||||
Undo: func(p *Player) error {
|
EnactionDesc: cardsim.MsgStr("A bunch of lumberjacks just disappeared in a forest."),
|
||||||
p.Stats.BasePopulation += 20
|
CanDo: YesWeAlsoCan,
|
||||||
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,
|
|
||||||
},
|
},
|
||||||
&BasicPolicy{
|
&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!"`),
|
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!"`),
|
||||||
|
@@ -9,7 +9,6 @@ import (
|
|||||||
var (
|
var (
|
||||||
ErrOptionNotEnabled = errors.New("option not enabled")
|
ErrOptionNotEnabled = errors.New("option not enabled")
|
||||||
ErrPolicyNotEnacted = errors.New("cannot unenact policy that is not enacted")
|
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
|
// ErrUnimplemented and ErrKeepMessaage are "non-errors". They are used
|
||||||
// as special signals that the result needs to be handled in a special way;
|
// as special signals that the result needs to be handled in a special way;
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
package koboldsim
|
package koboldsim
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
"git.chromaticdragon.app/kistaro/CardSimEngine/cardsim"
|
"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
|
type FieldLabel string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -180,6 +186,7 @@ const (
|
|||||||
FoodSupply FieldLabel = "FoodSupply"
|
FoodSupply FieldLabel = "FoodSupply"
|
||||||
ForeignRelations FieldLabel = "ForeignRelations"
|
ForeignRelations FieldLabel = "ForeignRelations"
|
||||||
ForeignRelExpense FieldLabel = "ForeignRelExpense"
|
ForeignRelExpense FieldLabel = "ForeignRelExpense"
|
||||||
|
Forestry FieldLabel = "Forestry"
|
||||||
Gadgetry FieldLabel = "Gadgetry"
|
Gadgetry FieldLabel = "Gadgetry"
|
||||||
Greed FieldLabel = "Greed"
|
Greed FieldLabel = "Greed"
|
||||||
Gullibility FieldLabel = "Gullibility"
|
Gullibility FieldLabel = "Gullibility"
|
||||||
@@ -199,89 +206,44 @@ const (
|
|||||||
Welfare FieldLabel = "Welfare"
|
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 {
|
func (k *KoboldMine) Add(which FieldLabel, amount float64) error {
|
||||||
switch which {
|
kv := reflect.ValueOf(k).Elem()
|
||||||
case Alchemy:
|
f := kv.FieldByName(string(which))
|
||||||
k.Alchemy += amount
|
if !f.IsValid() {
|
||||||
return nil
|
return fmt.Errorf("cannot add %f to field %q: %w", amount, which, ErrNoFieldLabel)
|
||||||
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
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
package koboldsim
|
package koboldsim
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
"golang.org/x/exp/constraints"
|
"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
|
// `a` is neither most nor least; therefore, `a` is mid
|
||||||
return a
|
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
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user