Compare commits
41 Commits
refactor-p
...
Rewrite-St
Author | SHA1 | Date | |
---|---|---|---|
37d3b639bf
|
|||
39e4c94b6f | |||
99c5e5af6d | |||
a237fa81bf
|
|||
68cab7d2be
|
|||
2d16f97314
|
|||
8a28f38d4d
|
|||
b2099586fc
|
|||
c4427885ca | |||
219ff33d66
|
|||
5b860135e9
|
|||
d2a00d2044
|
|||
ba5171fd67
|
|||
e018bd0ad6 | |||
16f452b08f | |||
18b221d972 | |||
e45c5b3711 | |||
f53beb4b17 | |||
c30254a36d | |||
350fd0f777
|
|||
816b5e8e7a
|
|||
e61d7571f0 | |||
6d6d0f5c8b | |||
3ca17d8881 | |||
976610b1bb | |||
a4692712cc | |||
2ca8f3ed13 | |||
0a39cc76d6
|
|||
de7092cf4b
|
|||
d434e50897
|
|||
95a30cb522 | |||
f3fd0c582f | |||
de98cf8fb3 | |||
d66e17a279 | |||
caa5c2e60c | |||
159508b202 | |||
301d8ae161
|
|||
e0dad09045 | |||
ccd141ddc5 | |||
d2f89f5bd4 | |||
f7bed6c4b9
|
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"
|
||||
// }
|
||||
}
|
9
go.mod
9
go.mod
@ -2,4 +2,11 @@ module git.chromaticdragon.app/kistaro/KoboldSim
|
||||
|
||||
go 1.20
|
||||
|
||||
require git.chromaticdragon.app/kistaro/CardSimEngine v0.1.3
|
||||
require git.chromaticdragon.app/kistaro/CardSimEngine v0.5.0
|
||||
|
||||
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
|
||||
)
|
||||
|
16
go.sum
16
go.sum
@ -1,4 +1,12 @@
|
||||
git.chromaticdragon.app/kistaro/CardSimEngine v0.1.2 h1:+8KVFhSxXbQO7CPzmL89sJ+qjgU4J42Z5OinGuDMR0U=
|
||||
git.chromaticdragon.app/kistaro/CardSimEngine v0.1.2/go.mod h1:VFaOagdbtM6gH87ioHent8v76nDh9PddpymMqWdrLfI=
|
||||
git.chromaticdragon.app/kistaro/CardSimEngine v0.1.3 h1:rNaDDXnPVoMpavpx4vR9k30sl0nO0BnCe32nZbAF2IM=
|
||||
git.chromaticdragon.app/kistaro/CardSimEngine v0.1.3/go.mod h1:VFaOagdbtM6gH87ioHent8v76nDh9PddpymMqWdrLfI=
|
||||
git.chromaticdragon.app/kistaro/CardSimEngine v0.5.0 h1:o4ncTVfDgax3w2tVhkap4/ZrZDe13YnNNJ0pe7zaqjM=
|
||||
git.chromaticdragon.app/kistaro/CardSimEngine v0.5.0/go.mod h1:FYuoJHaK7lDI8Fwf4lZY2Y+8P9zavT4oLvSFUG6drw4=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
|
||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
|
1243
koboldsim/cards.go
1243
koboldsim/cards.go
File diff suppressed because it is too large
Load Diff
@ -42,12 +42,62 @@ type Policy interface {
|
||||
// previously, and undoes it before doing a different one. It is always valid
|
||||
// to draw.
|
||||
type SwitchingCard struct {
|
||||
Name cardsim.Message
|
||||
Desc cardsim.Message
|
||||
IsUrgent bool
|
||||
After func(Card, *Player, CardOption) error
|
||||
Policies []Policy
|
||||
// Name contains the name of the card, displayed as its title in the
|
||||
// action selection menu and in the card detail page itself.
|
||||
Name cardsim.Message
|
||||
|
||||
// Desc contains the event description for the card, displayed in the
|
||||
// card detail page.
|
||||
Desc cardsim.Message
|
||||
|
||||
// IsUrgent marks a card as urgent. If the player has any urgent cards
|
||||
// in hand, they cannot act on any non-urgent cards (or permanent actions
|
||||
// not marked as urgent).
|
||||
IsUrgent bool
|
||||
|
||||
// After is invoked after the player has chosen to act on this card and
|
||||
// the chosen option has been fully enacted. If the card should be returned
|
||||
// to the deck, After is responsible for doing this!
|
||||
//
|
||||
// If After is not provided, ShuffleIntoBottomHalf is used as a fallback.
|
||||
//
|
||||
// The first argument to After is the card itself. This will be type
|
||||
// *SwitchingCard. It's represented as Card so general "After" functions
|
||||
// that can be used with multiple card types (for example,
|
||||
// ShuffleIntoBottomHalf) can be trivially implemented.
|
||||
//
|
||||
// If the card cannot be drawn into the hand because it has an IsValid
|
||||
// check and the check fails, After is invoked with a nil CardOption.
|
||||
// ShuffleIntoBottomHalf works just fine with a nil argument here.
|
||||
// (It doesn't care about the CardOption at all.)
|
||||
After func(Card, *Player, CardOption) error
|
||||
|
||||
// IsValid is used to check whether the card can be drawn into the hand.
|
||||
// If it cannot, After is immediately invoked (whenever the card was
|
||||
// being drawn, which is probably the "draw" stage of the turn but can
|
||||
// happen any time if something else causes the player to draw cards) with
|
||||
// a nil CardOption (because no option was selected) and the SwitchingCard
|
||||
// does not invoke any option and does not change its active policy.
|
||||
//
|
||||
// The first argument to IsValid is the card itself. This will be type
|
||||
// *SwitchingCard. It's presented via the Card interface to support
|
||||
// general validity functions that could be used with arbitrary kinds of Card.
|
||||
IsValid func(Card, *Player) bool
|
||||
|
||||
// Policies contains the options the player may choose between. Policy is
|
||||
// a more specific type than CardOption; a Policy can be un-enacted.
|
||||
// Unenactment of the previous policy before selecting a new one is the
|
||||
// core feature of SwitchingCard.
|
||||
Policies []Policy
|
||||
|
||||
// lastPolicy stores the last policy selected for this card. It's used
|
||||
// extensively by SwitchingCard's logic.
|
||||
lastPolicy Policy
|
||||
|
||||
// ShowUnavailable controls whether options for which Enabled() = false
|
||||
// should be presented to the player at all. Indexes for "last enacted"
|
||||
// still refer to the original list, not the shortened one.
|
||||
ShowUnavailable bool
|
||||
}
|
||||
|
||||
// Title implements Card.
|
||||
@ -61,7 +111,12 @@ func (s *SwitchingCard) Urgent(*Player) bool {
|
||||
}
|
||||
|
||||
// Drawn implements Card.
|
||||
func (s *SwitchingCard) Drawn(*Player) bool {
|
||||
func (s *SwitchingCard) Drawn(p *Player) bool {
|
||||
if s.IsValid != nil && !s.IsValid(s, p) {
|
||||
err := s.Then(p, nil)
|
||||
p.ReportError(err) // can't do anything with the error right now
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -71,7 +126,7 @@ func (s *SwitchingCard) EventText(*Player) (cardsim.Message, error) {
|
||||
}
|
||||
|
||||
// Options implements Card.
|
||||
func (s *SwitchingCard) Options(*Player) ([]CardOption, error) {
|
||||
func (s *SwitchingCard) Options(player *Player) ([]CardOption, error) {
|
||||
lastIdx := -1
|
||||
for i, p := range s.Policies {
|
||||
if p.Is(s.lastPolicy) {
|
||||
@ -79,32 +134,45 @@ func (s *SwitchingCard) Options(*Player) ([]CardOption, error) {
|
||||
break
|
||||
}
|
||||
}
|
||||
ret := make([]CardOption, len(s.Policies))
|
||||
for i, p := range s.Policies {
|
||||
ret := make([]CardOption, 0, len(s.Policies))
|
||||
for _, p := range s.Policies {
|
||||
p.LastEnacted(lastIdx, s.lastPolicy)
|
||||
ret[i] = p
|
||||
if s.ShowUnavailable || p.Enabled(player) {
|
||||
ret = append(ret, p)
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// Then implements Card.
|
||||
func (s *SwitchingCard) Then(p *Player, o CardOption) error {
|
||||
newPolicy := o.(Policy)
|
||||
var errs cardsim.ErrorCollector
|
||||
if s.lastPolicy != nil && !newPolicy.Is(s.lastPolicy) {
|
||||
err := s.lastPolicy.Unenact(p)
|
||||
if cardsim.IsSeriousError(err) {
|
||||
return err
|
||||
if o != nil {
|
||||
newPolicy := o.(Policy)
|
||||
if s.lastPolicy != nil && !newPolicy.Is(s.lastPolicy) {
|
||||
err := s.lastPolicy.Unenact(p)
|
||||
if cardsim.IsSeriousError(err) {
|
||||
return err
|
||||
}
|
||||
errs.Add(err)
|
||||
}
|
||||
errs.Add(err)
|
||||
s.lastPolicy = o.(Policy)
|
||||
}
|
||||
s.lastPolicy = o.(Policy)
|
||||
if s.After != nil {
|
||||
errs.Add(s.After(s, p, o))
|
||||
} else {
|
||||
// Fallback: Shuffle the card back into the bottom half of the deck.
|
||||
errs.Add(ShuffleIntoBottomHalf(s, p, o))
|
||||
|
||||
}
|
||||
return errs.Emit()
|
||||
}
|
||||
|
||||
// CurrentlyEnacted returns the currently enacted Policy, if any.
|
||||
func (s *SwitchingCard) CurrentlyEnacted() Policy {
|
||||
return s.lastPolicy
|
||||
}
|
||||
|
||||
// BasicPolicy is a straightfoward implementation of Policy. If the currently
|
||||
// enacted option is re-enacted, it refunds the player's action point.
|
||||
type BasicPolicy struct {
|
||||
@ -114,25 +182,29 @@ type BasicPolicy struct {
|
||||
NothingChanged cardsim.Message
|
||||
Do func(*Player) (cardsim.Message, error)
|
||||
Undo func(*Player) error
|
||||
CanDo func(*Player) bool
|
||||
CanDo func(*BasicPolicy, *Player) bool
|
||||
|
||||
currentlyEnacted bool
|
||||
CurrentlyEnacted bool
|
||||
LastEnactedPolicy Policy
|
||||
LastEnactedIdx int
|
||||
}
|
||||
|
||||
// YesWeCan returns true. It's the default value for BasicPolicy.CanDo / BasicPolicy.CanUndo.
|
||||
func YesWeCan(*Player) bool {
|
||||
func YesWeCan(*BasicPolicy, *Player) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// LastEnacted notifies b about the last-enacted policy in its group. It updates
|
||||
// b.currentlyEnacted accordingly.
|
||||
func (b *BasicPolicy) LastEnacted(_ int, p Policy) {
|
||||
b.currentlyEnacted = b.Is(p)
|
||||
func (b *BasicPolicy) LastEnacted(i int, p Policy) {
|
||||
b.LastEnactedPolicy = p
|
||||
b.LastEnactedIdx = i
|
||||
b.CurrentlyEnacted = b.Is(p)
|
||||
}
|
||||
|
||||
// OptionText implements CardOption.
|
||||
func (b *BasicPolicy) OptionText(*Player) (cardsim.Message, error) {
|
||||
if b.currentlyEnacted {
|
||||
if b.CurrentlyEnacted {
|
||||
if b.EnactedDesc == nil {
|
||||
return nil, ErrUnimplemented
|
||||
}
|
||||
@ -149,22 +221,19 @@ func (b *BasicPolicy) Enact(p *Player) (cardsim.Message, error) {
|
||||
if b.Do == nil {
|
||||
return nil, ErrUnimplemented
|
||||
}
|
||||
if b.currentlyEnacted {
|
||||
if b.CurrentlyEnacted {
|
||||
p.ActionsRemaining++
|
||||
if b.NothingChanged == nil {
|
||||
b.NothingChanged = cardsim.MsgStr("You continue your current approach.")
|
||||
}
|
||||
return b.NothingChanged, nil
|
||||
}
|
||||
if b.Enabled(p) {
|
||||
return b.Do(p)
|
||||
}
|
||||
return nil, ErrOptionNotEnabled
|
||||
return b.Do(p)
|
||||
}
|
||||
|
||||
// Unenact implements Policy.
|
||||
func (b *BasicPolicy) Unenact(p *Player) error {
|
||||
if !b.currentlyEnacted {
|
||||
if !b.CurrentlyEnacted {
|
||||
return ErrPolicyNotEnacted
|
||||
}
|
||||
if b.Undo == nil {
|
||||
@ -175,13 +244,13 @@ func (b *BasicPolicy) Unenact(p *Player) error {
|
||||
|
||||
// Enabled implements CardOption.
|
||||
func (b *BasicPolicy) Enabled(p *Player) bool {
|
||||
if b.currentlyEnacted {
|
||||
if b.CurrentlyEnacted {
|
||||
return true
|
||||
}
|
||||
if b.CanDo == nil {
|
||||
panic(ErrUnimplemented)
|
||||
}
|
||||
return b.CanDo(p)
|
||||
return b.CanDo(b, p)
|
||||
}
|
||||
|
||||
func (b *BasicPolicy) Is(p Policy) bool {
|
||||
@ -191,26 +260,119 @@ 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):
|
||||
//
|
||||
// * If no policy has yet been enacted, use FirstTime.
|
||||
// * If a policy has been enacted, use the Policy at the slot in Variants
|
||||
// that corresponds to the slot (on the Card) of the currently-enacted policy.
|
||||
// * If the policy retrieved in this way returns ErrUnimplemented, throw away
|
||||
// its response and use Default instead. For Enabled, which does not have
|
||||
// an error component to its return value, look for ErrUnimplemented as the
|
||||
// argument to a Panic call, instead.
|
||||
// * If the policy retrieved in this way returns ErrKeepMessage when Enact
|
||||
// is called, it calls Default for the side effects but ignores its message,
|
||||
// retaining the message from the original call. This is to avoid having to
|
||||
// repeat the same Enact function except with different text each time.
|
||||
// OptionText does this too, even though OptionText doesn't have side effects,
|
||||
// so the same helper function can create a "constant message" callback
|
||||
// that works the same for both fields so someone implementing a card won't
|
||||
// accidentally fail to enact their policy's effects by using the wrong one
|
||||
// in the wrong slot.
|
||||
// - If no policy has yet been enacted, use FirstTime.
|
||||
// - If a policy has been enacted, use the Policy at the slot in Variants
|
||||
// that corresponds to the slot (on the Card) of the currently-enacted policy.
|
||||
// - If the policy retrieved in this way returns ErrUnimplemented, throw away
|
||||
// its response and use Default instead. For Enabled, which does not have
|
||||
// an error component to its return value, look for ErrUnimplemented as the
|
||||
// argument to a Panic call, instead.
|
||||
// - If the policy retrieved in this way returns ErrKeepMessage when Enact
|
||||
// is called, it calls Default for the side effects but ignores its message,
|
||||
// retaining the message from the original call. This is to avoid having to
|
||||
// repeat the same Enact function except with different text each time.
|
||||
// OptionText does this too, even though OptionText doesn't have side effects,
|
||||
// so the same helper function can create a "constant message" callback
|
||||
// that works the same for both fields so someone implementing a card won't
|
||||
// accidentally fail to enact their policy's effects by using the wrong one
|
||||
// in the wrong slot.
|
||||
type VerbosePolicy struct {
|
||||
Default Policy
|
||||
FirstTime Policy
|
||||
@ -257,7 +419,7 @@ func (v *VerbosePolicy) fillDefaults() {
|
||||
for len(v.Variants) <= v.lastIdx {
|
||||
v.Variants = append(v.Variants, v.Default)
|
||||
}
|
||||
if v.lastIdx > 0 && v.Variants[v.lastIdx] == nil {
|
||||
if v.lastIdx >= 0 && v.Variants[v.lastIdx] == nil {
|
||||
v.Variants[v.lastIdx] = v.Default
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,10 @@
|
||||
package koboldsim
|
||||
|
||||
import "git.chromaticdragon.app/kistaro/CardSimEngine/cardsim"
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.chromaticdragon.app/kistaro/CardSimEngine/cardsim"
|
||||
)
|
||||
|
||||
func InitPlayer() *Player {
|
||||
p := cardsim.InitPlayer(NewKoboldMine())
|
||||
@ -13,6 +17,16 @@ func InitPlayer() *Player {
|
||||
Name: cardsim.MsgStr("All Stats"),
|
||||
Intro: cardsim.MsgStr("All available statistics."),
|
||||
},
|
||||
&cardsim.BasicStatsPanel[*KoboldMine]{
|
||||
Name: cardsim.MsgStr("Per Capita Economic"),
|
||||
Intro: cardsim.MsgStr("Yield and Investment per Capita"),
|
||||
Filter: cardsim.All(
|
||||
cardsim.VisibleOrDebug[*KoboldMine],
|
||||
func(p *Player, s cardsim.Stat) bool {
|
||||
return strings.Contains(s.StatName(), "Productivity") || strings.Contains(s.StatName(), "Investment")
|
||||
},
|
||||
),
|
||||
},
|
||||
}
|
||||
p.Prompt = &cardsim.BasicStatsPanel[*KoboldMine]{
|
||||
Name: cardsim.MsgStr("The Kobold Mine"),
|
||||
@ -21,6 +35,7 @@ func InitPlayer() *Player {
|
||||
"Kobolds",
|
||||
"Total Sector Income",
|
||||
"Total Government Expense",
|
||||
"Tax Rate",
|
||||
),
|
||||
}
|
||||
p.State = cardsim.GameActive
|
||||
|
@ -1,88 +1,248 @@
|
||||
package koboldsim
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"git.chromaticdragon.app/kistaro/CardSimEngine/cardsim"
|
||||
)
|
||||
|
||||
// KoboldMine is the state of a kobold mine.
|
||||
type KoboldMine struct {
|
||||
Kobolds cardsim.Stored[int64]
|
||||
BasePopulation float64 `cardsim:"stathidden"`
|
||||
DragonCount float64 `cardsim:"stat" cardsim_name:"Dragon Population"`
|
||||
|
||||
SectorMiningIncome cardsim.Stored[float64]
|
||||
SectorScavengingIncome cardsim.Stored[float64]
|
||||
Mining float64 `cardsim:"stathidden" cardsim_name:"Mining"`
|
||||
Scavenging float64 `cardsim:"stathidden" cardsim_name:"Scavenging"`
|
||||
Alchemy float64 `cardsim:"stathidden" cardsim_name:"Alchemy"`
|
||||
Hospitality float64 `cardsim:"stathidden" cardsim_name:"Hospitality"`
|
||||
Agriculture float64 `cardsim:"stathidden" cardsim_name:"Agriculture"`
|
||||
Manufacturing float64 `cardsim:"stathidden" cardsim_name:"Manufacturing"`
|
||||
PlanarConnections float64 `cardsim:"stathidden" cardsim_name:"Planar Connections"`
|
||||
Publishing float64 `cardsim:"stathidden" cardsim_name:"Publishing"`
|
||||
Forestry float64 `cardsim:"stathidden" cardsim_name:"Forestry"`
|
||||
Finance float64 `cardsim:"stathidden" cardsim_name:"Finance"`
|
||||
Gadgetry float64 `cardsim:"stathidden" cardsim_name:"Gadgetry"`
|
||||
Fishing float64 `cardsim:"stathidden" cardsim_name:"Fishing"`
|
||||
Construction float64 `cardsim:"stathidden" cardsim_name:"Construction"`
|
||||
|
||||
Propaganda float64 `cardsim:"stathidden" cardsim_name:"Propaganda"`
|
||||
Bureaucracy float64 `cardsim:"stathidden" cardsim_name:"Bureaucracy"`
|
||||
Militarism float64 `cardsim:"stathidden" cardsim_name:"Militarism"`
|
||||
Welfare float64 `cardsim:"stathidden" cardsim_name:"Social Safety Net"`
|
||||
Logistics float64 `cardsim:"stathidden" cardsim_name:"Logistics"`
|
||||
DragonSubs float64 `cardsim:"stathidden" cardsim_name:"Dragon Subsidies"`
|
||||
ResearchSubs float64 `cardsim:"stathidden" cardsim_name:"Research"`
|
||||
Education float64 `cardsim:"stathidden" cardsim_name:"Education"`
|
||||
Healthcare float64 `cardsim:"stathidden" cardsim_name:"Healthcare"`
|
||||
ForeignRelExpense float64 `cardsim:"stathidden" cardsim_name:"Diplomatic Investment"`
|
||||
ParksExpense float64 `cardsim:"stathidden" cardsim_name:"City Beautification"`
|
||||
Faith float64 `cardsim:"stathidden" cardsim_name:"Faith"`
|
||||
|
||||
FoodSupply float64 `cardsim:"stathidden"`
|
||||
ForeignRelations float64 `cardsim:"stathidden"`
|
||||
HiddenRelPenalty float64 `cardsim:"stathidden"` // Lower is better.
|
||||
Secrecy float64 `cardsim:"stathidden"`
|
||||
Rebellion float64 `cardsim:"stathidden"`
|
||||
Madness float64 `cardsim:"stathidden"`
|
||||
Cruelty float64 `cardsim:"stat"`
|
||||
Greed float64 `cardsim:"stathidden"`
|
||||
Gullibility float64 `cardsim:"stathidden"`
|
||||
Authoritarianism float64 `cardsim:"stat"`
|
||||
|
||||
Taxation float64 `cardsim:"stat"`
|
||||
TaxEvasion float64 `cardsim:"stat"`
|
||||
Squalor float64 `cardsim:"stat"`
|
||||
// Idea for tax evasion. The corruption stat should increase tax evasion, while the squalor stat should reduce it. The link with corruption should be obvious, but for squalor: when the economy improves, more people have resources with which to attempt to protect their income. This is part of why rich people can evade taxes more effectively than poor people. It's not ALL lobbying advantage.
|
||||
|
||||
GovBureaucracyExpense cardsim.Stored[float64]
|
||||
GovWarExpense cardsim.Stored[float64]
|
||||
}
|
||||
|
||||
func (k *KoboldMine) ProductivityFunc(s *cardsim.Stored[float64]) func() float64 {
|
||||
return func() float64 {
|
||||
return s.Value * float64(k.Kobolds.Value)
|
||||
}
|
||||
func (k *KoboldMine) Kobolds() int64 {
|
||||
return int64((k.BasePopulation * (k.Healthcare + 1)) * (k.FoodSupply / 10) * (1 + k.TrueForeignRelations()/100))
|
||||
}
|
||||
|
||||
func (k *KoboldMine) TotalSectorIncome() float64 {
|
||||
return float64(k.Kobolds.Value) * (k.SectorMiningIncome.Value + k.SectorScavengingIncome.Value)
|
||||
const (
|
||||
MinFoodSupply = 18
|
||||
MaxFoodSupply = 37
|
||||
MinForeignRelations = -11
|
||||
MaxForeignRelations = 9
|
||||
)
|
||||
|
||||
func (k *KoboldMine) DisplayedFoodSupply() float64 {
|
||||
return 100 * (k.FoodSupply - MinFoodSupply) / (MaxFoodSupply - MinFoodSupply) //This returns a Policy Implementation Percentage, so that the player can see how good they are at capturing food supply. When it becomes possible for Food Supply to hit 0 and end the game in famine, the minimum survivable food supply PIP should be calculated and returned to the player as a warned-against failure threshold.
|
||||
}
|
||||
|
||||
func (k *KoboldMine) TotalGovExpense() float64 {
|
||||
return float64(k.Kobolds.Value) * (k.GovBureaucracyExpense.Value + k.GovWarExpense.Value)
|
||||
//func (k *KoboldMine) StatObesity() float64 {
|
||||
//return (100 / (2.3 + math.Exp(-0.04*(k.DisplayedFoodSupply()-37)))) + 100/(2.3+math.Exp(-0.04*(k.StatObesogenicity()-37)))
|
||||
//}
|
||||
|
||||
func (k *KoboldMine) TrueForeignRelations() float64 {
|
||||
openness := 100 - clamp(k.Secrecy, 0, 100)
|
||||
effectiveRel := clamp(k.ForeignRelations, -100, 100) - k.HiddenRelPenalty
|
||||
return openness / 100 * effectiveRel
|
||||
}
|
||||
|
||||
func (k *KoboldMine) DisplayedForeignRelations() float64 {
|
||||
return 100 * (k.ForeignRelations - MinForeignRelations) / (MaxForeignRelations - MinForeignRelations)
|
||||
}
|
||||
|
||||
func (k *KoboldMine) DisplayedSecrecy() float64 {
|
||||
return k.Secrecy
|
||||
}
|
||||
|
||||
func (k *KoboldMine) StatChaos() float64 {
|
||||
return Mean(k.Rebellion, k.Madness, k.Cruelty)
|
||||
}
|
||||
|
||||
// This is the "crimes of chaos" stat for my crime calculations. It will also be displayed to the player as just Chaos. The player can see Cruelty, but not Madness or Rebellion. I'm concerned some players may think, "Oh, chaos is a good thing, I want more of that as long as it's not cruel!" In that case, they'll have mad, rebellious colonies that commit crimes against those perceived to be cruel...
|
||||
|
||||
func (k *KoboldMine) StatCorruption() float64 {
|
||||
return Mean(k.Greed, k.Gullibility, k.Authoritarianism)
|
||||
}
|
||||
|
||||
// This is the "crimes of cunning" stat for my crime calculations. It will also be displayed to the player as just Corruption. The player can see Authoritarianism, but they'll have to infer the importance of Greed and Gullibility.
|
||||
|
||||
func (k *KoboldMine) Stats() []cardsim.Stat {
|
||||
stats := cardsim.ExtractStats(k)
|
||||
funcs := []cardsim.Stat{
|
||||
cardsim.StatFunc(
|
||||
"Total Sector Mining Income",
|
||||
k.ProductivityFunc(&k.SectorMiningIncome),
|
||||
"Foreign Relations",
|
||||
k.DisplayedForeignRelations,
|
||||
),
|
||||
cardsim.StatFunc(
|
||||
"Total Sector Scavenging Income",
|
||||
k.ProductivityFunc(&k.SectorScavengingIncome),
|
||||
"Secrecy",
|
||||
k.DisplayedSecrecy,
|
||||
),
|
||||
cardsim.StatFunc(
|
||||
"Total Government Bureaucracy Expense",
|
||||
k.ProductivityFunc(&k.GovBureaucracyExpense),
|
||||
"Food Supply",
|
||||
k.DisplayedFoodSupply,
|
||||
),
|
||||
cardsim.StatFunc(
|
||||
"Total Government War Expense",
|
||||
k.ProductivityFunc(&k.GovWarExpense),
|
||||
),
|
||||
cardsim.StatFunc(
|
||||
"Total Sector Income",
|
||||
k.TotalSectorIncome,
|
||||
),
|
||||
cardsim.StatFunc(
|
||||
"Total Government Expense",
|
||||
k.TotalGovExpense,
|
||||
"Kobolds",
|
||||
k.Kobolds,
|
||||
),
|
||||
}
|
||||
stats = append(stats, funcs...)
|
||||
cardsim.SortStats(stats)
|
||||
// cardsim.SortStats(stats)
|
||||
return stats
|
||||
}
|
||||
|
||||
func NewKoboldMine() *KoboldMine {
|
||||
return &KoboldMine{
|
||||
Kobolds: cardsim.Stored[int64]{
|
||||
Name: "Kobolds",
|
||||
Value: 1000,
|
||||
},
|
||||
SectorMiningIncome: cardsim.Stored[float64]{
|
||||
Name: "Sector Mining Income",
|
||||
Value: 0.15,
|
||||
},
|
||||
SectorScavengingIncome: cardsim.Stored[float64]{
|
||||
Name: "Sector Scavenging Income",
|
||||
Value: 0.1,
|
||||
},
|
||||
GovBureaucracyExpense: cardsim.Stored[float64]{
|
||||
Name: "Government Bureaucracy Expense",
|
||||
Value: 0.05,
|
||||
},
|
||||
GovWarExpense: cardsim.Stored[float64]{
|
||||
Name: "Government War Expense",
|
||||
Value: 0.1,
|
||||
},
|
||||
BasePopulation: 1025,
|
||||
Mining: 0,
|
||||
Scavenging: 0,
|
||||
Alchemy: 0,
|
||||
Hospitality: 0,
|
||||
Agriculture: 0,
|
||||
Manufacturing: 0,
|
||||
PlanarConnections: 0,
|
||||
Publishing: 0,
|
||||
Forestry: 0,
|
||||
Finance: 0,
|
||||
Gadgetry: 0,
|
||||
Fishing: 0,
|
||||
Construction: 0,
|
||||
Propaganda: 0,
|
||||
Bureaucracy: 0,
|
||||
Militarism: 0,
|
||||
Welfare: 0,
|
||||
Logistics: 0,
|
||||
DragonSubs: 0,
|
||||
ResearchSubs: 0,
|
||||
Education: 0,
|
||||
Healthcare: 0,
|
||||
ForeignRelExpense: 0,
|
||||
ParksExpense: 0,
|
||||
Faith: 0,
|
||||
FoodSupply: 20,
|
||||
ForeignRelations: 0,
|
||||
HiddenRelPenalty: 0,
|
||||
Rebellion: 0,
|
||||
Madness: 0,
|
||||
Cruelty: 0,
|
||||
Greed: 0,
|
||||
Gullibility: 0,
|
||||
Authoritarianism: 0,
|
||||
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
|
||||
}
|
||||
|
71
koboldsim/util.go
Normal file
71
koboldsim/util.go
Normal file
@ -0,0 +1,71 @@
|
||||
package koboldsim
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"golang.org/x/exp/constraints"
|
||||
)
|
||||
|
||||
// Generic helper functions not directly attached to Card Sim Engine mechanics.
|
||||
|
||||
// Mean calculates the mathematical mean of its arguments (the sum, divided
|
||||
// by the number of elements). If it is called with no arguments, it returns
|
||||
// NaN ("not a number"). If there are contradictory infinities among the
|
||||
// arguments, it also returns NaN. Overflowing or underflowing can create an
|
||||
// infinity.
|
||||
func Mean(vals ...float64) float64 {
|
||||
if len(vals) == 0 {
|
||||
return math.NaN()
|
||||
}
|
||||
|
||||
total := 0.0
|
||||
for _, x := range vals {
|
||||
total += x
|
||||
}
|
||||
return total / float64(len(vals))
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
return c
|
||||
}
|
||||
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