Compare commits

..

No commits in common. "main" and "0.1.0" have entirely different histories.
main ... 0.1.0

17 changed files with 147 additions and 867 deletions

2
.gitignore vendored
View File

@ -21,5 +21,3 @@
# Go workspace file # Go workspace file
go.work go.work
# Visual studio Code exclusions
.vscode/settings.json

View File

@ -2,8 +2,6 @@
Basic engine for NationStates-like "make decisions on issues" simulation games. Very incomplete. Basic engine for NationStates-like "make decisions on issues" simulation games. Very incomplete.
[![Go Reference](https://pkg.go.dev/badge/git.chromaticdragon.app/kistaro/CardSimEngine.svg)](https://pkg.go.dev/git.chromaticdragon.app/kistaro/CardSimEngine)
## General turn model ## General turn model
1. Player has a hand of cards. Each card has one or more actions available. 1. Player has a hand of cards. Each card has one or more actions available.
@ -42,7 +40,7 @@ A bucket of game state.
### Stat ### Stat
An arbitrary variable (or function, if it's calculated) tagged with some stuff to make it easier to display to the player. Stats can be extracted automatically (see "Stat Extraction" below). An arbitrary variable (or function, if it's calculated) tagged with some stuff to make it easier to display to the player.
### StatsCollection ### StatsCollection
@ -57,26 +55,3 @@ There are some special errors that Rules can use to "communicate with" the rule
### Messages ### Messages
For now, strings but inconvenient. Intended to provide forwards compatibility when we eventually include some way to format text, where all the stuff written for "it's just a string" would break if not for having this extra type in the way to wrap it where we can stay compatible with "it's just a string" mode. For now, strings but inconvenient. Intended to provide forwards compatibility when we eventually include some way to format text, where all the stuff written for "it's just a string" would break if not for having this extra type in the way to wrap it where we can stay compatible with "it's just a string" mode.
## Stat Extraction
The function `ExtractStats` creates a stats list automatically by searching through a struct's fields and methods. The following things are recognized as stats:
* any method with a name like `StatFoo` or `HiddenStatFoo` (the latter are marked as invisible stats, which show up only in debug mode with the implementation in BasicStatsPane)
* any exported field with a type that is already a `Stat`; the `Stored[T]` and `Hidden[T]` generic types are containers for this
* any exported field tagged with `cardsim:"stat"`
* or `cardsim:"hidden"` for hidden stats. `"hiddenstat"` also works.
* you can use `"round2"` to round to two decimal places -- you can use any integer here, not just 2. works with both `float` types.
* `"hiddenround3"` (or any other number) creates a hidden rounded stat.
* To change the display name of a stat, use a separate tag phrase in addition to the stat tag, `cardsim_name:"name"`.
* For example: `cardsim:"stat" cardsim_name:"Stat Display Name"` creates a visible stat that shows up as "Stat Display Name".
* `cardsim:"hiddenround1" cardsim_name:"Hidden Rounded Stat"` creates an invisible stat that shows up, rounded to one decimal place, as "Hidden Rounded Stat".
Stat extraction can implement most or all of your type's `Stats` method for you:
```
func (e *ExampleType) Stats() []cardsim.Stat {
return cardsim.ExtractStats(e)
}
```
ExtractStats puts methods first (lexicographically), then fields (in the order they appear). You can use `cardsim.SortStats` to instead put visible stats before hidden stats, alphabetized (case-insensitive).

View File

@ -101,63 +101,6 @@ func (b *BasicCard[C]) Drawn(_ *Player[C]) bool {
return true return true
} }
// RefundAction returns a func that can be used as an AfterOption, which returns
// the player's action point.
func RefundAction[C StatsCollection]() func(c Card[C], p *Player[C], option CardOption[C]) error {
return func(c Card[C], p *Player[C], option CardOption[C]) error {
p.ActionsRemaining++
return nil
}
}
// A PanelCard is a Card that takes its title and text from an InfoPanel,
// while options, urgency, and the post-option callback are specified
// (like a BasicCard). It never does anything in particular when drawn.
//
// Omitting all options yields an inactionable card, which can be displayed
// but not played. This can be useful for adding an info panel as a debug action.
type PanelCard[C StatsCollection] struct {
Panel InfoPanel[C]
IsUrgent bool
CardOptions []CardOption[C]
// AfterOption is given the card itself as its first argument.
AfterOption func(c Card[C], p *Player[C], option CardOption[C]) error
}
// Title implements Card.
func (c *PanelCard[C]) Title(p *Player[C]) Message {
return c.Panel.Title(p)
}
// Urgent implements Card.
func (c *PanelCard[C]) Urgent(_ *Player[C]) bool {
return c.IsUrgent
}
// EventText implements Card.
func (c *PanelCard[C]) EventText(p *Player[C]) (Message, error) {
msgs, err := c.Panel.Info(p)
return MultiMessage(msgs), err
}
// Options implements Card.
func (c *PanelCard[C]) Options(_ *Player[C]) ([]CardOption[C], error) {
return c.CardOptions, nil
}
// Then implements Card.
func (c *PanelCard[C]) Then(p *Player[C], option CardOption[C]) error {
if c.AfterOption == nil {
return nil
}
return c.AfterOption(c, p, option)
}
// Drawn implements Card.
func (c *PanelCard[C]) Drawn(_ *Player[C]) bool {
return true
}
// A BasicOption is a CardOption with fixed text, effects, and output. // A BasicOption is a CardOption with fixed text, effects, and output.
// It's always enabled. // It's always enabled.
type BasicOption[C StatsCollection] struct { type BasicOption[C StatsCollection] struct {
@ -208,17 +151,3 @@ func (o *optionFunc[C]) Enact(p *Player[C]) (Message, error) {
func (o *optionFunc[C]) Enabled(p *Player[C]) bool { func (o *optionFunc[C]) Enabled(p *Player[C]) bool {
return true return true
} }
// OnlyDiscardFree returns a []CardOption[C] providing a single option, which
// returns the action point. It does not shuffle the card back into the deck
// or draw a replacement (consider the AfterFunc for that if needed). This
// can be used for cards that are displayable but not actionable, but show up
// as cards rather than permanent or debug actions for some reason.
func OnlyDiscardFree[C StatsCollection](msg Message) []CardOption[C] {
return []CardOption[C]{
OptionFunc(msg, func(p *Player[C]) (Message, error) {
p.ActionsRemaining++
return MsgStr("Okay."), nil
}),
}
}

View File

@ -1,103 +0,0 @@
package cardsim
// Named debug verbosity levels. Using the raw constants is fine too. This
// is roughly consistent with "standard" meanings for these debug levels.
const (
HideWarnings = -1
NotDebugging = 0
DebugWarning = 1
DebugInfo = 2
DebugDetail = 3
DebugFine = 4
DebugSuperfine = 5
)
// ActionCounterDebugCard constructs a BasicCard intended for use only as a
// Debug Action that tinkers with the player's action counter.
func ActionCounterDebugCard[C StatsCollection]() Card[C] {
return &BasicCard[C]{
CardTitle: MsgStr("Adjust Action Counter"),
CardText: MsgStr("Change the number of actions you have available this turn."),
CardOptions: []CardOption[C]{
&BasicOption[C]{
Text: MsgStr("Get an extra action."),
Effect: func(p *Player[C]) error {
p.ActionsRemaining += 2 // counteract the one this costs
return nil
},
Output: MsgStr("Gotten."),
},
&BasicOption[C]{
Text: MsgStr("Waste an action."),
Effect: func(p *Player[C]) error {
return nil
},
Output: MsgStr("Wasted."),
},
&BasicOption[C]{
Text: MsgStr("Get a thousand actions."),
Effect: func(p *Player[C]) error {
p.ActionsRemaining = 1000
return nil
},
Output: MsgStr("ActionsRemaining set to 1000."),
},
&BasicOption[C]{
Text: MsgStr("Go to exactly 1 action remaining."),
Effect: func(p *Player[C]) error {
p.ActionsRemaining = 1
return nil
},
Output: MsgStr("ActionsRemaining set to 1."),
},
&BasicOption[C]{
Text: MsgStr("End the turn. (Set actions to 0.)"),
Effect: func(p *Player[C]) error {
p.ActionsRemaining = 0
return nil
},
Output: MsgStr("ActionsRemaining zeroed out."),
},
},
}
}
// DebugModeCard constructs a BasicCard to change the player's debug level.
// It is intended for use only as a Debug Action.
func DebugModeCard[C StatsCollection]() Card[C] {
return &BasicCard[C]{
CardTitle: MsgStr("Change Debug Level"),
CardText: MsgStr("Adjust verbosity of output, or exit debug mode entirely (not recommended)."),
CardOptions: []CardOption[C]{
debugLevelOption[C]{1, "Enable debug mode. Show warnings."},
debugLevelOption[C]{2, "Enable debug mode. Show info messages."},
debugLevelOption[C]{3, "Enable debug mode. Show detailed messages."},
debugLevelOption[C]{4, "Enable debug mode. Show individual details of operations."},
debugLevelOption[C]{5, "Enable debug mode. Show every event in excruciating detail."},
debugLevelOption[C]{0, "NOT RECOMMENDED. Disable debugging (show warnings). IT CAN'T BE TURNED BACK ON."},
debugLevelOption[C]{-1, "NOT RECOMMENDED. Disable debugging (hide warnings). IT CAN'T BE TURNED BACK ON."},
},
AfterOption: RefundAction[C](),
}
}
type debugLevelOption[C StatsCollection] struct {
level int
description string
}
// OptionText implements CardOption[C].
func (d debugLevelOption[C]) OptionText(*Player[C]) (Message, error) {
return Msgf("Set debug level %d: %s", d.level, d.description), nil
}
// Enact implements CardOption[C].
func (d debugLevelOption[C]) Enact(p *Player[C]) (Message, error) {
p.DebugLevel = d.level
return Msgf("Debug level is now %d.", d.level), nil
}
// Enabled implements CardOption[C].
func (d debugLevelOption[C]) Enabled(p *Player[C]) bool {
return true
}

View File

@ -279,145 +279,3 @@ func (d *Deck[C]) Strip(shouldRemove func(idx int, c Card[C]) bool) int {
d.cards = Strip(d.cards, shouldRemove) d.cards = Strip(d.cards, shouldRemove)
return origLen - d.Len() return origLen - d.Len()
} }
// DeckDebugger is a Card[C], intended for use only as a debug action, that
// lists the top 10 cards of the deck (without checking if they are drawable)
// and allows various sorts of deck manipulation for free. It can't be drawn.
type DeckDebugger[C StatsCollection] struct{}
// Title implements Card[C].
func (DeckDebugger[C]) Title(p *Player[C]) Message {
return MsgStr("Debug Mode Deck Controls")
}
// Urgent implements Card[C] as used in permanent actions. It's always valid
// to use the deck debugger. Debug actions do not check urgency flags, but this
// marks itself as urgent-compatible just in case.
func (DeckDebugger[C]) Urgent(p *Player[C]) bool {
return true
}
// Drawn implements Card[C]. It can't be drawn.
func (DeckDebugger[C]) Drawn(p *Player[C]) bool {
return false
}
// EventText implements Card[C]. It lists the top ten cards of the deck and
// a few deck-related and hand-related stats.
func (DeckDebugger[C]) EventText(p *Player[C]) (Message, error) {
var msgs []Message
msgs = append(msgs, Msgf("The Deck contains %d cards.", p.Deck.Len()))
if p.Deck.Len() > 0 {
portion := p.Deck.cards
msgs = append(msgs, nil)
topness := "All"
if p.Deck.Len() > 10 {
portion = p.Deck.cards[:10]
topness = "Top 10"
}
msgs = append(msgs, Msgf("%s cards in the Deck:", topness))
for i, c := range portion {
urgency := " "
if c.Urgent(p) {
urgency = "!"
}
msgs = append(msgs, Msgf(" %s %2d) %v", urgency, i+1, c.Title(p)))
}
}
msgs = append(msgs, nil)
msgs = append(msgs, Msgf("At the start of each turn, the Player draws to %d cards. The player has %d cards in hand.", p.HandLimit, len(p.Hand)))
return MultiMessage(msgs), nil
}
// Options implements Card[C]. It offers many possible actions.
func (DeckDebugger[C]) Options(p *Player[C]) ([]CardOption[C], error) {
ret := []CardOption[C]{
&BasicOption[C]{
Text: MsgStr("Draw a card."),
Effect: func(p *Player[C]) error {
return p.Draw()
},
Output: MsgStr("Done."),
},
&BasicOption[C]{
Text: MsgStr("Shuffle the deck."),
Effect: func(p *Player[C]) error {
return p.Deck.Shuffle()
},
Output: MsgStr("Done."),
},
&BasicOption[C]{
Text: MsgStr("Shuffle top half."),
Effect: func(p *Player[C]) error {
return p.Deck.ShuffleTop(0.5)
},
Output: MsgStr("Done."),
},
&BasicOption[C]{
Text: MsgStr("Shuffle bottom half."),
Effect: func(p *Player[C]) error {
return p.Deck.ShuffleBottom(0.5)
},
Output: MsgStr("Done."),
},
}
for _, n := range []int{1, 3, 5, 10} {
if p.Deck.Len() <= n {
break
}
// We don't want the functions we're creating to all share the same "n"
// field -- we want to create distinct functions that move distinct
// numbers of cards. For more information on what's going on here, see
// https://eli.thegreenplace.net/2019/go-internals-capturing-loop-variables-in-closures/
//
// For the curious, the Go developers don't like the gotcha that
// this "shadow variable with itself" workaround patches over either.
// Here's the guy who implemented apologizing for it and discussing
// changing it: https://github.com/golang/go/discussions/56010
//
// "Loop variables being per-loop instead of per-iteration is the only
// design decision I know of in Go that makes programs incorrect more
// often than it makes them correct." -- Russ Cox (rsc)
n := n
invN := p.Deck.Len() - n
ret = append(ret,
&BasicOption[C]{
Text: Msgf("Move the top %d card(s) to the bottom of the deck, in order.", n),
Effect: func(p *Player[C]) error {
p.Deck.cards = append(p.Deck.cards, p.Deck.cards[:n]...)
p.Deck.cards = p.Deck.cards[n:]
return nil
},
Output: MsgStr("Done."),
},
&BasicOption[C]{
Text: Msgf("Move the bottom %d card(s) to the top of the deck, in order.", n),
Effect: func(p *Player[C]) error {
p.Deck.cards = append(p.Deck.cards, p.Deck.cards[:invN]...)
p.Deck.cards = p.Deck.cards[invN:]
return nil
},
Output: MsgStr("Done."),
},
)
if n > 1 {
ret = append(ret, &BasicOption[C]{
Text: Msgf("Shuffle the top %d card(s) of the deck.", n),
Effect: func(p *Player[C]) error {
return p.Deck.ShufflePart(0, n)
},
Output: MsgStr("Done."),
})
}
}
return ret, nil
}
// Then implements Card[C]. It refunds the action point.
func (DeckDebugger[C]) Then(p *Player[C], o CardOption[C]) error {
p.ActionsRemaining++
return nil
}

View File

@ -12,12 +12,6 @@ type Message interface {
fmt.Stringer fmt.Stringer
} }
// Titled desccribes any type that returns a Message as a title, given a Player
// (which it may ignore).
type Titled[C StatsCollection] interface {
Title(*Player[C]) Message
}
type stringMessage string type stringMessage string
func (s stringMessage) String() string { func (s stringMessage) String() string {
@ -35,19 +29,6 @@ func Msgf(f string, args ...any) Message {
return stringMessage(fmt.Sprintf(f, args...)) return stringMessage(fmt.Sprintf(f, args...))
} }
// ErrorMessage returns a Message representing an Error.
// This is preferred over Msgf for errors, since future versions of the library
// may perform special message formatting for errors.
func ErrorMessage(e error) Message {
if e == nil {
return nil
}
if IsSeriousError(e) {
return MultiMessage{MsgStr("SERIOUS ERROR:"), Msgf("%v", e)}
}
return MultiMessage{MsgStr("Warning:"), Msgf("%v", e)}
}
// A SpecialMessage is a specific, uniquely identifiable message. // A SpecialMessage is a specific, uniquely identifiable message.
type SpecialMessage struct { type SpecialMessage struct {
msg Message msg Message

View File

@ -8,14 +8,13 @@ import (
) )
var ( var (
ErrUncooperativeCards = errors.New("a milion cards refused to join the hand")
ErrInvalidCard = errors.New("invalid card specified") ErrInvalidCard = errors.New("invalid card specified")
ErrInvalidChoice = errors.New("invalid choice specified") ErrInvalidChoice = errors.New("invalid choice specified")
ErrNotUrgent = errors.New("action not urgent when urgent card is available") ErrNotUrgent = errors.New("action not urgent when urgent card is available")
ErrNoActions = errors.New("no actions remaining") ErrNoActions = errors.New("no actions remaining")
ErrNotDebugging = errors.New("this is a debug-only feature and you're not in debug mode")
WarningStalemate = &Warning{errors.New("no actions can be taken")} WarningStalemate = errors.New("no actions can be taken")
WarningUncoperativeCards = &Warning{errors.New("a milion cards refused to join the hand")}
) )
// Player stores all gameplay state for one player at a specific point in time. // Player stores all gameplay state for one player at a specific point in time.
@ -105,10 +104,6 @@ type Player[C StatsCollection] struct {
// card is in the hand. // card is in the hand.
PermanentActions []Card[C] PermanentActions []Card[C]
// DebugActions are PermanentActions only available when the player is in
// debug mode. InitPlayer adds some standard debugging actions by default.
DebugActions []Card[C]
// InfoPanels lists informational views available to the player. The Prompt // InfoPanels lists informational views available to the player. The Prompt
// is the InfoPanel shown before the main action menu. // is the InfoPanel shown before the main action menu.
InfoPanels []InfoPanel[C] InfoPanels []InfoPanel[C]
@ -175,12 +170,6 @@ func InitPlayer[C StatsCollection](stats C) *Player[C] {
HandLimit: 1, HandLimit: 1,
ActionsPerTurn: 1, ActionsPerTurn: 1,
Rules: NewRuleCollection[C](), Rules: NewRuleCollection[C](),
DebugActions: []Card[C]{
&DeckDebugger[C]{},
&PanelCard[C]{Panel: RuleDumper[C]{}},
ActionCounterDebugCard[C](),
DebugModeCard[C](),
},
} }
} }
@ -272,9 +261,9 @@ func (p *Player[C]) StartNextTurn() error {
} }
// Draw draws a card into the hand, informing the card that it has been drawn. // Draw draws a card into the hand, informing the card that it has been drawn.
// If more than a million cards refuse to enter the hand, this gives up and // If more than a million cards refuse to enter the hand, this crashes with
// returns WarningUncooperativeCards. If the deck does not have enough cards, // ErrUncooperativeCards. If the deck does not have enough cards, this
// this returns WarningTooFewCards. // returns WarningTooFewCards.
func (p *Player[C]) Draw() error { func (p *Player[C]) Draw() error {
for attempts := 0; attempts < 1000000; attempts++ { for attempts := 0; attempts < 1000000; attempts++ {
if p.Deck.Len() == 0 { if p.Deck.Len() == 0 {
@ -286,13 +275,13 @@ func (p *Player[C]) Draw() error {
return nil return nil
} }
} }
return WarningUncoperativeCards return ErrUncooperativeCards
} }
// FillHand draws up to the hand limit, informing cards that they have been // FillHand draws up to the hand limit, informing cards that they have been
// drawn. If more than a million cards refuse to enter the hand, this gives up // drawn. If more than a million cards refuse to enter the hand, this crashes
// and returns WarningUncooperativeCards. If the deck does not have enough // with ErrUncooperativeCards. If the deck does not have enough cards, this
// cards, this returns WarningTooFewCards. // returns WarningTooFewCards.
func (p *Player[C]) FillHand() error { func (p *Player[C]) FillHand() error {
var lastErr error var lastErr error
for p.Deck.Len() > 0 && len(p.Hand) < p.HandLimit { for p.Deck.Len() > 0 && len(p.Hand) < p.HandLimit {
@ -319,38 +308,6 @@ func (p *Player[C]) HasUrgentCards() bool {
return false return false
} }
// EnactableType is an enumeration representing a category of enactable thing.
// Debug actions, permanent actions, and cards behave equivalently in many ways,
// so EnactableType allows parts of the program to work with any of these and
// represent which one they apply to.
type EnactableType int
const (
// InvalidEnactable is an uninitialized EnactableType value with no meaning.
// Using it is generally an error. If you initialize EnactableType fields
// with this value when your program has not yet calculated what type of
// enactable will be used, CardSimEngine will be able to detect bugs where
// such a calcualation, inadvertently, does not come to any conclusion.
// Unlike NothingEnactable, there are no circumstances where this has a
// specific valid meaning.
InvalidEnactable = EnactableType(iota)
// NothingEnactable specifically represents not enacting anything. In some
// contexts, it's an error to use it; in others, it is a sentinel value
// for "do not enact anything". Unlike InvalidEnactable, this has a specific
// valid meaning, it's just that the meaning is specifically "nothing".
NothingEnactable
// CardEnactable refers to a card in the hand.
CardEnactable
// PermanentActionEnactable refers to an item in the permanent actions list.
PermanentActionEnactable
// DebugActionEnactable refers to an item in the debug actions list.
DebugActionEnactable
)
// EnactCardUnchecked executes a card choice, removes it from the hand, and // EnactCardUnchecked executes a card choice, removes it from the hand, and
// decrements the ActionsRemaining. It does not check for conflicting Urgent // decrements the ActionsRemaining. It does not check for conflicting Urgent
// cards or already being out of actions. If no such card or card choice // cards or already being out of actions. If no such card or card choice
@ -386,14 +343,17 @@ func (p *Player[C]) EnactCardUnchecked(cardIdx, choiceIdx int) (Message, error)
ret, err := options[choiceIdx].Enact(p) ret, err := options[choiceIdx].Enact(p)
errs.Add(err) errs.Add(err)
if IsSeriousError(err) {
p.State = GameCrashed
return ret, errs.Emit()
}
err = card.Then(p, options[choiceIdx]) err = card.Then(p, options[choiceIdx])
errs.Add(err) errs.Add(err)
err = errs.Emit()
if IsSeriousError(err) { if IsSeriousError(err) {
p.State = GameCrashed p.State = GameCrashed
} }
return ret, err return ret, errs.Emit()
} }
// EnactCard executes a card choice, removes it from the hand, and decrements // EnactCard executes a card choice, removes it from the hand, and decrements
@ -421,31 +381,10 @@ func (p *Player[C]) EnactCard(cardIdx, choiceIdx int) (Message, error) {
// result of enacting the permanent action. If enacting the card causes a // result of enacting the permanent action. If enacting the card causes a
// serious error, the State becomes GameCrashed. // serious error, the State becomes GameCrashed.
func (p *Player[C]) EnactPermanentActionUnchecked(actionIdx, choiceIdx int) (Message, error) { func (p *Player[C]) EnactPermanentActionUnchecked(actionIdx, choiceIdx int) (Message, error) {
return p.enactActionUnchecked(p.PermanentActions, actionIdx, choiceIdx) if actionIdx < 0 || actionIdx >= len(p.PermanentActions) {
return nil, fmt.Errorf("%w: no action #%d when %d permanent actions exist", ErrInvalidCard, actionIdx, len(p.PermanentActions))
} }
card := p.PermanentActions[actionIdx]
// EnactDebugActionUnchecked executes a debug action and decrements the
// ActionsRemaining, even though most debug actions will want to refund that
// action point. (Consistency with other actions is important.) It does not
// check for Urgent cards or for already being out of actions. If no such action
// or card option exists, or the option is not enabled, this returns nil and
// ErrInvalidCard or ErrInvalidChoice without changing anything. If the player
// is not in debug mode (DebugLevel >= 1), this returns ErrNotDebugging.
// Otherwise, this returns the result of enacting the debug action. If enacting
// the action causes a serious error, the State becomes GameCrashed.
func (p *Player[C]) EnactDebugActionUnchecked(actionIdx, choiceIdx int) (Message, error) {
if p.DebugLevel < 1 {
return nil, ErrNotDebugging
}
return p.enactActionUnchecked(p.DebugActions, actionIdx, choiceIdx)
}
// enactActionUnchecked implements EnactPermanentActionUnchecked and EnactDebugActionUnchecked.
func (p *Player[C]) enactActionUnchecked(actionSource []Card[C], actionIdx, choiceIdx int) (Message, error) {
if actionIdx < 0 || actionIdx >= len(actionSource) {
return nil, fmt.Errorf("%w: no action #%d when %d actions exist", ErrInvalidCard, actionIdx, len(actionSource))
}
card := actionSource[actionIdx]
var errs ErrorCollector var errs ErrorCollector
options, err := card.Options(p) options, err := card.Options(p)
if IsSeriousError(err) { if IsSeriousError(err) {
@ -454,12 +393,12 @@ func (p *Player[C]) enactActionUnchecked(actionSource []Card[C], actionIdx, choi
} }
errs.Add(err) errs.Add(err)
if choiceIdx < 0 || choiceIdx > len(options) { if choiceIdx < 0 || choiceIdx > len(options) {
errs.Add(fmt.Errorf("%w: no option #%d on action #%d with %d options", ErrInvalidChoice, choiceIdx, actionIdx, len(options))) errs.Add(fmt.Errorf("%w: no option #%d on permanent action #%d with %d options", ErrInvalidChoice, choiceIdx, actionIdx, len(options)))
return nil, errs.Emit() return nil, errs.Emit()
} }
chosen := options[choiceIdx] chosen := options[choiceIdx]
if !chosen.Enabled(p) { if !chosen.Enabled(p) {
errs.Add(fmt.Errorf("%w: option #%d on action #%d is not enabled", ErrInvalidChoice, choiceIdx, actionIdx)) errs.Add(fmt.Errorf("%w: option #%d on permanent action #%d is not enabled", ErrInvalidChoice, choiceIdx, actionIdx))
return nil, errs.Emit() return nil, errs.Emit()
} }
@ -467,13 +406,17 @@ func (p *Player[C]) enactActionUnchecked(actionSource []Card[C], actionIdx, choi
ret, err := chosen.Enact(p) ret, err := chosen.Enact(p)
errs.Add(err) errs.Add(err)
if IsSeriousError(err) {
p.State = GameCrashed
return ret, errs.Emit()
}
err = card.Then(p, chosen) err = card.Then(p, chosen)
errs.Add(err) errs.Add(err)
retErr := errs.Emit() if IsSeriousError(err) {
if IsSeriousError(retErr) {
p.State = GameCrashed p.State = GameCrashed
} }
return ret, retErr return ret, errs.Emit()
} }
// EnactPermanentAction executes a permanently-available card and decrements // EnactPermanentAction executes a permanently-available card and decrements
@ -505,11 +448,15 @@ func (p *Player[C]) ReportError(e error) {
if e == nil || p.DebugLevel < -1 { if e == nil || p.DebugLevel < -1 {
return return
} }
minLvl := NotDebugging if p.DebugLevel < 0 && !IsSeriousError(e) {
if IsSeriousError(e) { return
minLvl = HideWarnings
} }
p.Debug(minLvl, ErrorMessage(e)) p.ChapterBreak()
severity := "[Warning]"
if IsSeriousError(e) {
severity = "[ERROR]"
}
p.TemporaryMessages = append(p.TemporaryMessages, Msgf("%s: %v", severity, e))
} }
// CanAct returns whether the player has actions theoretically available. // CanAct returns whether the player has actions theoretically available.
@ -523,7 +470,6 @@ func (p *Player[C]) Debug(minLevel int, msg Message) {
if p.DebugLevel < minLevel || msg == nil { if p.DebugLevel < minLevel || msg == nil {
return return
} }
p.ChapterBreak()
p.TemporaryMessages = append(p.TemporaryMessages, msg) p.TemporaryMessages = append(p.TemporaryMessages, msg)
} }

View File

@ -4,8 +4,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"sort" "sort"
"github.com/kr/pretty"
) )
// A Rule implements an operation run on every game turn. // A Rule implements an operation run on every game turn.
@ -404,14 +402,3 @@ func (r *RuleCollection[C]) applyDelayedUpdates() {
r.RemoveID(id) r.RemoveID(id)
} }
} }
// RuleDumper is an InfoPanel[C] that dumps all rules in P.
type RuleDumper[C StatsCollection] struct{}
func (RuleDumper[C]) Title(p *Player[C]) Message {
return MsgStr("Rule Dumper")
}
func (RuleDumper[C]) Info(p *Player[C]) ([]Message, error) {
return []Message{Msgf("%# v", pretty.Formatter(p.Rules))}, nil
}

