Compare commits

..

14 Commits

Author SHA1 Message Date
73c2826273
Don't add an extra line when offering a "quit" prompt. 2024-09-29 12:59:21 -07:00
c30aca1f31
Better error management.
* "Uncooperative cards" is now a warning.
* Cards and actions get "Then" invoked before the card processor considers erroring out.
* Terminal UI: Errors and warnings from actions are displayed during the response; they're not only added to the temporary messages now.
2023-04-15 20:59:21 -07:00
abb00e30c3
Debug action to adjust debug level. 2023-04-15 20:16:54 -07:00
65c01318f0
Fix UI urgency handling.
Only cards can block other cards or actions from being used due to urgency, so only display the `[URGENT!]` header for cards. Debug actions can't be blocked, so skip the "urgency conflict" check for those.
2023-04-15 19:43:11 -07:00
2b788f517c
Add action counter spoofer. 2023-04-15 19:36:53 -07:00
8d1aa0141f
Don't stomp on the default debugers in SmokeTest. 2023-04-15 19:17:34 -07:00
74fac625f2
Implement standard debuggers.
These debug actions are added to all players by default.
2023-04-15 19:16:08 -07:00
22c4718faf
Include the Number in the smoke test display. 2023-04-15 17:30:08 -07:00
deb3b1c5a1
Don't charge an action point for debug drawing. 2023-04-15 17:26:02 -07:00
4a91230376
Smoke testing for debug actions.
Just moves one of the existing actions to a debug action slot.
2023-04-15 17:21:28 -07:00
54711b36a8
Display debug enactables.
Includes some refactoring work to pull out common code and express the idea of "wait, which panel, exactly?".
2023-04-15 17:17:51 -07:00
6c3c936dbd
Debug Actions: Another set of permanent actions.
These are only reachable in debug mode.
2023-04-15 16:06:55 -07:00
8d9303c8bc
stats manual in README 2023-04-04 12:29:36 -07:00
ad9e5764f1
Allow renaming of extracted stats via tag. 2023-04-04 12:14:04 -07:00
13 changed files with 602 additions and 129 deletions

View File

@ -42,7 +42,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. 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).
### StatsCollection ### StatsCollection
@ -57,3 +57,26 @@ 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,6 +101,63 @@ 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 {
@ -151,3 +208,17 @@ 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
}),
}
}

103
cardsim/debugging.go Normal file
View File

@ -0,0 +1,103 @@
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,3 +279,145 @@ 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,6 +12,12 @@ 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 {
@ -29,6 +35,19 @@ 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,13 +8,14 @@ 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 = errors.New("no actions can be taken") WarningStalemate = &Warning{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.
@ -104,6 +105,10 @@ 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]
@ -170,6 +175,12 @@ 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](),
},
} }
} }
@ -261,9 +272,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 crashes with // If more than a million cards refuse to enter the hand, this gives up and
// ErrUncooperativeCards. If the deck does not have enough cards, this // returns WarningUncooperativeCards. If the deck does not have enough cards,
// returns WarningTooFewCards. // this 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 {
@ -275,13 +286,13 @@ func (p *Player[C]) Draw() error {
return nil return nil
} }
} }
return ErrUncooperativeCards return WarningUncoperativeCards
} }
// 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 crashes // drawn. If more than a million cards refuse to enter the hand, this gives up
// with ErrUncooperativeCards. If the deck does not have enough cards, this // and returns WarningUncooperativeCards. If the deck does not have enough
// returns WarningTooFewCards. // cards, this 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 {
@ -308,6 +319,38 @@ 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
@ -343,17 +386,14 @@ 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, errs.Emit() return ret, err
} }
// EnactCard executes a card choice, removes it from the hand, and decrements // EnactCard executes a card choice, removes it from the hand, and decrements
@ -381,10 +421,31 @@ 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) {
if actionIdx < 0 || actionIdx >= len(p.PermanentActions) { return p.enactActionUnchecked(p.PermanentActions, actionIdx, choiceIdx)
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) {
@ -393,12 +454,12 @@ func (p *Player[C]) EnactPermanentActionUnchecked(actionIdx, choiceIdx int) (Mes
} }
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 permanent action #%d with %d options", ErrInvalidChoice, choiceIdx, actionIdx, len(options))) errs.Add(fmt.Errorf("%w: no option #%d on 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 permanent action #%d is not enabled", ErrInvalidChoice, choiceIdx, actionIdx)) errs.Add(fmt.Errorf("%w: option #%d on action #%d is not enabled", ErrInvalidChoice, choiceIdx, actionIdx))
return nil, errs.Emit() return nil, errs.Emit()
} }
@ -406,17 +467,13 @@ func (p *Player[C]) EnactPermanentActionUnchecked(actionIdx, choiceIdx int) (Mes
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)
if IsSeriousError(err) { retErr := errs.Emit()
if IsSeriousError(retErr) {
p.State = GameCrashed p.State = GameCrashed
} }
return ret, errs.Emit() return ret, retErr
} }
// EnactPermanentAction executes a permanently-available card and decrements // EnactPermanentAction executes a permanently-available card and decrements
@ -448,15 +505,11 @@ func (p *Player[C]) ReportError(e error) {
if e == nil || p.DebugLevel < -1 { if e == nil || p.DebugLevel < -1 {
return return
} }
if p.DebugLevel < 0 && !IsSeriousError(e) { minLvl := NotDebugging
return
}
p.ChapterBreak()
severity := "[Warning]"
if IsSeriousError(e) { if IsSeriousError(e) {
severity = "[ERROR]" minLvl = HideWarnings
} }
p.TemporaryMessages = append(p.TemporaryMessages, Msgf("%s: %v", severity, e)) p.Debug(minLvl, ErrorMessage(e))
} }
// CanAct returns whether the player has actions theoretically available. // CanAct returns whether the player has actions theoretically available.
@ -470,6 +523,7 @@ 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,6 +4,8 @@ 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.
@ -402,3 +404,14 @@ 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,3 +145,21 @@ 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

