Compare commits
13 Commits
e018bd0ad6
...
Rewrite-St
Author | SHA1 | Date | |
---|---|---|---|
37d3b639bf
|
|||
39e4c94b6f | |||
99c5e5af6d | |||
a237fa81bf
|
|||
68cab7d2be
|
|||
2d16f97314
|
|||
8a28f38d4d
|
|||
b2099586fc
|
|||
c4427885ca | |||
219ff33d66
|
|||
5b860135e9
|
|||
d2a00d2044
|
|||
ba5171fd67
|
18
.vscode/koboldsimsnippets.code-snippets
vendored
Normal file
18
.vscode/koboldsimsnippets.code-snippets
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
// Place your KoboldSim workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
|
||||
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
|
||||
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
|
||||
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
|
||||
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
|
||||
// Placeholders with the same ids are connected.
|
||||
// Example:
|
||||
// "Print to console": {
|
||||
// "scope": "javascript,typescript",
|
||||
// "prefix": "log",
|
||||
// "body": [
|
||||
// "console.log('$1');",
|
||||
// "$2"
|
||||
// ],
|
||||
// "description": "Log output to console"
|
||||
// }
|
||||
}
|
2
go.mod
2
go.mod
@ -8,5 +8,5 @@ require (
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.9.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect
|
||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29
|
||||
)
|
||||
|
@ -10,99 +10,62 @@ var cards = []Card{
|
||||
Desc: cardsim.MsgStr(" A surge of anti-kobold sentiment has been reported by your spies on the surface and your military is concerned that anti-kobold vigilantes will attack the tunnels in the near-future. They want to build up now while doing so is safe."),
|
||||
After: ShuffleIntoBottomHalf,
|
||||
Policies: []Policy{
|
||||
&BasicPolicy{
|
||||
&TablePolicy{
|
||||
UnenactedDesc: cardsim.MsgStr(`Your war chief paces irritably. "We have terrible threats facing us. It's imperative that we build up our numbers in both the military and the domestic sense. I want creches under military control and a good hunting crew to supply them with food."`),
|
||||
EnactedDesc: cardsim.MsgStr("[current policy] Your war chief is presently monitoring the situation, building up your military, and securing your creches."),
|
||||
Do: func(p *Player) (cardsim.Message, error) {
|
||||
p.Stats.BasePopulation += 100
|
||||
p.Stats.Scavenging += 1
|
||||
p.Stats.Militarism += 2
|
||||
p.Stats.FoodSupply += 1
|
||||
p.Stats.ForeignRelations -= 2
|
||||
p.Stats.Rebellion -= 3
|
||||
p.Stats.Madness += 1
|
||||
p.Stats.Cruelty += 1
|
||||
p.Stats.Authoritarianism += 4
|
||||
return cardsim.MsgStr("Kobolds are known to be born warriors."), nil
|
||||
EffectsTable: map[FieldLabel]float64{
|
||||
BasePopulation: 100,
|
||||
Scavenging: 1,
|
||||
Militarism: 2,
|
||||
FoodSupply: 1,
|
||||
ForeignRelations: -2,
|
||||
Rebellion: -3,
|
||||
Madness: 1,
|
||||
Cruelty: 1,
|
||||
Authoritarianism: 4,
|
||||
},
|
||||
Undo: func(p *Player) error {
|
||||
p.Stats.BasePopulation -= 100
|
||||
p.Stats.Scavenging -= 1
|
||||
p.Stats.Militarism -= 2
|
||||
p.Stats.FoodSupply -= 1
|
||||
p.Stats.ForeignRelations += 2
|
||||
p.Stats.Rebellion += 3
|
||||
p.Stats.Madness -= 1
|
||||
p.Stats.Cruelty -= 1
|
||||
p.Stats.Authoritarianism -= 4
|
||||
return nil
|
||||
EnactionDesc: cardsim.MsgStr("Kobolds are known to be born warriors."),
|
||||
CanDo: YesWeAlsoCan,
|
||||
},
|
||||
CanDo: YesWeCan,
|
||||
},
|
||||
&BasicPolicy{
|
||||
&TablePolicy{
|
||||
UnenactedDesc: cardsim.MsgStr(`Your head miner considers the matter worriedly. "Creches under military control? No. That would invite chaos. We need to dig deeper; we can have a peaceful, orderly society if we just get far enough away from surfacers."`),
|
||||
EnactedDesc: cardsim.MsgStr("[current policy] Your head miner is presently leading a project to dig as far away from the surface as possible."),
|
||||
Do: func(p *Player) (cardsim.Message, error) {
|
||||
p.Stats.BasePopulation += 40
|
||||
p.Stats.Mining += 2
|
||||
p.Stats.Construction += 1
|
||||
p.Stats.Manufacturing += 1
|
||||
p.Stats.Bureaucracy += 1
|
||||
p.Stats.FoodSupply -= 1
|
||||
p.Stats.Secrecy += 3
|
||||
return cardsim.MsgStr("Kobolds are known to be cowards hiding in the dark."), nil
|
||||
EffectsTable: map[FieldLabel]float64{
|
||||
BasePopulation: 40,
|
||||
Mining: 2,
|
||||
Construction: 1,
|
||||
Manufacturing: 1,
|
||||
Bureaucracy: 1,
|
||||
FoodSupply: -1,
|
||||
Secrecy: 3,
|
||||
},
|
||||
Undo: func(p *Player) error {
|
||||
p.Stats.BasePopulation -= 40
|
||||
p.Stats.Mining -= 2
|
||||
p.Stats.Manufacturing -= 1
|
||||
p.Stats.Construction -= 1
|
||||
p.Stats.Bureaucracy -= 1
|
||||
p.Stats.FoodSupply += 1
|
||||
p.Stats.Secrecy -= 3
|
||||
return nil
|
||||
EnactionDesc: cardsim.MsgStr("Kobolds are known to be cowards hiding in the dark."),
|
||||
CanDo: YesWeAlsoCan,
|
||||
},
|
||||
CanDo: YesWeCan,
|
||||
},
|
||||
&BasicPolicy{
|
||||
&TablePolicy{
|
||||
UnenactedDesc: cardsim.MsgStr(`Your nursery director is incensed. "Creches under military control? Never! Let young kobolds play! In fact, cut the military just for suggesting this. The threats facing us are completely overstated."`),
|
||||
EnactedDesc: cardsim.MsgStr("[current policy] Military funding has been diverted into early childhood education."),
|
||||
Do: func(p *Player) (cardsim.Message, error) {
|
||||
p.Stats.BasePopulation -= 40
|
||||
p.Stats.Scavenging -= 1
|
||||
p.Stats.Militarism -= 2
|
||||
p.Stats.Education += 1
|
||||
p.Stats.FoodSupply -= 1
|
||||
p.Stats.ForeignRelations += 2
|
||||
p.Stats.Madness -= 1
|
||||
p.Stats.Cruelty -= 1
|
||||
return cardsim.MsgStr("An undefended hunting outpost near the surface was recently wiped out by a raid."), nil
|
||||
EffectsTable: map[FieldLabel]float64{
|
||||
BasePopulation: -40,
|
||||
Scavenging: -1,
|
||||
Militarism: -2,
|
||||
Education: 1,
|
||||
FoodSupply: -2,
|
||||
ForeignRelations: 2,
|
||||
Madness: -1,
|
||||
Cruelty: -1,
|
||||
},
|
||||
Undo: func(p *Player) error {
|
||||
p.Stats.BasePopulation += 40
|
||||
p.Stats.Scavenging += 1
|
||||
p.Stats.Militarism += 2
|
||||
p.Stats.Education -= 1
|
||||
p.Stats.FoodSupply += 1
|
||||
p.Stats.ForeignRelations -= 2
|
||||
p.Stats.Madness += 1
|
||||
p.Stats.Cruelty += 1
|
||||
return nil
|
||||
},
|
||||
CanDo: YesWeCan,
|
||||
EnactionDesc: cardsim.MsgStr("An undefended hunting outpost near the surface was recently wiped out by a raid."),
|
||||
CanDo: YesWeAlsoCan,
|
||||
},
|
||||
&VerbosePolicy{
|
||||
Default: &BasicPolicy{
|
||||
Default: &TablePolicy{
|
||||
UnenactedDesc: cardsim.MsgStr("This isn't about a disaster and can probably be safely dismissed."),
|
||||
Do: func(p *Player) (cardsim.Message, error) {
|
||||
p.Stats.BasePopulation += 20
|
||||
return cardsim.MsgStr("Creche control doesn't shift that easily."), nil
|
||||
EffectsTable: map[FieldLabel]float64{
|
||||
BasePopulation: 20,
|
||||
},
|
||||
Undo: func(p *Player) error {
|
||||
p.Stats.BasePopulation -= 20
|
||||
return nil
|
||||
},
|
||||
CanDo: YesWeCan,
|
||||
EnactionDesc: cardsim.MsgStr("Creche control doesn't shift that easily."),
|
||||
CanDo: YesWeAlsoCan,
|
||||
},
|
||||
Variants: []Policy{
|
||||
&BasicPolicy{
|
||||
@ -130,93 +93,66 @@ var cards = []Card{
|
||||
Desc: cardsim.MsgStr(" Good times are upon us! A great festival has been declared between many kobold tribes, celebrating the orderly nature of the kobold soul! That's right, it's the Pan-Tribal Festival of Bureaucracy!"),
|
||||
After: ShuffleIntoBottomHalf,
|
||||
Policies: []Policy{
|
||||
&BasicPolicy{
|
||||
&TablePolicy{
|
||||
UnenactedDesc: cardsim.MsgStr(`Your Minister of Administration is practically jumping for joy. "This is our opportunity to prove that we're a prosperous society and attract some fresh blood! We need to raise salaries in the bureaucracy, grant some time off to our bureaucrats to make sure they can attend, and try to hire talent away from other tribes. We'll forge productive trade relations by this, you'll see!`),
|
||||
EnactedDesc: cardsim.MsgStr("[current policy] Your bureaucrats are really looking forward to attending, where they can boast about how the festival boosted their salaries."),
|
||||
Do: func(p *Player) (cardsim.Message, error) {
|
||||
p.Stats.BasePopulation += 60
|
||||
p.Stats.Scavenging += 1
|
||||
p.Stats.Bureaucracy += 3
|
||||
p.Stats.FoodSupply += 2
|
||||
p.Stats.Gullibility -= 1
|
||||
return cardsim.MsgStr("Bureaucrats are considered pillars of society."), nil
|
||||
EffectsTable: map[FieldLabel]float64{
|
||||
BasePopulation: 60,
|
||||
Scavenging: 1,
|
||||
Bureaucracy: 3,
|
||||
FoodSupply: 1,
|
||||
Gullibility: -1,
|
||||
},
|
||||
Undo: func(p *Player) error {
|
||||
p.Stats.BasePopulation -= 80
|
||||
p.Stats.Scavenging -= 1
|
||||
p.Stats.Bureaucracy -= 3
|
||||
p.Stats.FoodSupply -= 2
|
||||
p.Stats.Gullibility += 1
|
||||
return nil
|
||||
EnactionDesc: cardsim.MsgStr("Bureaucrats are considered pillars of society."),
|
||||
CanDo: YesWeAlsoCan,
|
||||
},
|
||||
CanDo: YesWeCan,
|
||||
},
|
||||
&BasicPolicy{
|
||||
&TablePolicy{
|
||||
UnenactedDesc: cardsim.MsgStr(`Your Minister of Finance pulls you aside. "This is a good opportunity to head-hunt. Bureaucrats from many tribes will be attending. There's got to be some who'll work for less than the ones we've got. There are some terribly threatened or impoverished communities among kobolds.`),
|
||||
EnactedDesc: cardsim.MsgStr("[current policy] Your tribe's policy is clear: the festival of bureaucracy is a chance to trade workers with other kobold tribes in your attempts to build a more efficient government."),
|
||||
Do: func(p *Player) (cardsim.Message, error) {
|
||||
p.Stats.BasePopulation += 20
|
||||
p.Stats.Bureaucracy -= 1
|
||||
p.Stats.Gullibility -= 1
|
||||
return cardsim.MsgStr("Immigrant bureaucrats complain about being under-appreciated."), nil
|
||||
EffectsTable: map[FieldLabel]float64{
|
||||
BasePopulation: 20,
|
||||
Bureaucracy: -1,
|
||||
},
|
||||
Undo: func(p *Player) error {
|
||||
p.Stats.BasePopulation -= 20
|
||||
p.Stats.Bureaucracy += 1
|
||||
p.Stats.Gullibility += 1
|
||||
return nil
|
||||
},
|
||||
CanDo: func(b *BasicPolicy, p *Player) bool {
|
||||
return p.Stats.Bureaucracy >= 2 && b.LastEnactedIdx != 3
|
||||
EnactionDesc: cardsim.MsgStr("Immigrant bureaucrats complain about being under-appreciated."),
|
||||
CanDo: func(t *TablePolicy, p *Player) bool {
|
||||
return p.Stats.Bureaucracy >= 2 && t.LastEnactedIdx != 3
|
||||
},
|
||||
},
|
||||
&BasicPolicy{
|
||||
&TablePolicy{
|
||||
UnenactedDesc: cardsim.MsgStr(`Your Minister of Finance greets you exuberantly. "We've trimmed the wages of the bureaucracy beautifully, but maybe we can scout some outright volunteers. Some tribes are absolute disasters, after all.`),
|
||||
EnactedDesc: cardsim.MsgStr(`[current policy] It's hard to find volunteer workers at the festival, but your tribe is doing its best.`),
|
||||
Do: func(p *Player) (cardsim.Message, error) {
|
||||
p.Stats.BasePopulation += 10
|
||||
p.Stats.Bureaucracy -= 2
|
||||
p.Stats.Rebellion += 1
|
||||
return cardsim.MsgStr("The local bureaucracy is staffed by volunteer labor."), nil
|
||||
EffectsTable: map[FieldLabel]float64{
|
||||
BasePopulation: 10,
|
||||
Bureaucracy: -2,
|
||||
Rebellion: 1,
|
||||
Gullibility: 2,
|
||||
Authoritarianism: -1,
|
||||
},
|
||||
Undo: func(p *Player) error {
|
||||
p.Stats.BasePopulation -= 10
|
||||
p.Stats.Bureaucracy += 2
|
||||
p.Stats.Rebellion -= 1
|
||||
return nil
|
||||
},
|
||||
CanDo: func(b *BasicPolicy, p *Player) bool {
|
||||
return p.Stats.Bureaucracy < 2 && b.LastEnactedIdx != 3
|
||||
EnactionDesc: cardsim.MsgStr("The local bureaucracy is staffed by volunteer labor."),
|
||||
CanDo: func(t *TablePolicy, p *Player) bool {
|
||||
return p.Stats.Bureaucracy < 2 && t.LastEnactedIdx != 3
|
||||
},
|
||||
},
|
||||
&BasicPolicy{
|
||||
UnenactedDesc: cardsim.MsgStr(`One of your non-bureaucrat friends has been in a foul temper. "This festival is everything that's wrong with our society. Life isn't about filling out forms. We ought to snub this festival and outright fire some bureaucrats."`),
|
||||
&TablePolicy{
|
||||
UnenactedDesc: cardsim.MsgStr(`One of your non-bureaucrat friends has been in a foul temper. "This festival is everything that's wrong with our society. Life isn't about filling out forms. We ought to snub this festival and outright fire a bunch of paper-pushers."`),
|
||||
EnactedDesc: cardsim.MsgStr("Your tribe currently bans the Pan-Tribal Festival of Bureaucracy. There will be no local celebration without a policy change."),
|
||||
Do: func(p *Player) (cardsim.Message, error) {
|
||||
p.Stats.BasePopulation -= 80
|
||||
p.Stats.Bureaucracy -= 4
|
||||
return cardsim.MsgStr("A wave of bureaucrats just emigrated along with kobolds incensed by the tribe's lack of respect for administration."), nil
|
||||
EffectsTable: map[FieldLabel]float64{
|
||||
BasePopulation: 80,
|
||||
Bureaucracy: 4,
|
||||
Gullibility: 4,
|
||||
},
|
||||
Undo: func(p *Player) error {
|
||||
p.Stats.BasePopulation += 80
|
||||
p.Stats.Bureaucracy += 4
|
||||
return nil
|
||||
},
|
||||
CanDo: YesWeCan,
|
||||
EnactionDesc: cardsim.MsgStr("A wave of bureaucrats just emigrated along with kobolds incensed by the tribe's lack of respect for administration."),
|
||||
CanDo: YesWeAlsoCan,
|
||||
},
|
||||
&VerbosePolicy{
|
||||
Default: &BasicPolicy{
|
||||
Default: &TablePolicy{
|
||||
UnenactedDesc: cardsim.MsgStr("Some kobolds will attend the festival on their own, and we may face desertions for our lack of interest."),
|
||||
EnactedDesc: cardsim.MsgStr("Some kobolds will attend the festival on their own. No change is expected."),
|
||||
Do: func(p *Player) (cardsim.Message, error) {
|
||||
p.Stats.BasePopulation -= 20
|
||||
return cardsim.MsgStr("A festival of bureaucracy lured away a few kobolds to other tribes."), nil
|
||||
EffectsTable: map[FieldLabel]float64{
|
||||
BasePopulation: -20,
|
||||
},
|
||||
Undo: func(p *Player) error {
|
||||
p.Stats.BasePopulation += 20
|
||||
return nil
|
||||
},
|
||||
CanDo: YesWeCan,
|
||||
EnactionDesc: cardsim.MsgStr("A festival of bureaucracy lured away a few kobolds to other tribes."),
|
||||
CanDo: YesWeAlsoCan,
|
||||
},
|
||||
Variants: []Policy{
|
||||
nil,
|
||||
@ -246,137 +182,81 @@ var cards = []Card{
|
||||
Desc: cardsim.MsgStr("An inevitable concern of kobold tribes is the matter of trade. Trade with other kobolds is straightforward, but overland caravans are unsafe. Trade with surfacers is vital... but achieving it is difficult owing to anti-kobold sentiment."),
|
||||
After: ShuffleIntoBottomHalf,
|
||||
Policies: []Policy{
|
||||
&BasicPolicy{
|
||||
&TablePolicy{
|
||||
UnenactedDesc: cardsim.MsgStr(`Speaking for the conservative opinion, one prospective merchant says, "We need illusions. Our priority needs to be studying or trading to acquire or create spells or artifacts of disguising. The high start-up costs will be balanced by subsequent free movement in a surface society that detests us."`),
|
||||
EnactedDesc: cardsim.MsgStr("[current policy] Your merchants rely on magical disguises to avoid being identified as kobolds while doing business on the surface."),
|
||||
Do: func(p *Player) (cardsim.Message, error) {
|
||||
p.Stats.Manufacturing += 3
|
||||
p.Stats.Logistics += 4
|
||||
p.Stats.Mining += 1
|
||||
p.Stats.Alchemy += 1
|
||||
p.Stats.Gadgetry += 1
|
||||
p.Stats.Scavenging += 1
|
||||
p.Stats.FoodSupply += 1
|
||||
p.Stats.Secrecy += 5
|
||||
p.Stats.Rebellion += 1
|
||||
p.Stats.Greed += 1
|
||||
p.Stats.Gullibility += 1
|
||||
return cardsim.MsgStr("Nobody ever meets a kobold merchant."), nil
|
||||
EffectsTable: map[FieldLabel]float64{
|
||||
Manufacturing: 3,
|
||||
Logistics: 4,
|
||||
Mining: 1,
|
||||
Alchemy: 1,
|
||||
Gadgetry: 1,
|
||||
Scavenging: 1,
|
||||
FoodSupply: 1,
|
||||
Secrecy: 5,
|
||||
Rebellion: 1,
|
||||
Greed: 1,
|
||||
Gullibility: 1,
|
||||
},
|
||||
Undo: func(p *Player) error {
|
||||
p.Stats.Manufacturing -= 3
|
||||
p.Stats.Logistics -= 4
|
||||
p.Stats.Mining -= 1
|
||||
p.Stats.Alchemy -= 1
|
||||
p.Stats.Gadgetry -= 1
|
||||
p.Stats.Scavenging -= 1
|
||||
p.Stats.FoodSupply -= 1
|
||||
p.Stats.Secrecy -= 5
|
||||
p.Stats.Rebellion -= 1
|
||||
p.Stats.Greed -= 1
|
||||
p.Stats.Gullibility -= 1
|
||||
return nil
|
||||
EnactionDesc: cardsim.MsgStr("Nobody ever meets a kobold merchant."),
|
||||
CanDo: YesWeAlsoCan,
|
||||
},
|
||||
CanDo: YesWeCan,
|
||||
},
|
||||
&BasicPolicy{
|
||||
&TablePolicy{
|
||||
UnenactedDesc: cardsim.MsgStr(`Brave but potentially headstrong, another prospective merchant has attended the meeting with the following plea. "We must take head-on the risk of improving the status of kobolds! Only by bravely attending the surface markets can we hope to achieve a true and lasting peace!"`),
|
||||
EnactedDesc: cardsim.MsgStr(`[current policy] Your merchants represent the tribe's peaceful ideals by visiting the surface openly despite occasional casaulties.`),
|
||||
Do: func(p *Player) (cardsim.Message, error) {
|
||||
p.Stats.BasePopulation -= 15
|
||||
p.Stats.Manufacturing += 3
|
||||
p.Stats.Logistics += 4
|
||||
p.Stats.Mining += 1
|
||||
p.Stats.Alchemy += 2
|
||||
p.Stats.Gadgetry += 2
|
||||
p.Stats.Hospitality += 1
|
||||
p.Stats.Publishing += 1
|
||||
p.Stats.ForeignRelExpense += 1
|
||||
p.Stats.Militarism -= 1
|
||||
p.Stats.Scavenging -= 1
|
||||
p.Stats.Secrecy -= 10
|
||||
p.Stats.ForeignRelations += 5
|
||||
p.Stats.FoodSupply += 3
|
||||
p.Stats.Greed += 1
|
||||
p.Stats.Madness -= 1
|
||||
p.Stats.Cruelty -= 1
|
||||
return cardsim.MsgStr("The tribe formally plans for attrition among its merchants."), nil
|
||||
EffectsTable: map[FieldLabel]float64{
|
||||
BasePopulation: -15,
|
||||
Manufacturing: 3,
|
||||
Logistics: 4,
|
||||
Mining: 2,
|
||||
Alchemy: 1,
|
||||
Gadgetry: 2,
|
||||
Hospitality: 1,
|
||||
Publishing: 1,
|
||||
ForeignRelExpense: 1,
|
||||
Militarism: -1,
|
||||
Scavenging: -1,
|
||||
Secrecy: -10,
|
||||
ForeignRelations: 5,
|
||||
FoodSupply: 3,
|
||||
Greed: 1,
|
||||
Madness: -1,
|
||||
Cruelty: -1,
|
||||
},
|
||||
Undo: func(p *Player) error {
|
||||
p.Stats.BasePopulation += 15
|
||||
p.Stats.Manufacturing -= 3
|
||||
p.Stats.Logistics -= 4
|
||||
p.Stats.Mining -= 1
|
||||
p.Stats.Alchemy -= 2
|
||||
p.Stats.Gadgetry -= 2
|
||||
p.Stats.Hospitality -= 1
|
||||
p.Stats.Publishing -= 1
|
||||
p.Stats.ForeignRelExpense -= 1
|
||||
p.Stats.Militarism += 1
|
||||
p.Stats.Scavenging += 1
|
||||
p.Stats.Secrecy += 10
|
||||
p.Stats.ForeignRelations -= 5
|
||||
p.Stats.FoodSupply -= 3
|
||||
p.Stats.Greed -= 1
|
||||
p.Stats.Madness -= 1
|
||||
p.Stats.Cruelty -= 1
|
||||
return nil
|
||||
EnactionDesc: cardsim.MsgStr("The tribe formally plans for attrition among its merchants."),
|
||||
CanDo: YesWeAlsoCan,
|
||||
},
|
||||
CanDo: YesWeCan,
|
||||
},
|
||||
&BasicPolicy{
|
||||
&TablePolicy{
|
||||
UnenactedDesc: cardsim.MsgStr(`A shady scavenger has offered to change careers and become a merchant. Their proposal is, "Surfacers aren't all monolithic. If we talk to the ones who are more greedy than prejudiced, we don't need magic or idealism. We can get in contact with lots of people who'll buy lots of things from us, even things that might not have a market if we worked with 'good' merchants."`),
|
||||
EnactedDesc: cardsim.MsgStr("[current policy] Your merchants work with the underworld of the surface, forging connections with the people who are ironically less likely to murder them."),
|
||||
Do: func(p *Player) (cardsim.Message, error) {
|
||||
p.Stats.BasePopulation -= 5
|
||||
p.Stats.Logistics += 1
|
||||
p.Stats.Scavenging += 3
|
||||
p.Stats.Alchemy += 2
|
||||
p.Stats.Gadgetry += 2
|
||||
p.Stats.Mining -= 1
|
||||
p.Stats.Manufacturing -= 1
|
||||
p.Stats.HiddenRelPenalty += 2 // High is bad on this hidden stat.
|
||||
p.Stats.Greed += 3
|
||||
p.Stats.Gullibility -= 2
|
||||
return cardsim.MsgStr("Kobold merchants are instantly suspected of black market connections."), nil
|
||||
EffectsTable: map[FieldLabel]float64{
|
||||
BasePopulation: -5,
|
||||
Logistics: 1,
|
||||
Scavenging: 3,
|
||||
Alchemy: 2,
|
||||
Gadgetry: 2,
|
||||
Mining: -1,
|
||||
Manufacturing: -1,
|
||||
HiddenRelPenalty: 2, //High is bad on this hidden stat.
|
||||
Greed: 3,
|
||||
Gullibility: -2,
|
||||
},
|
||||
Undo: func(p *Player) error {
|
||||
p.Stats.BasePopulation += 5
|
||||
p.Stats.Logistics -= 1
|
||||
p.Stats.Scavenging -= 3
|
||||
p.Stats.Alchemy -= 2
|
||||
p.Stats.Gadgetry -= 3
|
||||
p.Stats.Mining += 1
|
||||
p.Stats.Manufacturing += 1
|
||||
p.Stats.HiddenRelPenalty -= 2
|
||||
p.Stats.Greed -= 3
|
||||
p.Stats.Gullibility += 2
|
||||
return nil
|
||||
EnactionDesc: cardsim.MsgStr("Kobold merchants are instantly suspected of black market connections."),
|
||||
CanDo: YesWeAlsoCan,
|
||||
},
|
||||
CanDo: YesWeCan,
|
||||
},
|
||||
&BasicPolicy{
|
||||
UnenactedDesc: cardsim.MsgStr(`A mining forebold registered for the right to attend this meeting, and says, "Surfacers can't be trusted and we shouldn't be supporting them with trade. We need underground roads to ease trade with other kobolds. It's a massive investment, but think of the reward: a world of underground civilizations supporting each other."`),
|
||||
&TablePolicy{
|
||||
UnenactedDesc: cardsim.MsgStr(`A mining forebold registered for the right to attend this meeting to say, "Surfacers can't be trusted and we shouldn't be supporting them with trade. We need underground roads to ease trade with other kobolds. It's a massive investment, but think of the reward: a world of underground civilizations supporting each other."`),
|
||||
EnactedDesc: cardsim.MsgStr(`[currentpolicy] Your merchants have been directed to focus on trade with other kobolds, and your miners are perpetually tunnelling towards other kobold tribes in pursuit of the dream of a world of underground roads.`),
|
||||
Do: func(p *Player) (cardsim.Message, error) {
|
||||
p.Stats.Mining += 5
|
||||
p.Stats.Construction += 2
|
||||
p.Stats.Logistics += 4
|
||||
p.Stats.FoodSupply -= 1
|
||||
p.Stats.Secrecy += 15
|
||||
p.Stats.Gullibility -= 2
|
||||
return cardsim.MsgStr("The vast network of caverns under the surface of the world is not a natural phenomenon."), nil
|
||||
EffectsTable: map[FieldLabel]float64{
|
||||
Mining: 5,
|
||||
Construction: 2,
|
||||
Logistics: 3,
|
||||
FoodSupply: -1,
|
||||
Secrecy: 15,
|
||||
Gullibility: -2,
|
||||
},
|
||||
Undo: func(p *Player) error {
|
||||
p.Stats.Mining -= 5
|
||||
p.Stats.Construction -= 2
|
||||
p.Stats.Logistics -= 4
|
||||
p.Stats.FoodSupply += 1
|
||||
p.Stats.Secrecy -= 15
|
||||
p.Stats.Gullibility += 2
|
||||
return nil
|
||||
},
|
||||
CanDo: YesWeCan,
|
||||
EnactionDesc: cardsim.MsgStr("The vast network of caverns under the surface of the world is not a natural phenomenon."),
|
||||
CanDo: YesWeAlsoCan,
|
||||
},
|
||||
&VerbosePolicy{
|
||||
Default: &BasicPolicy{
|
||||
@ -395,7 +275,7 @@ var cards = []Card{
|
||||
UnenactedDesc: cardsim.MsgStr("Rejecting the illusion-based merchantry will cost us a great deal in trade and surface food, but we can certainly do it."),
|
||||
},
|
||||
&BasicPolicy{
|
||||
UnenactedDesc: cardsim.MsgStr("Rejecting our idealistic approach to surface trade will save a few lives directly, but consider how much we need the food we might buy!"),
|
||||
UnenactedDesc: cardsim.MsgStr("Rejecting our idealistic approach to surface trade will save a few lives directly, but consider how much we need the food we buy!"),
|
||||
},
|
||||
&BasicPolicy{
|
||||
UnenactedDesc: cardsim.MsgStr("The surfacers' governments would certainly prefer us to abandon our underworld connections in their cities. Do we care about that? We'd be giving up some excellent profits."),
|
||||
@ -412,32 +292,21 @@ var cards = []Card{
|
||||
Desc: cardsim.MsgStr("Nobody would ever say a kobold smells bad, but the constant burrowing, manufacturing, and metallurgy that is common to kobold life is still a recipe for tense muscles and filthy scales. A slate of proposals have come through to address this issue."),
|
||||
After: ShuffleIntoBottomHalf,
|
||||
Policies: []Policy{
|
||||
&BasicPolicy{
|
||||
&TablePolicy{
|
||||
UnenactedDesc: cardsim.MsgStr(`A calm and cheerful scribe says, "We should have public baths! We have more problems with flooding than drought in the deep underground, so we've got plenty of water. We just need to channel it. We could create a hub of social life while improving the health of everybody!"`),
|
||||
EnactedDesc: cardsim.MsgStr("[current policy] Public bathing is a staple social policy in the mines and warrens of your tribe."),
|
||||
Do: func(p *Player) (cardsim.Message, error) {
|
||||
p.Stats.Construction += 2
|
||||
p.Stats.Welfare += 1
|
||||
p.Stats.Healthcare += 1
|
||||
p.Stats.ParksExpense += 4
|
||||
p.Stats.Hospitality += 1
|
||||
p.Stats.Madness -= 1
|
||||
p.Stats.Cruelty -= 1
|
||||
p.Stats.Greed -= 1
|
||||
return cardsim.MsgStr("The company of others makes cold water tolerable."), nil
|
||||
EffectsTable: map[FieldLabel]float64{
|
||||
Construction: 2,
|
||||
Welfare: 1,
|
||||
Healthcare: 1,
|
||||
ParksExpense: 4,
|
||||
Hospitality: 1,
|
||||
Madness: -1,
|
||||
Cruelty: -1,
|
||||
Greed: -1,
|
||||
},
|
||||
Undo: func(p *Player) error {
|
||||
p.Stats.Construction -= 2
|
||||
p.Stats.Welfare -= 1
|
||||
p.Stats.Healthcare -= 1
|
||||
p.Stats.ParksExpense -= 4
|
||||
p.Stats.Hospitality -= 1
|
||||
p.Stats.Madness += 1
|
||||
p.Stats.Cruelty += 1
|
||||
p.Stats.Greed += 1
|
||||
return nil
|
||||
},
|
||||
CanDo: YesWeCan,
|
||||
EnactionDesc: cardsim.MsgStr("The company of others makes cold water tolerable."),
|
||||
CanDo: YesWeAlsoCan,
|
||||
},
|
||||
&BasicPolicy{
|
||||
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."`),
|
||||
|
@ -260,6 +260,99 @@ func (b *BasicPolicy) Is(p Policy) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// TablePolicy is a Policy where all numerical changes are defined by
|
||||
// adding a constant to some set of fields (defined by `EffectsTable“)
|
||||
// and subtracting it back out when de-enacting. If the currently
|
||||
// enacted option is re-enacted, it refunds the player's action point.
|
||||
type TablePolicy struct {
|
||||
Desc cardsim.Message
|
||||
UnenactedDesc cardsim.Message
|
||||
EnactedDesc cardsim.Message
|
||||
NothingChanged cardsim.Message
|
||||
EffectsTable map[FieldLabel]float64
|
||||
EnactionDesc cardsim.Message
|
||||
CanDo func(*TablePolicy, *Player) bool
|
||||
|
||||
CurrentlyEnacted bool
|
||||
LastEnactedPolicy Policy
|
||||
LastEnactedIdx int
|
||||
}
|
||||
|
||||
func YesWeAlsoCan(*TablePolicy, *Player) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// LastEnacted notifies t about the last-enacted policy in its group. It updates
|
||||
// t.currentlyEnacted accordingly.
|
||||
func (t *TablePolicy) LastEnacted(i int, p Policy) {
|
||||
t.LastEnactedPolicy = p
|
||||
t.LastEnactedIdx = i
|
||||
t.CurrentlyEnacted = t.Is(p)
|
||||
}
|
||||
|
||||
// OptionText implements CardOption.
|
||||
func (t *TablePolicy) OptionText(*Player) (cardsim.Message, error) {
|
||||
if t.CurrentlyEnacted {
|
||||
if t.EnactedDesc == nil {
|
||||
return nil, ErrUnimplemented
|
||||
}
|
||||
return t.EnactedDesc, nil
|
||||
}
|
||||
if t.UnenactedDesc == nil {
|
||||
return nil, ErrUnimplemented
|
||||
}
|
||||
return t.UnenactedDesc, nil
|
||||
}
|
||||
|
||||
// Enact implements CardOption.
|
||||
func (t *TablePolicy) Enact(p *Player) (cardsim.Message, error) {
|
||||
if t.EffectsTable == nil {
|
||||
return nil, ErrUnimplemented
|
||||
}
|
||||
if t.CurrentlyEnacted {
|
||||
p.ActionsRemaining++
|
||||
if t.NothingChanged == nil {
|
||||
t.NothingChanged = cardsim.MsgStr("You continue your current approach.")
|
||||
}
|
||||
return t.NothingChanged, nil
|
||||
}
|
||||
var errs cardsim.ErrorCollector
|
||||
for label, amount := range t.EffectsTable {
|
||||
errs.Add(p.Stats.Add(label, amount))
|
||||
}
|
||||
return t.EnactionDesc, errs.Emit()
|
||||
}
|
||||
|
||||
// Unenact implements Policy.
|
||||
func (t *TablePolicy) Unenact(p *Player) error {
|
||||
if !t.CurrentlyEnacted {
|
||||
return ErrPolicyNotEnacted
|
||||
}
|
||||
var errs cardsim.ErrorCollector
|
||||
for label, amount := range t.EffectsTable {
|
||||
errs.Add(p.Stats.Add(label, -amount))
|
||||
}
|
||||
return errs.Emit()
|
||||
}
|
||||
|
||||
// Enabled implements CardOption.
|
||||
func (t *TablePolicy) Enabled(p *Player) bool {
|
||||
if t.CurrentlyEnacted {
|
||||
return true
|
||||
}
|
||||
if t.CanDo == nil {
|
||||
panic(ErrUnimplemented)
|
||||
}
|
||||
return t.CanDo(t, p)
|
||||
}
|
||||
|
||||
func (t *TablePolicy) Is(p Policy) bool {
|
||||
if o, ok := p.(*TablePolicy); ok {
|
||||
return o == t
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// A VerbosePolicy is a group of related policies pretending to all be the same
|
||||
// policy. Which policy is used is determined by what the previous policy for
|
||||
// the card was (as reported via a call to LastEnacted):
|
||||
|
@ -1,6 +1,10 @@
|
||||
package koboldsim
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"git.chromaticdragon.app/kistaro/CardSimEngine/cardsim"
|
||||
)
|
||||
|
||||
@ -164,3 +168,81 @@ func NewKoboldMine() *KoboldMine {
|
||||
Secrecy: 95,
|
||||
}
|
||||
}
|
||||
|
||||
// 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 (
|
||||
Alchemy FieldLabel = "Alchemy"
|
||||
Authoritarianism FieldLabel = "Authoritarianism"
|
||||
BasePopulation FieldLabel = "BasePopulation"
|
||||
Bureaucracy FieldLabel = "Bureaucracy"
|
||||
Construction FieldLabel = "Construction"
|
||||
Cruelty FieldLabel = "Cruelty"
|
||||
Education FieldLabel = "Education"
|
||||
FoodSupply FieldLabel = "FoodSupply"
|
||||
ForeignRelations FieldLabel = "ForeignRelations"
|
||||
ForeignRelExpense FieldLabel = "ForeignRelExpense"
|
||||
Gadgetry FieldLabel = "Gadgetry"
|
||||
Greed FieldLabel = "Greed"
|
||||
Gullibility FieldLabel = "Gullibility"
|
||||
Healthcare FieldLabel = "Healthcare"
|
||||
HiddenRelPenalty FieldLabel = "HiddenRelPenalty"
|
||||
Hospitality FieldLabel = "Hospitality"
|
||||
Logistics FieldLabel = "Logistics"
|
||||
Madness FieldLabel = "Madness"
|
||||
Manufacturing FieldLabel = "Manufacturing"
|
||||
Militarism FieldLabel = "Militarism"
|
||||
Mining FieldLabel = "Mining"
|
||||
ParksExpense FieldLabel = "ParksExpense"
|
||||
Publishing FieldLabel = "Publishing"
|
||||
Rebellion FieldLabel = "Rebellion"
|
||||
Scavenging FieldLabel = "Scavenging"
|
||||
Secrecy FieldLabel = "Secrecy"
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
package koboldsim
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"golang.org/x/exp/constraints"
|
||||
@ -25,12 +27,45 @@ func Mean(vals ...float64) float64 {
|
||||
return total / float64(len(vals))
|
||||
}
|
||||
|
||||
func clamp[T constraints.Ordered](val, low, high T) T {
|
||||
if val < low {
|
||||
return low
|
||||
// clamp returns the middle of the three provided values. It doesn't
|
||||
// matter which order the values are in. This function is known as `mid` in
|
||||
// Pico-8's library, among others. It is usually used to clamp a value to a
|
||||
// range, and it doesn't care which order the range is written in.
|
||||
func clamp[T constraints.Ordered](a, b, c T) T {
|
||||
if a <= b && a <= c {
|
||||
// `a` is least; mid is lower of `b` or `c`
|
||||
if b <= c {
|
||||
return b
|
||||
}
|
||||
if val > high {
|
||||
return high
|
||||
return c
|
||||
}
|
||||
return val
|
||||
if a >= b && a >= c {
|
||||
// `a` is most; mid is greater of `b` or `c`
|
||||
if b >= c {
|
||||
return b
|
||||
}
|
||||
return c
|
||||
}
|
||||
// `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
|
||||
}
|
||||
|
Reference in New Issue
Block a user