View File

@ -145,21 +145,3 @@ func Strip[T any](slice []T, removeWhen func(idx int, t T) bool) []T {
} }
return slice[:to] return slice[:to]
} }
// EnsureCapacity checks if `cap(slice)` is at least req. If so, it returns
// slice unchanged. Otherwise, it copies `slice` to a new slice that is at least
// capacity `req` (but may be larger) and returns the copy.
//
// It is reasonably efficient to use EnsureCapacity consecutively without
// regard for the final overall capacity that a specific slice will need to be.
func EnsureCapacity[T any](slice []T, req int) []T {
if cap(slice) >= req {
return slice
}
if req < 2*cap(slice) {
req = 2 * cap(slice)
}
ret := make([]T, len(slice), req)
copy(ret, slice)
return ret
}

View File

@ -4,11 +4,7 @@ import (
"fmt" "fmt"
"reflect" "reflect"
"sort" "sort"
"strconv"
"strings" "strings"
"unicode"
"golang.org/x/exp/constraints"
) )
// A StatsCollection contains stats. // A StatsCollection contains stats.
@ -118,30 +114,9 @@ func (s statFunc[T]) Visible() bool {
return s.visible return s.visible
} }
// ExtractStats pulls all exported stats out of a struct. It puts methods before // ExtractStats pulls all exported Stat fields (not functions) out of a struct.
// fields. If the calculated name of a method conflicts with the calculated // If x cannot be resolved to a struct, it panics. It unwraps interfaces and
// name of a stat from a field, the method wins. // follows pointers to try to find a struct.
//
// A field is a stat if it is of some Stat type or is tagged with `cardsim:"stat"`,
// `cardsim:"hidden"` (invisible stat), `cardsim:"round2"` (or any integer, 2 is
// just an example), or `cardsim:"hiddenround3"`. `hiddenstat`, `statround`, and
// `hiddenstatround` are also accepted, but other orders of these directives
// are not. A "round" stat must be a float type and it will be rounded to
// this number of decimal places.
//
// A method is a Stat if it takes 0 arguments, returns exactly 1 value, and
// starts with Stat or HiddenStat.
//
// The name of these inferred stats is calculated by breaking the name into
// separate words before each capital letter, unless there are consecutive
// capital letters, which it interprets as an initialism (followed by the
// start of another word, if it's not at the end). To insert a space between
// consecutive capital letters, insert an underscore (`_`). This name inference
// trims "Stat" and "HiddenStat" off the front of method names.
//
// To override the name extracted from a field name, add `cardsim_name:"name"`
// to the tag, where the name part is the name you want to use. It will not be
// formatted further - use normal spaces, capitalization, etc.
func ExtractStats(x any) []Stat { func ExtractStats(x any) []Stat {
v := reflect.ValueOf(x) v := reflect.ValueOf(x)
for k := v.Kind(); k == reflect.Pointer || k == reflect.Interface; k = v.Kind() { for k := v.Kind(); k == reflect.Pointer || k == reflect.Interface; k = v.Kind() {
@ -150,125 +125,19 @@ func ExtractStats(x any) []Stat {
if v.Kind() != reflect.Struct { if v.Kind() != reflect.Struct {
panic(fmt.Errorf("%T is not a struct", x)) panic(fmt.Errorf("%T is not a struct", x))
} }
typ := v.Type()
var ret []Stat var ret []Stat
lim := v.NumField()
known := make(map[string]bool)
for _, vv := range []reflect.Value{v, v.Addr()} {
xt := vv.Type()
lim := xt.NumMethod()
for i := 0; i < lim; i++ { for i := 0; i < lim; i++ {
m := xt.Method(i) f := v.Field(i)
if !m.IsExported() {
continue
}
tm := m.Type
if tm.NumIn() != 1 {
// 1 arg -- receiver
continue
}
if tm.NumOut() != 1 {
continue
}
nameParts := explode(m.Name)
if len(nameParts) < 2 {
continue
}
isHidden := false
if nameParts[0] == "Hidden" {
isHidden = true
nameParts = nameParts[1:]
}
if nameParts[0] != "Stat" {
continue
}
n := strings.Join(nameParts[1:], " ")
if n == "" {
continue
}
if known[n] {
continue
}
known[n] = true
val := vv.Method(i).Call([]reflect.Value{})
if len(val) != 1 {
// This shouldn't happen - we already checked Out. Weird.
continue
}
if !val[0].CanInterface() {
continue
}
ret = append(ret, &StatLiteral{
Name: n,
Value: fmt.Sprint(val[0].Interface()),
IsVisible: !isHidden,
})
}
}
fields := reflect.VisibleFields(typ)
for _, sf := range fields {
if !sf.IsExported() {
continue
}
f := v.FieldByIndex(sf.Index)
if !f.CanInterface() { if !f.CanInterface() {
continue continue
} }
iface := f.Interface() x := f.Interface()
if s, ok := iface.(Stat); ok { if s, ok := x.(Stat); ok {
if known[s.StatName()] {
continue
}
known[s.StatName()] = true
ret = append(ret, s) ret = append(ret, s)
continue
} }
if t := sf.Tag.Get("cardsim"); t != "" {
isStat := false
isHidden := false
t = strings.ToLower(t)
t = strings.TrimSpace(t)
if strings.HasPrefix(t, "hidden") {
isStat = true
isHidden = true
t = t[6:]
} }
if strings.HasPrefix(t, "stat") {
isStat = true
t = t[4:]
}
var val string
if strings.HasPrefix(t, "round") {
isStat = true
t = t[5:]
n, _ := strconv.Atoi(t)
fs := fmt.Sprintf("%%.%df", n)
val = fmt.Sprintf(fs, iface)
} else if isStat {
val = fmt.Sprint(iface)
} else {
continue // not identifiably a stat
}
nm := strings.Join(explode(sf.Name), " ")
if t := sf.Tag.Get("cardsim_name"); t != "" {
nm = t
}
if known[nm] {
continue
}
known[nm] = true
ret = append(ret, &StatLiteral{
Name: nm,
Value: val,
IsVisible: !isHidden,
})
continue
}
// Else, not a stat.
}
return ret return ret
} }
@ -304,105 +173,3 @@ func (s statSorter) Less(i, j int) bool {
// Names differ only by capitalization, if that. // Names differ only by capitalization, if that.
return lhs.StatName() < rhs.StatName() return lhs.StatName() < rhs.StatName()
} }
// StatLiteral stores a ready-to-emit stat value.
type StatLiteral struct {
Name string
Value string
IsVisible bool
}
func (s *StatLiteral) StatName() string {
return s.Name
}
func (s *StatLiteral) String() string {
return s.Value
}
func (s *StatLiteral) Visible() bool {
return s.IsVisible
}
func EmitStat(name string, v any) *StatLiteral {
return &StatLiteral{
Name: name,
Value: fmt.Sprint(v),
IsVisible: true,
}
}
func EmitHiddenStat(name string, v any) *StatLiteral {
return &StatLiteral{
Name: name,
Value: fmt.Sprint(v),
IsVisible: false,
}
}
func Statf(name string, f string, args ...any) *StatLiteral {
return &StatLiteral{
Name: name,
Value: fmt.Sprintf(f, args...),
IsVisible: true,
}
}
func HiddentStatf(name string, f string, args ...any) *StatLiteral {
return &StatLiteral{
Name: name,
Value: fmt.Sprintf(f, args...),
IsVisible: false,
}
}
func RoundStat[N constraints.Float](name string, val N, decimals int) *StatLiteral {
f := fmt.Sprintf("%%.%df", decimals)
return &StatLiteral{
Name: name,
Value: fmt.Sprintf(f, val),
IsVisible: true,
}
}
func RoundHiddenStat[N constraints.Float](name string, val N, decimals int) *StatLiteral {
r := RoundStat(name, val, decimals)
r.IsVisible = false
return r
}
// explode turns CamelCase into multiple strings. It recognizes initialisms. To
// split consecutive capital letters into separate words instead of recognizing
// them as an initialism, insert underscores.
func explode(s string) []string {
var parts []string
started := 0
initialism := false
for i, r := range s {
if unicode.IsUpper(r) {
if initialism || (started == i) {
continue
}
if started == i-1 {
initialism = true
continue
}
parts = append(parts, s[started:i])
started = i
continue
}
if r == '_' {
parts = append(parts, s[started:i])
initialism = false
started = i + 1
continue
}
if initialism {
parts = append(parts, s[started:i-1])
initialism = false
started = i - 1
}
}
parts = append(parts, s[started:])
return parts
}

View File

@ -17,7 +17,7 @@ func RunSimpleTerminalUI[C StatsCollection](p *Player[C]) error {
for { for {
for p.CanAct() { for p.CanAct() {
actionType, cardIdx, choiceIdx, err := pickNextAction(p) isCard, cardIdx, choiceIdx, err := pickNextAction(p)
p.ReportError(err) p.ReportError(err)
if IsSeriousError(err) { if IsSeriousError(err) {
if p.DebugLevel < 1 { if p.DebugLevel < 1 {
@ -26,18 +26,10 @@ func RunSimpleTerminalUI[C StatsCollection](p *Player[C]) error {
continue continue
} }
var msg Message var msg Message
switch actionType { if isCard {
case CardEnactable:
msg, err = p.EnactCard(cardIdx, choiceIdx) msg, err = p.EnactCard(cardIdx, choiceIdx)
case DebugActionEnactable: } else {
msg, err = p.EnactDebugActionUnchecked(cardIdx, choiceIdx)
case PermanentActionEnactable:
msg, err = p.EnactPermanentAction(cardIdx, choiceIdx) msg, err = p.EnactPermanentAction(cardIdx, choiceIdx)
case NothingEnactable:
continue
default:
msg = nil
err = fmt.Errorf("invalid enaction type in action loop: %d", actionType)
} }
p.ReportError(err) p.ReportError(err)
if IsSeriousError(err) { if IsSeriousError(err) {
@ -46,10 +38,6 @@ func RunSimpleTerminalUI[C StatsCollection](p *Player[C]) error {
} }
continue continue
} }
if err != nil {
display(ErrorMessage(err))
display(MsgStr(""))
}
display(msg) display(msg)
wait() wait()
} }
@ -89,7 +77,7 @@ func wait() {
fmt.Scanln() fmt.Scanln()
} }
func displayMainMenu[C StatsCollection](p *Player[C]) (debugOffset, actionsOffset, handOffset, max int) { func displayMainMenu[C StatsCollection](p *Player[C]) (actionsOffset, handOffset, max int) {
cls() cls()
needsDivider := displayMessageSection(p) needsDivider := displayMessageSection(p)
if needsDivider { if needsDivider {
@ -97,14 +85,10 @@ func displayMainMenu[C StatsCollection](p *Player[C]) (debugOffset, actionsOffse
} }
displayOnePanel(p, p.Prompt) displayOnePanel(p, p.Prompt)
divider() divider()
debugOffset = displayStatsMenu(p) actionsOffset = displayStatsMenu(p)
if debugOffset > 0 { if actionsOffset > 0 {
divider() divider()
} }
actionsOffset = displayDebugActionsMenu(p, debugOffset)
if actionsOffset > debugOffset {
fmt.Println()
}
handOffset = displayPermanentActionsMenu(p, actionsOffset) handOffset = displayPermanentActionsMenu(p, actionsOffset)
if handOffset > actionsOffset { if handOffset > actionsOffset {
fmt.Println() fmt.Println()
@ -113,9 +97,9 @@ func displayMainMenu[C StatsCollection](p *Player[C]) (debugOffset, actionsOffse
return // uses named return values return // uses named return values
} }
func pickNextAction[C StatsCollection](p *Player[C]) (actionType EnactableType, cardIdx int, choiceIdx int, err error) { func pickNextAction[C StatsCollection](p *Player[C]) (isCard bool, cardIdx int, choiceIdx int, err error) {
for { for {
debugOffset, actionsOffset, handOffset, max := displayMainMenu(p) actionsOffset, handOffset, max := displayMainMenu(p)
divider() divider()
fmt.Printf("%d actions remaining.\n", p.ActionsRemaining) fmt.Printf("%d actions remaining.\n", p.ActionsRemaining)
@ -133,8 +117,9 @@ func pickNextAction[C StatsCollection](p *Player[C]) (actionType EnactableType,
case "s", "stat", "stats", "i", "info", "p", "panel", "panels", "infopanel", "infopanels": case "s", "stat", "stats", "i", "info", "p", "panel", "panels", "infopanel", "infopanels":
statsMode(p) statsMode(p)
case "a", "act", "actions": case "a", "act", "actions":
actionType, cardIdx, choiceIdx, err = actionsMode(p, true) var committed bool
if actionType != NothingEnactable { isCard, cardIdx, choiceIdx, committed, err = actionsMode(p, true)
if committed {
return return
} }
case "q", "quit", "exit": case "q", "quit", "exit":
@ -147,27 +132,21 @@ func pickNextAction[C StatsCollection](p *Player[C]) (actionType EnactableType,
} else if i > max { } else if i > max {
fmt.Println("That's not on this menu. If the menu is too big to read, choose a detail view.") fmt.Println("That's not on this menu. If the menu is too big to read, choose a detail view.")
wait() wait()
} else if i <= debugOffset { } else if i <= actionsOffset {
cls() cls()
displayOnePanel(p, p.InfoPanels[i-1]) displayOnePanel(p, p.InfoPanels[i-1])
wait() wait()
} else if i <= actionsOffset {
i = i - debugOffset - 1
option, promptErr := promptCard(p, p.DebugActions[i], DebugActionEnactable)
if option >= 0 || IsSeriousError(promptErr) {
return DebugActionEnactable, i, option, promptErr
}
} else if i <= handOffset { } else if i <= handOffset {
i = i - actionsOffset - 1 i = i - actionsOffset - 1
option, promptErr := promptCard(p, p.PermanentActions[i], PermanentActionEnactable) option, promptErr := promptCard(p, p.PermanentActions[i])
if option >= 0 || IsSeriousError(promptErr) { if option >= 0 || IsSeriousError(promptErr) {
return PermanentActionEnactable, i, option, promptErr return false, i, option, promptErr
} }
} else { } else {
i = i - handOffset - 1 i = i - handOffset - 1
option, promptErr := promptCard(p, p.Hand[i], CardEnactable) option, promptErr := promptCard(p, p.Hand[i])
if option >= 0 || IsSeriousError(promptErr) { if option >= 0 || IsSeriousError(promptErr) {
return CardEnactable, i, option, nil return true, i, option, nil
} }
} }
} }
@ -200,7 +179,7 @@ func lightDivider() {
func confirmQuit() { func confirmQuit() {
divider() divider()
fmt.Printf("Are you sure you want to quit? (Y/N) > ") fmt.Println("Are you sure you want to quit? (Y/N) > ")
s := getResponse() s := getResponse()
if s == "y" || s == "yes" { if s == "y" || s == "yes" {
fmt.Println("Bye!") fmt.Println("Bye!")
@ -227,11 +206,14 @@ func displayMessageSection[C StatsCollection](p *Player[C]) bool {
if len(p.TemporaryMessages) == 0 { if len(p.TemporaryMessages) == 0 {
return false return false
} }
hasPrevious := false
for _, m := range p.TemporaryMessages { for _, m := range p.TemporaryMessages {
if m != nil { if m != nil {
if hasPrevious {
lightDivider()
}
display(m) display(m)
} else { hasPrevious = true
fmt.Println()
} }
} }
return true return true
@ -243,7 +225,9 @@ func displayStatsMenu[C StatsCollection](p *Player[C]) int {
} }
fmt.Println("Info Panels") fmt.Println("Info Panels")
fmt.Println("-----------") fmt.Println("-----------")
displayNumberedTitles(p, p.InfoPanels, 0) for i, s := range p.InfoPanels {
fmt.Printf("[%2d]: %s\n", i+1, s.Title(p).String())
}
return len(p.InfoPanels) return len(p.InfoPanels)
} }
@ -253,42 +237,30 @@ func displayPermanentActionsMenu[C StatsCollection](p *Player[C], offset int) in
} }
fmt.Println("Always Available") fmt.Println("Always Available")
fmt.Println("----------------") fmt.Println("----------------")
displayNumberedTitles(p, p.PermanentActions, offset) for i, s := range p.PermanentActions {
fmt.Printf("[%2d]: %s\n", i+offset+1, s.Title(p))
}
return offset + len(p.PermanentActions) return offset + len(p.PermanentActions)
} }
func displayDebugActionsMenu[C StatsCollection](p *Player[C], offset int) int {
if p.DebugLevel < 1 || len(p.DebugActions) == 0 {
return offset
}
fmt.Println("Debug Mode")
fmt.Println("----------")
displayNumberedTitles(p, p.DebugActions, offset)
return offset + len(p.DebugActions)
}
func displayHandMenu[C StatsCollection](p *Player[C], offset int) int { func displayHandMenu[C StatsCollection](p *Player[C], offset int) int {
if len(p.Hand) == 0 { if len(p.Hand) == 0 {
return offset return offset
} }
fmt.Println("Hand") fmt.Println("Hand")
fmt.Println("----") fmt.Println("----")
displayNumberedTitles(p, p.Hand, offset) for i, s := range p.Hand {
return offset + len(p.Hand)
}
func displayNumberedTitles[C StatsCollection, T Titled[C]](p *Player[C], cards []T, offset int) {
for i, s := range cards {
fmt.Printf("[%2d]: %s\n", i+offset+1, s.Title(p)) fmt.Printf("[%2d]: %s\n", i+offset+1, s.Title(p))
} }
return offset + len(p.Hand)
} }
// promptCard asks the player to take an action on a card. Returns the option // promptCard asks the player to take an action on a card. Returns the option
// they chose, or -1 if there was a serious error or they cancelled selection. // they chose, or -1 if there was a serious error or they cancelled selection.
func promptCard[C StatsCollection](p *Player[C], card Card[C], cardType EnactableType) (optionIdx int, err error) { func promptCard[C StatsCollection](p *Player[C], card Card[C]) (optionIdx int, err error) {
// Iterate until the player makes a valid choice. // Iterate until the player makes a valid choice.
for { for {
opts, valid, err := displayCard(p, card, cardType, true) opts, valid, err := displayCard(p, card, true)
if IsSeriousError(err) { if IsSeriousError(err) {
return -1, err return -1, err
} }
@ -326,11 +298,11 @@ func promptCard[C StatsCollection](p *Player[C], card Card[C], cardType Enactabl
} }
} }
func displayCard[C StatsCollection](p *Player[C], card Card[C], cardType EnactableType, canAct bool) ([]CardOption[C], bool, error) { func displayCard[C StatsCollection](p *Player[C], card Card[C], canAct bool) ([]CardOption[C], bool, error) {
cls() cls()
t := card.Title(p).String() t := card.Title(p).String()
urgent := card.Urgent(p) urgent := card.Urgent(p)
if urgent && cardType == CardEnactable { if urgent {
t = "[URGENT!] " + t t = "[URGENT!] " + t
} }
fmt.Println(t) fmt.Println(t)
@ -346,7 +318,7 @@ func displayCard[C StatsCollection](p *Player[C], card Card[C], cardType Enactab
fmt.Println() fmt.Println()
fmt.Println(SectionBreak.String()) fmt.Println(SectionBreak.String())
fmt.Println() fmt.Println()
if !urgent && cardType != DebugActionEnactable && p.HasUrgentCards() { if !urgent && p.HasUrgentCards() {
fmt.Println("<You have more urgent matters to attend to! You cannot act on this right now.>") fmt.Println("<You have more urgent matters to attend to! You cannot act on this right now.>")
fmt.Println() fmt.Println()
canAct = false canAct = false
@ -416,16 +388,12 @@ func statsMode[C StatsCollection](p *Player[C]) error {
} }
} }
func actionsMode[C StatsCollection](p *Player[C], canAct bool) (actionType EnactableType, cardIdx, choiceIdx int, err error) { func actionsMode[C StatsCollection](p *Player[C], canAct bool) (isCard bool, cardIdx, choiceIdx int, committed bool, err error) {
var errs ErrorCollector var errs ErrorCollector
for { for {
cls() cls()
dOff := displayDebugActionsMenu(p, 0) pOff := displayPermanentActionsMenu(p, 0)
if dOff > 0 { if pOff > 0 {
fmt.Println()
}
pOff := displayPermanentActionsMenu(p, dOff)
if pOff > dOff {
fmt.Println() fmt.Println()
} }
max := displayHandMenu(p, pOff) max := displayHandMenu(p, pOff)
@ -435,7 +403,7 @@ func actionsMode[C StatsCollection](p *Player[C], canAct bool) (actionType Enact
fmt.Println("That's a problem. The game is stuck.") fmt.Println("That's a problem. The game is stuck.")
confirmQuit() confirmQuit()
errs.Add(WarningStalemate) errs.Add(WarningStalemate)
return NothingEnactable, -1, -1, errs.Emit() return false, -1, -1, true, errs.Emit()
} }
fmt.Println() fmt.Println()
@ -447,7 +415,7 @@ func actionsMode[C StatsCollection](p *Player[C], canAct bool) (actionType Enact
input := getResponse() input := getResponse()
switch input { switch input {
case "b", "back": case "b", "back":
return NothingEnactable, -1, -1, errs.Emit() return false, -1, -1, false, errs.Emit()
case "q", "quit": case "q", "quit":
confirmQuit() confirmQuit()
default: default:
@ -458,51 +426,35 @@ func actionsMode[C StatsCollection](p *Player[C], canAct bool) (actionType Enact
} else if v < 1 || v > max { } else if v < 1 || v > max {
fmt.Println("That's not a card or action.") fmt.Println("That's not a card or action.")
wait() wait()
} else if v <= dOff { } else if v <= pOff {
v-- v--
if canAct { if canAct {
optIdx, err := promptCard(p, p.DebugActions[v], DebugActionEnactable) optIdx, err := promptCard(p, p.PermanentActions[v])
errs.Add(err) errs.Add(err)
if optIdx >= 0 || IsSeriousError(err) { if optIdx >= 0 || IsSeriousError(err) {
return DebugActionEnactable, v, optIdx, errs.Emit() return false, v, optIdx, true, errs.Emit()
} }
} else { } else {
_, _, err := displayCard(p, p.DebugActions[v], DebugActionEnactable, false) _, _, err := displayCard(p, p.PermanentActions[v], false)
errs.Add(err) errs.Add(err)
if IsSeriousError(err) { if IsSeriousError(err) {
return DebugActionEnactable, -1, -1, errs.Emit() return false, -1, -1, true, errs.Emit()
}
wait()
}
} else if v <= pOff {
v = v - dOff - 1
if canAct {
optIdx, err := promptCard(p, p.PermanentActions[v], PermanentActionEnactable)
errs.Add(err)
if optIdx >= 0 || IsSeriousError(err) {
return PermanentActionEnactable, v, optIdx, errs.Emit()
}
} else {
_, _, err := displayCard(p, p.PermanentActions[v], PermanentActionEnactable, false)
errs.Add(err)
if IsSeriousError(err) {
return PermanentActionEnactable, -1, -1, errs.Emit()
} }
wait() wait()
} }
} else { } else {
v = v - pOff - 1 v = v - pOff - 1
if canAct { if canAct {
optIdx, err := promptCard(p, p.Hand[v], CardEnactable) optIdx, err := promptCard(p, p.Hand[v])
errs.Add(err) errs.Add(err)
if optIdx >= 0 || IsSeriousError(err) { if optIdx >= 0 || IsSeriousError(err) {
return CardEnactable, v, optIdx, errs.Emit() return true, v, optIdx, false, errs.Emit()
} }
} else { } else {
_, _, err := displayCard(p, p.Hand[v], CardEnactable, false) _, _, err := displayCard(p, p.Hand[v], false)
errs.Add(err) errs.Add(err)
if IsSeriousError(err) { if IsSeriousError(err) {
return CardEnactable, -1, -1, errs.Emit() return false, -1, -1, false, errs.Emit()
} }
wait() wait()
} }
@ -515,7 +467,7 @@ func actionsMode[C StatsCollection](p *Player[C], canAct bool) (actionType Enact
func review[C StatsCollection](p *Player[C]) error { func review[C StatsCollection](p *Player[C]) error {
var errs ErrorCollector var errs ErrorCollector
for { for {
debugOffset, actionsOffset, handOffset, max := displayMainMenu(p) actionsOffset, handOffset, max := displayMainMenu(p)
divider() divider()
fmt.Println("No actions remaining.") fmt.Println("No actions remaining.")
fmt.Printf("(C)ontinue, review just (M)essages, (I)nfo Panels, (A)ctions, or an item (1-%d), or (Q)uit? > ", max) fmt.Printf("(C)ontinue, review just (M)essages, (I)nfo Panels, (A)ctions, or an item (1-%d), or (Q)uit? > ", max)
@ -536,7 +488,7 @@ func review[C StatsCollection](p *Player[C]) error {
return errs.Emit() return errs.Emit()
} }
case "a", "act", "actions": case "a", "act", "actions":
_, _, _, err := actionsMode(p, false) _, _, _, _, err := actionsMode(p, false)
errs.Add(err) errs.Add(err)
if IsSeriousError(err) { if IsSeriousError(err) {
return errs.Emit() return errs.Emit()
@ -549,34 +501,31 @@ func review[C StatsCollection](p *Player[C]) error {
i, err := strconv.Atoi(input) i, err := strconv.Atoi(input)
if err != nil { if err != nil {
fmt.Println("Sorry, I don't understand.") fmt.Println("Sorry, I don't understand.")
wait()
} else if i <= 0 || i > max { } else if i <= 0 || i > max {
fmt.Println("That's not on this menu. If the menu is too big to read, choose a detail view.") fmt.Println("That's not on this menu. If the menu is too big to read, choose a detail view.")
} else if i <= debugOffset { wait()
} else if i <= actionsOffset {
cls() cls()
displayOnePanel(p, p.InfoPanels[i-1]) displayOnePanel(p, p.InfoPanels[i-1])
} else if i <= actionsOffset { wait()
i = i - debugOffset - 1
_, _, err := displayCard(p, p.DebugActions[i], DebugActionEnactable, false)
errs.Add(err)
if IsSeriousError(err) {
return errs.Emit()
}
} else if i <= handOffset { } else if i <= handOffset {
i = i - actionsOffset - 1 i = i - actionsOffset - 1
_, _, err := displayCard(p, p.PermanentActions[i], PermanentActionEnactable, false) _, _, err := displayCard(p, p.PermanentActions[i], false)
errs.Add(err) errs.Add(err)
if IsSeriousError(err) { if IsSeriousError(err) {
return errs.Emit() return errs.Emit()
} }
wait()
} else { } else {
i = i - handOffset - 1 i = i - handOffset - 1
_, _, err := displayCard(p, p.Hand[i], CardEnactable, false) _, _, err := displayCard(p, p.Hand[i], false)
errs.Add(err) errs.Add(err)
if IsSeriousError(err) { if IsSeriousError(err) {
return errs.Emit() return errs.Emit()
} }
}
wait() wait()
} }
} }
} }
}

6
go.mod
View File

@ -1,11 +1,9 @@
module git.chromaticdragon.app/kistaro/CardSimEngine module cardSimEngine
go 1.20 go 1.20
require github.com/kr/pretty v0.3.1
require ( require (
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect github.com/kr/text v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect
golang.org/x/exp v0.0.0-20230321023759-10a507213a29
) )

2
go.sum
View File

@ -6,5 +6,3 @@ 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/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 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 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=

View File

@ -1,10 +1,9 @@
package main package main
import ( import (
"cardSimEngine/cardsim"
"errors" "errors"
"fmt" "fmt"
"git.chromaticdragon.app/kistaro/CardSimEngine/cardsim"
) )
// Type aliases, unlike distinctly named types, are fully substitutable for // Type aliases, unlike distinctly named types, are fully substitutable for
@ -85,7 +84,7 @@ func (i inverseDivision) OptionText(p *player) (cardsim.Message, error) {
func (i inverseDivision) Enact(p *player) (cardsim.Message, error) { func (i inverseDivision) Enact(p *player) (cardsim.Message, error) {
if p.Stats.Number.Value == 0 { if p.Stats.Number.Value == 0 {
return nil, errors.New("you can't divide by zero") return nil, errors.New("you can't divide by zero!")
} }
p.Stats.Number.Value = int(i) / p.Stats.Number.Value p.Stats.Number.Value = int(i) / p.Stats.Number.Value
return cardsim.MsgStr("Inverse divided."), nil return cardsim.MsgStr("Inverse divided."), nil
@ -119,8 +118,8 @@ func initDeck(d *cardsim.Deck[*SmokeTestCollection]) {
func installPermanentActions(pa *[]card) { func installPermanentActions(pa *[]card) {
*pa = []card{ *pa = []card{
&cardsim.BasicCard[*SmokeTestCollection]{ &cardsim.BasicCard[*SmokeTestCollection]{
CardTitle: cardsim.MsgStr("Reset Number"), CardTitle: cardsim.MsgStr("Reset to 0"),
CardText: cardsim.MsgStr("Resets Number to a fixed value."), CardText: cardsim.MsgStr("Resets Number to 0."),
CardOptions: []cardOption{ CardOptions: []cardOption{
&cardsim.BasicOption[*SmokeTestCollection]{ &cardsim.BasicOption[*SmokeTestCollection]{
Text: cardsim.MsgStr("Reset to 0."), Text: cardsim.MsgStr("Reset to 0."),
@ -130,6 +129,12 @@ func installPermanentActions(pa *[]card) {
}, },
Output: cardsim.MsgStr("Done."), Output: cardsim.MsgStr("Done."),
}, },
},
},
&cardsim.BasicCard[*SmokeTestCollection]{
CardTitle: cardsim.MsgStr("Reset to 1000000"),
CardText: cardsim.MsgStr("Resets Number to one million."),
CardOptions: []cardOption{
&cardsim.BasicOption[*SmokeTestCollection]{ &cardsim.BasicOption[*SmokeTestCollection]{
Text: cardsim.MsgStr("Reset to 1,000,000"), Text: cardsim.MsgStr("Reset to 1,000,000"),
Effect: func(p *player) error { Effect: func(p *player) error {
@ -140,5 +145,18 @@ func installPermanentActions(pa *[]card) {
}, },
}, },
}, },
&cardsim.BasicCard[*SmokeTestCollection]{
CardTitle: cardsim.MsgStr("Draw a card"),
CardText: cardsim.MsgStr("Draw an extra card."),
CardOptions: []cardOption{
&cardsim.BasicOption[*SmokeTestCollection]{
Text: cardsim.MsgStr("Draw an extra card."),
Effect: func(p *player) error {
return p.Draw()
},
Output: cardsim.MsgStr("Drawn. Probably."),
},
},
},
} }
} }

View File

@ -1,7 +1,7 @@
package main package main
import ( import (
"git.chromaticdragon.app/kistaro/CardSimEngine/cardsim" "cardSimEngine/cardsim"
) )
// SmokeTestCollection is a stats collection for the simple test sim. // SmokeTestCollection is a stats collection for the simple test sim.
@ -11,11 +11,6 @@ type SmokeTestCollection struct {
Turns cardsim.Invisible[int] Turns cardsim.Invisible[int]
Flavor cardsim.Stored[string] Flavor cardsim.Stored[string]
Things int `cardsim:"stat" cardsim_name:"A Renamed Thing"`
MoreThings int `cardsim:"hidden"`
FloatyThings float64 `cardsim:"round1"`
Label string `cardsim:"stat"`
} }
func (c *SmokeTestCollection) Average() float64 { func (c *SmokeTestCollection) Average() float64 {
@ -28,7 +23,3 @@ func (c *SmokeTestCollection) Stats() []cardsim.Stat {
cardsim.SortStats(stats) cardsim.SortStats(stats)
return stats return stats
} }
func (c *SmokeTestCollection) StatTotalThings() float64 {
return float64(c.Things+c.MoreThings) + c.FloatyThings
}

View File

@ -2,9 +2,10 @@
package main package main
import ( import (
"cardSimEngine/cardsim"
"fmt" "fmt"
"git.chromaticdragon.app/kistaro/CardSimEngine/cardsim" "github.com/kr/pretty"
) )
func main() { func main() {
@ -26,10 +27,6 @@ func main() {
Name: "Flavor", Name: "Flavor",
Value: "Lemon", Value: "Lemon",
}, },
Things: 5,
MoreThings: 9,
FloatyThings: 123.456,
Label: "whee",
}, },
) )
p.Name = "Dave" p.Name = "Dave"
@ -43,6 +40,7 @@ func main() {
Name: cardsim.MsgStr("Stats"), Name: cardsim.MsgStr("Stats"),
Intro: cardsim.MsgStr("Hi! These are the smoke test stats."), Intro: cardsim.MsgStr("Hi! These are the smoke test stats."),
}, },
ruledumper{},
} }
p.Prompt = prompt{} p.Prompt = prompt{}
p.DebugLevel = 5 p.DebugLevel = 5
@ -66,6 +64,15 @@ func (prompt) Info(p *cardsim.Player[*SmokeTestCollection]) ([]cardsim.Message,
return []cardsim.Message{ return []cardsim.Message{
cardsim.MsgStr("Here, have some stuff."), cardsim.MsgStr("Here, have some stuff."),
cardsim.Msgf("It's turn %d according to the player and turn %d according to me.", p.TurnNumber, p.Stats.Turns.Value), cardsim.Msgf("It's turn %d according to the player and turn %d according to me.", p.TurnNumber, p.Stats.Turns.Value),
cardsim.Msgf("The current Number is %d. It tastes like %s.", p.Stats.Number.Value, p.Stats.Flavor.Value),
}, nil }, nil
} }
type ruledumper struct{}
func (ruledumper) Title(p *player) cardsim.Message {
return cardsim.MsgStr("Rule Dumper")
}
func (ruledumper) Info(p *player) ([]cardsim.Message, error) {
return []cardsim.Message{cardsim.Msgf("%# v", pretty.Formatter(p.Rules))}, nil
}

View File

@ -1,9 +1,8 @@
package main package main
import ( import (
"cardSimEngine/cardsim"
"math" "math"
"git.chromaticdragon.app/kistaro/CardSimEngine/cardsim"
) )
var ( var (