7 Commits

Author SHA1 Message Date
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
8 changed files with 205 additions and 56 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

@ -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 {

View File

@ -13,6 +13,7 @@ var (
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 = errors.New("no actions can be taken")
) )
@ -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.
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]
@ -308,6 +313,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
@ -381,10 +418,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)) }
// 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
} }
card := p.PermanentActions[actionIdx] 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 +451,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()
} }

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) {
@ -77,7 +85,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 +93,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 +109,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 +129,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 +143,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])
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])
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])
if option >= 0 || IsSeriousError(promptErr) { if option >= 0 || IsSeriousError(promptErr) {
return true, i, option, nil return CardEnactable, i, option, nil
} }
} }
} }
@ -222,9 +239,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,22 +249,34 @@ 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
@ -385,12 +412,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 +431,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 +443,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,19 +454,35 @@ 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 {
optIdx, err := promptCard(p, p.DebugActions[v])
errs.Add(err)
if optIdx >= 0 || IsSeriousError(err) {
return DebugActionEnactable, v, optIdx, errs.Emit()
}
} else {
_, _, err := displayCard(p, p.DebugActions[v], false)
errs.Add(err)
if IsSeriousError(err) {
return DebugActionEnactable, -1, -1, errs.Emit()
}
wait()
}
} else if v <= pOff {
v = v - dOff - 1
if canAct { if canAct {
optIdx, err := promptCard(p, p.PermanentActions[v]) optIdx, err := promptCard(p, p.PermanentActions[v])
errs.Add(err) errs.Add(err)
if optIdx >= 0 || IsSeriousError(err) { if optIdx >= 0 || IsSeriousError(err) {
return false, v, optIdx, true, errs.Emit() return PermanentActionEnactable, v, optIdx, errs.Emit()
} }
} else { } else {
_, _, err := displayCard(p, p.PermanentActions[v], false) _, _, err := displayCard(p, p.PermanentActions[v], false)
errs.Add(err) errs.Add(err)
if IsSeriousError(err) { if IsSeriousError(err) {
return false, -1, -1, true, errs.Emit() return PermanentActionEnactable, -1, -1, errs.Emit()
} }
wait() wait()
} }
@ -445,13 +492,13 @@ func actionsMode[C StatsCollection](p *Player[C], canAct bool) (isCard bool, car
optIdx, err := promptCard(p, p.Hand[v]) optIdx, err := promptCard(p, p.Hand[v])
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], 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 +511,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 +532,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,14 +545,18 @@ 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], 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], false)
@ -513,7 +564,6 @@ func review[C StatsCollection](p *Player[C]) error {
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], false)
@ -521,8 +571,8 @@ func review[C StatsCollection](p *Player[C]) error {
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,6 +140,11 @@ func installPermanentActions(pa *[]card) {
}, },
}, },
}, },
}
}
func installDebugActions(pa *[]card) {
*pa = []card{
&cardsim.BasicCard[*SmokeTestCollection]{ &cardsim.BasicCard[*SmokeTestCollection]{
CardTitle: cardsim.MsgStr("Draw a card"), CardTitle: cardsim.MsgStr("Draw a card"),
CardText: cardsim.MsgStr("Draw an extra card."), CardText: cardsim.MsgStr("Draw an extra card."),
@ -158,6 +157,10 @@ func installPermanentActions(pa *[]card) {
Output: cardsim.MsgStr("Drawn. Probably."), Output: cardsim.MsgStr("Drawn. Probably."),
}, },
}, },
AfterOption: func(c card, p *player, option cardOption) error {
p.ActionsRemaining++
return nil
},
}, },
} }
} }

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

@ -40,6 +40,7 @@ func main() {
installRules(p.Rules) installRules(p.Rules)
initDeck(p.Deck) initDeck(p.Deck)
installPermanentActions(&p.PermanentActions) installPermanentActions(&p.PermanentActions)
installDebugActions(&p.DebugActions)
p.InfoPanels = []cardsim.InfoPanel[*SmokeTestCollection]{ p.InfoPanels = []cardsim.InfoPanel[*SmokeTestCollection]{
&cardsim.BasicStatsPanel[*SmokeTestCollection]{ &cardsim.BasicStatsPanel[*SmokeTestCollection]{
Name: cardsim.MsgStr("Stats"), Name: cardsim.MsgStr("Stats"),
@ -69,6 +70,7 @@ 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
} }