@ -138,6 +138,10 @@ func (s statFunc[T]) Visible() bool {
// start of another word, if it's not at the end). To insert a space between // 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 // consecutive capital letters, insert an underscore (`_`). This name inference
// trims "Stat" and "HiddenStat" off the front of method names. // 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() {
@ -248,6 +252,9 @@ func ExtractStats(x any) []Stat {
continue // not identifiably a stat continue // not identifiably a stat
} }
nm := strings.Join(explode(sf.Name), " ") nm := strings.Join(explode(sf.Name), " ")
if t := sf.Tag.Get("cardsim_name"); t != "" {
nm = t
}
if known[nm] { if known[nm] {
continue continue
} }

View File

@ -17,7 +17,7 @@ func RunSimpleTerminalUI[C StatsCollection](p *Player[C]) error {
for { for {
for p.CanAct() { for p.CanAct() {
isCard, cardIdx, choiceIdx, err := pickNextAction(p) actionType, 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,10 +26,18 @@ func RunSimpleTerminalUI[C StatsCollection](p *Player[C]) error {
continue continue
} }
var msg Message var msg Message
if isCard { switch actionType {
case CardEnactable:
msg, err = p.EnactCard(cardIdx, choiceIdx) msg, err = p.EnactCard(cardIdx, choiceIdx)
} else { case DebugActionEnactable:
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) {
@ -38,6 +46,10 @@ func RunSimpleTerminalUI[C StatsCollection](p *Player[C]) error {
} }
continue continue
} }
if err != nil {
display(ErrorMessage(err))
display(MsgStr(""))
}
display(msg) display(msg)
wait() wait()
} }
@ -77,7 +89,7 @@ func wait() {
fmt.Scanln() fmt.Scanln()
} }
func displayMainMenu[C StatsCollection](p *Player[C]) (actionsOffset, handOffset, max int) { func displayMainMenu[C StatsCollection](p *Player[C]) (debugOffset, actionsOffset, handOffset, max int) {
cls() cls()
needsDivider := displayMessageSection(p) needsDivider := displayMessageSection(p)
if needsDivider { if needsDivider {
@ -85,10 +97,14 @@ func displayMainMenu[C StatsCollection](p *Player[C]) (actionsOffset, handOffset
} }
displayOnePanel(p, p.Prompt) displayOnePanel(p, p.Prompt)
divider() divider()
actionsOffset = displayStatsMenu(p) debugOffset = displayStatsMenu(p)
if actionsOffset > 0 { if debugOffset > 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()
@ -97,9 +113,9 @@ func displayMainMenu[C StatsCollection](p *Player[C]) (actionsOffset, handOffset
return // uses named return values return // uses named return values
} }
func pickNextAction[C StatsCollection](p *Player[C]) (isCard bool, cardIdx int, choiceIdx int, err error) { func pickNextAction[C StatsCollection](p *Player[C]) (actionType EnactableType, cardIdx int, choiceIdx int, err error) {
for { for {
actionsOffset, handOffset, max := displayMainMenu(p) debugOffset, actionsOffset, handOffset, max := displayMainMenu(p)
divider() divider()
fmt.Printf("%d actions remaining.\n", p.ActionsRemaining) fmt.Printf("%d actions remaining.\n", p.ActionsRemaining)
@ -117,9 +133,8 @@ func pickNextAction[C StatsCollection](p *Player[C]) (isCard bool, cardIdx int,
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":
var committed bool actionType, cardIdx, choiceIdx, err = actionsMode(p, true)
isCard, cardIdx, choiceIdx, committed, err = actionsMode(p, true) if actionType != NothingEnactable {
if committed {
return return
} }
case "q", "quit", "exit": case "q", "quit", "exit":
@ -132,21 +147,27 @@ func pickNextAction[C StatsCollection](p *Player[C]) (isCard bool, cardIdx int,
} 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 <= actionsOffset { } else if i <= debugOffset {
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]) option, promptErr := promptCard(p, p.PermanentActions[i], PermanentActionEnactable)
if option >= 0 || IsSeriousError(promptErr) { if option >= 0 || IsSeriousError(promptErr) {
return false, i, option, promptErr return PermanentActionEnactable, i, option, promptErr
} }
} else { } else {
i = i - handOffset - 1 i = i - handOffset - 1
option, promptErr := promptCard(p, p.Hand[i]) option, promptErr := promptCard(p, p.Hand[i], CardEnactable)
if option >= 0 || IsSeriousError(promptErr) { if option >= 0 || IsSeriousError(promptErr) {
return true, i, option, nil return CardEnactable, i, option, nil
} }
} }
} }
@ -179,7 +200,7 @@ func lightDivider() {
func confirmQuit() { func confirmQuit() {
divider() divider()
fmt.Println("Are you sure you want to quit? (Y/N) > ") fmt.Printf("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!")
@ -222,9 +243,7 @@ func displayStatsMenu[C StatsCollection](p *Player[C]) int {
} }
fmt.Println("Info Panels") fmt.Println("Info Panels")
fmt.Println("-----------") fmt.Println("-----------")
for i, s := range p.InfoPanels { displayNumberedTitles(p, p.InfoPanels, 0)
fmt.Printf("[%2d]: %s\n", i+1, s.Title(p).String())
}
return len(p.InfoPanels) return len(p.InfoPanels)
} }
@ -234,30 +253,42 @@ func displayPermanentActionsMenu[C StatsCollection](p *Player[C], offset int) in
} }
fmt.Println("Always Available") fmt.Println("Always Available")
fmt.Println("----------------") fmt.Println("----------------")
for i, s := range p.PermanentActions { displayNumberedTitles(p, p.PermanentActions, offset)
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("----")
for i, s := range p.Hand { displayNumberedTitles(p, p.Hand, offset)
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]) (optionIdx int, err error) { func promptCard[C StatsCollection](p *Player[C], card Card[C], cardType EnactableType) (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, true) opts, valid, err := displayCard(p, card, cardType, true)
if IsSeriousError(err) { if IsSeriousError(err) {
return -1, err return -1, err
} }
@ -295,11 +326,11 @@ func promptCard[C StatsCollection](p *Player[C], card Card[C]) (optionIdx int, e
} }
} }
func displayCard[C StatsCollection](p *Player[C], card Card[C], canAct bool) ([]CardOption[C], bool, error) { func displayCard[C StatsCollection](p *Player[C], card Card[C], cardType EnactableType, 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 { if urgent && cardType == CardEnactable {
t = "[URGENT!] " + t t = "[URGENT!] " + t
} }
fmt.Println(t) fmt.Println(t)
@ -315,7 +346,7 @@ func displayCard[C StatsCollection](p *Player[C], card Card[C], canAct bool) ([]
fmt.Println() fmt.Println()
fmt.Println(SectionBreak.String()) fmt.Println(SectionBreak.String())
fmt.Println() fmt.Println()
if !urgent && p.HasUrgentCards() { if !urgent && cardType != DebugActionEnactable && 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
@ -385,12 +416,16 @@ func statsMode[C StatsCollection](p *Player[C]) error {
} }
} }
func actionsMode[C StatsCollection](p *Player[C], canAct bool) (isCard bool, cardIdx, choiceIdx int, committed bool, err error) { func actionsMode[C StatsCollection](p *Player[C], canAct bool) (actionType EnactableType, cardIdx, choiceIdx int, err error) {
var errs ErrorCollector var errs ErrorCollector
for { for {
cls() cls()
pOff := displayPermanentActionsMenu(p, 0) dOff := displayDebugActionsMenu(p, 0)
if pOff > 0 { if dOff > 0 {
fmt.Println()
}
pOff := displayPermanentActionsMenu(p, dOff)
if pOff > dOff {
fmt.Println() fmt.Println()
} }
max := displayHandMenu(p, pOff) max := displayHandMenu(p, pOff)
@ -400,7 +435,7 @@ func actionsMode[C StatsCollection](p *Player[C], canAct bool) (isCard bool, car
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 false, -1, -1, true, errs.Emit() return NothingEnactable, -1, -1, errs.Emit()
} }
fmt.Println() fmt.Println()
@ -412,7 +447,7 @@ func actionsMode[C StatsCollection](p *Player[C], canAct bool) (isCard bool, car
input := getResponse() input := getResponse()
switch input { switch input {
case "b", "back": case "b", "back":
return false, -1, -1, false, errs.Emit() return NothingEnactable, -1, -1, errs.Emit()
case "q", "quit": case "q", "quit":
confirmQuit() confirmQuit()
default: default:
@ -423,35 +458,51 @@ func actionsMode[C StatsCollection](p *Player[C], canAct bool) (isCard bool, car
} 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 <= pOff { } else if v <= dOff {
v-- v--
if canAct { if canAct {
optIdx, err := promptCard(p, p.PermanentActions[v]) optIdx, err := promptCard(p, p.DebugActions[v], DebugActionEnactable)
errs.Add(err) errs.Add(err)
if optIdx >= 0 || IsSeriousError(err) { if optIdx >= 0 || IsSeriousError(err) {
return false, v, optIdx, true, errs.Emit() return DebugActionEnactable, v, optIdx, errs.Emit()
} }
} else { } else {
_, _, err := displayCard(p, p.PermanentActions[v], false) _, _, err := displayCard(p, p.DebugActions[v], DebugActionEnactable, false)
errs.Add(err) errs.Add(err)
if IsSeriousError(err) { if IsSeriousError(err) {
return false, -1, -1, true, errs.Emit() return DebugActionEnactable, -1, -1, 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]) optIdx, err := promptCard(p, p.Hand[v], CardEnactable)
errs.Add(err) errs.Add(err)
if optIdx >= 0 || IsSeriousError(err) { if optIdx >= 0 || IsSeriousError(err) {
return true, v, optIdx, false, errs.Emit() return CardEnactable, v, optIdx, errs.Emit()
} }
} else { } else {
_, _, err := displayCard(p, p.Hand[v], false) _, _, err := displayCard(p, p.Hand[v], CardEnactable, false)
errs.Add(err) errs.Add(err)
if IsSeriousError(err) { if IsSeriousError(err) {
return false, -1, -1, false, errs.Emit() return CardEnactable, -1, -1, errs.Emit()
} }
wait() wait()
} }
@ -464,7 +515,7 @@ func actionsMode[C StatsCollection](p *Player[C], canAct bool) (isCard bool, car
func review[C StatsCollection](p *Player[C]) error { func review[C StatsCollection](p *Player[C]) error {
var errs ErrorCollector var errs ErrorCollector
for { for {
actionsOffset, handOffset, max := displayMainMenu(p) debugOffset, 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)
@ -485,7 +536,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()
@ -498,31 +549,34 @@ 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.")
wait() } else if i <= debugOffset {
} else if i <= actionsOffset {
cls() cls()
displayOnePanel(p, p.InfoPanels[i-1]) displayOnePanel(p, p.InfoPanels[i-1])
wait() } else if i <= actionsOffset {
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], false) _, _, err := displayCard(p, p.PermanentActions[i], PermanentActionEnactable, 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], false) _, _, err := displayCard(p, p.Hand[i], CardEnactable, false)
errs.Add(err) errs.Add(err)
if IsSeriousError(err) { if IsSeriousError(err) {
return errs.Emit() return errs.Emit()
} }
}
wait() wait()
} }
} }
} }
}

View File

@ -85,7 +85,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 +119,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 to 0"), CardTitle: cardsim.MsgStr("Reset Number"),
CardText: cardsim.MsgStr("Resets Number to 0."), CardText: cardsim.MsgStr("Resets Number to a fixed value."),
CardOptions: []cardOption{ CardOptions: []cardOption{
&cardsim.BasicOption[*SmokeTestCollection]{ &cardsim.BasicOption[*SmokeTestCollection]{
Text: cardsim.MsgStr("Reset to 0."), Text: cardsim.MsgStr("Reset to 0."),
@ -130,12 +130,6 @@ 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 {
@ -146,18 +140,5 @@ 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

@ -12,7 +12,7 @@ type SmokeTestCollection struct {
Flavor cardsim.Stored[string] Flavor cardsim.Stored[string]
Things int `cardsim:"stat"` Things int `cardsim:"stat" cardsim_name:"A Renamed Thing"`
MoreThings int `cardsim:"hidden"` MoreThings int `cardsim:"hidden"`
FloatyThings float64 `cardsim:"round1"` FloatyThings float64 `cardsim:"round1"`
Label string `cardsim:"stat"` Label string `cardsim:"stat"`

View File

@ -5,8 +5,6 @@ import (
"fmt" "fmt"
"git.chromaticdragon.app/kistaro/CardSimEngine/cardsim" "git.chromaticdragon.app/kistaro/CardSimEngine/cardsim"
"github.com/kr/pretty"
) )
func main() { func main() {
@ -45,7 +43,6 @@ 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
@ -69,15 +66,6 @@ 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
}