From 54711b36a8ca70f41ea562d572a3452c7b9ce697 Mon Sep 17 00:00:00 2001 From: Kistaro Windrider Date: Sat, 15 Apr 2023 17:17:51 -0700 Subject: [PATCH] Display debug enactables. Includes some refactoring work to pull out common code and express the idea of "wait, which panel, exactly?". --- cardsim/messages.go | 6 ++ cardsim/player.go | 32 +++++++++++ cardsim/terminalui.go | 130 +++++++++++++++++++++++++++++------------- 3 files changed, 128 insertions(+), 40 deletions(-) diff --git a/cardsim/messages.go b/cardsim/messages.go index 456c60a..32345fe 100644 --- a/cardsim/messages.go +++ b/cardsim/messages.go @@ -12,6 +12,12 @@ type Message interface { 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 func (s stringMessage) String() string { diff --git a/cardsim/player.go b/cardsim/player.go index 1fea13b..b4c16ef 100644 --- a/cardsim/player.go +++ b/cardsim/player.go @@ -313,6 +313,38 @@ func (p *Player[C]) HasUrgentCards() bool { 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 // decrements the ActionsRemaining. It does not check for conflicting Urgent // cards or already being out of actions. If no such card or card choice diff --git a/cardsim/terminalui.go b/cardsim/terminalui.go index a4da28d..4565cbe 100644 --- a/cardsim/terminalui.go +++ b/cardsim/terminalui.go @@ -17,7 +17,7 @@ func RunSimpleTerminalUI[C StatsCollection](p *Player[C]) error { for { for p.CanAct() { - isCard, cardIdx, choiceIdx, err := pickNextAction(p) + actionType, cardIdx, choiceIdx, err := pickNextAction(p) p.ReportError(err) if IsSeriousError(err) { if p.DebugLevel < 1 { @@ -26,10 +26,18 @@ func RunSimpleTerminalUI[C StatsCollection](p *Player[C]) error { continue } var msg Message - if isCard { + switch actionType { + case CardEnactable: msg, err = p.EnactCard(cardIdx, choiceIdx) - } else { + case DebugActionEnactable: + msg, err = p.EnactDebugActionUnchecked(cardIdx, choiceIdx) + case PermanentActionEnactable: 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) if IsSeriousError(err) { @@ -77,7 +85,7 @@ func wait() { 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() needsDivider := displayMessageSection(p) if needsDivider { @@ -85,10 +93,14 @@ func displayMainMenu[C StatsCollection](p *Player[C]) (actionsOffset, handOffset } displayOnePanel(p, p.Prompt) divider() - actionsOffset = displayStatsMenu(p) - if actionsOffset > 0 { + debugOffset = displayStatsMenu(p) + if debugOffset > 0 { divider() } + actionsOffset = displayDebugActionsMenu(p, debugOffset) + if actionsOffset > debugOffset { + fmt.Println() + } handOffset = displayPermanentActionsMenu(p, actionsOffset) if handOffset > actionsOffset { fmt.Println() @@ -97,9 +109,9 @@ func displayMainMenu[C StatsCollection](p *Player[C]) (actionsOffset, handOffset 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 { - actionsOffset, handOffset, max := displayMainMenu(p) + debugOffset, actionsOffset, handOffset, max := displayMainMenu(p) divider() 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": statsMode(p) case "a", "act", "actions": - var committed bool - isCard, cardIdx, choiceIdx, committed, err = actionsMode(p, true) - if committed { + actionType, cardIdx, choiceIdx, err = actionsMode(p, true) + if actionType != NothingEnactable { return } case "q", "quit", "exit": @@ -132,21 +143,27 @@ func pickNextAction[C StatsCollection](p *Player[C]) (isCard bool, cardIdx int, } else if i > max { fmt.Println("That's not on this menu. If the menu is too big to read, choose a detail view.") wait() - } else if i <= actionsOffset { + } else if i <= debugOffset { cls() displayOnePanel(p, p.InfoPanels[i-1]) 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 { i = i - actionsOffset - 1 option, promptErr := promptCard(p, p.PermanentActions[i]) if option >= 0 || IsSeriousError(promptErr) { - return false, i, option, promptErr + return PermanentActionEnactable, i, option, promptErr } } else { i = i - handOffset - 1 option, promptErr := promptCard(p, p.Hand[i]) 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("-----------") - for i, s := range p.InfoPanels { - fmt.Printf("[%2d]: %s\n", i+1, s.Title(p).String()) - } + displayNumberedTitles(p, p.InfoPanels, 0) return len(p.InfoPanels) } @@ -234,22 +249,34 @@ func displayPermanentActionsMenu[C StatsCollection](p *Player[C], offset int) in } fmt.Println("Always Available") fmt.Println("----------------") - for i, s := range p.PermanentActions { - fmt.Printf("[%2d]: %s\n", i+offset+1, s.Title(p)) - } + displayNumberedTitles(p, p.PermanentActions, offset) 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 { if len(p.Hand) == 0 { return offset } fmt.Println("Hand") 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)) } - return offset + len(p.Hand) } // 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 for { cls() - pOff := displayPermanentActionsMenu(p, 0) - if pOff > 0 { + dOff := displayDebugActionsMenu(p, 0) + if dOff > 0 { + fmt.Println() + } + pOff := displayPermanentActionsMenu(p, dOff) + if pOff > dOff { fmt.Println() } 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.") confirmQuit() errs.Add(WarningStalemate) - return false, -1, -1, true, errs.Emit() + return NothingEnactable, -1, -1, errs.Emit() } fmt.Println() @@ -412,7 +443,7 @@ func actionsMode[C StatsCollection](p *Player[C], canAct bool) (isCard bool, car input := getResponse() switch input { case "b", "back": - return false, -1, -1, false, errs.Emit() + return NothingEnactable, -1, -1, errs.Emit() case "q", "quit": confirmQuit() default: @@ -423,19 +454,35 @@ func actionsMode[C StatsCollection](p *Player[C], canAct bool) (isCard bool, car } else if v < 1 || v > max { fmt.Println("That's not a card or action.") wait() - } else if v <= pOff { + } else if v <= dOff { 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 { optIdx, err := promptCard(p, p.PermanentActions[v]) errs.Add(err) if optIdx >= 0 || IsSeriousError(err) { - return false, v, optIdx, true, errs.Emit() + return PermanentActionEnactable, v, optIdx, errs.Emit() } } else { _, _, err := displayCard(p, p.PermanentActions[v], false) errs.Add(err) if IsSeriousError(err) { - return false, -1, -1, true, errs.Emit() + return PermanentActionEnactable, -1, -1, errs.Emit() } wait() } @@ -445,13 +492,13 @@ func actionsMode[C StatsCollection](p *Player[C], canAct bool) (isCard bool, car optIdx, err := promptCard(p, p.Hand[v]) errs.Add(err) if optIdx >= 0 || IsSeriousError(err) { - return true, v, optIdx, false, errs.Emit() + return CardEnactable, v, optIdx, errs.Emit() } } else { _, _, err := displayCard(p, p.Hand[v], false) errs.Add(err) if IsSeriousError(err) { - return false, -1, -1, false, errs.Emit() + return CardEnactable, -1, -1, errs.Emit() } 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 { var errs ErrorCollector for { - actionsOffset, handOffset, max := displayMainMenu(p) + debugOffset, actionsOffset, handOffset, max := displayMainMenu(p) divider() 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) @@ -485,7 +532,7 @@ func review[C StatsCollection](p *Player[C]) error { return errs.Emit() } case "a", "act", "actions": - _, _, _, _, err := actionsMode(p, false) + _, _, _, err := actionsMode(p, false) errs.Add(err) if IsSeriousError(err) { return errs.Emit() @@ -498,14 +545,18 @@ func review[C StatsCollection](p *Player[C]) error { i, err := strconv.Atoi(input) if err != nil { fmt.Println("Sorry, I don't understand.") - wait() } 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.") - wait() - } else if i <= actionsOffset { + } else if i <= debugOffset { cls() 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 { i = i - actionsOffset - 1 _, _, err := displayCard(p, p.PermanentActions[i], false) @@ -513,7 +564,6 @@ func review[C StatsCollection](p *Player[C]) error { if IsSeriousError(err) { return errs.Emit() } - wait() } else { i = i - handOffset - 1 _, _, err := displayCard(p, p.Hand[i], false) @@ -521,8 +571,8 @@ func review[C StatsCollection](p *Player[C]) error { if IsSeriousError(err) { return errs.Emit() } - wait() } + wait() } } }