Kistaro Windrider
54711b36a8
Includes some refactoring work to pull out common code and express the idea of "wait, which panel, exactly?".
579 lines
14 KiB
Go
579 lines
14 KiB
Go
package cardsim
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
func RunSimpleTerminalUI[C StatsCollection](p *Player[C]) error {
|
|
|
|
err := p.StartNextTurn()
|
|
if p.DebugLevel < 1 && IsSeriousError(err) {
|
|
return err
|
|
}
|
|
p.ReportError(err)
|
|
|
|
for {
|
|
for p.CanAct() {
|
|
actionType, cardIdx, choiceIdx, err := pickNextAction(p)
|
|
p.ReportError(err)
|
|
if IsSeriousError(err) {
|
|
if p.DebugLevel < 1 {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
var msg Message
|
|
switch actionType {
|
|
case CardEnactable:
|
|
msg, err = p.EnactCard(cardIdx, choiceIdx)
|
|
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) {
|
|
if p.DebugLevel < 1 {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
display(msg)
|
|
wait()
|
|
}
|
|
|
|
// Allow player to review state before continuing simulation.
|
|
// Errors from review mode are reported *after* the simulation
|
|
// step because the first thing Simulate does is throw out old
|
|
// messages -- like these errors.
|
|
reviewErr := review(p)
|
|
if p.DebugLevel < 1 && IsSeriousError(err) {
|
|
return reviewErr
|
|
}
|
|
err = p.Simulate()
|
|
if p.DebugLevel < 1 && IsSeriousError(err) {
|
|
return err
|
|
}
|
|
// Simulation errors are already in messages; now add the review error.
|
|
p.ReportError(reviewErr)
|
|
|
|
if p.DebugLevel < 1 && p.State.Over() {
|
|
return nil
|
|
}
|
|
}
|
|
// loop forever until the game ends or the player quits
|
|
}
|
|
|
|
func display(m Message) {
|
|
if m == nil {
|
|
return
|
|
}
|
|
fmt.Println(m.String())
|
|
}
|
|
|
|
func wait() {
|
|
fmt.Println()
|
|
fmt.Println("<press ENTER to continue>")
|
|
fmt.Scanln()
|
|
}
|
|
|
|
func displayMainMenu[C StatsCollection](p *Player[C]) (debugOffset, actionsOffset, handOffset, max int) {
|
|
cls()
|
|
needsDivider := displayMessageSection(p)
|
|
if needsDivider {
|
|
divider()
|
|
}
|
|
displayOnePanel(p, p.Prompt)
|
|
divider()
|
|
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()
|
|
}
|
|
max = displayHandMenu(p, handOffset)
|
|
return // uses named return values
|
|
}
|
|
|
|
func pickNextAction[C StatsCollection](p *Player[C]) (actionType EnactableType, cardIdx int, choiceIdx int, err error) {
|
|
for {
|
|
debugOffset, actionsOffset, handOffset, max := displayMainMenu(p)
|
|
|
|
divider()
|
|
fmt.Printf("%d actions remaining.\n", p.ActionsRemaining)
|
|
fmt.Printf("Show just (M)essages, (I)nfo Panels, (A)ctions, or consider an item (1-%d), or (Q)uit? > ", max)
|
|
input := getResponse()
|
|
switch input {
|
|
// Special cases
|
|
case "m", "msg", "message", "messages":
|
|
cls()
|
|
if displayMessageSection(p) {
|
|
divider()
|
|
}
|
|
displayOnePanel(p, p.Prompt)
|
|
wait()
|
|
case "s", "stat", "stats", "i", "info", "p", "panel", "panels", "infopanel", "infopanels":
|
|
statsMode(p)
|
|
case "a", "act", "actions":
|
|
actionType, cardIdx, choiceIdx, err = actionsMode(p, true)
|
|
if actionType != NothingEnactable {
|
|
return
|
|
}
|
|
case "q", "quit", "exit":
|
|
confirmQuit()
|
|
default:
|
|
i, err := strconv.Atoi(input)
|
|
if err != nil {
|
|
fmt.Println("Sorry, I don't understand.")
|
|
wait()
|
|
} 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 <= 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 PermanentActionEnactable, i, option, promptErr
|
|
}
|
|
} else {
|
|
i = i - handOffset - 1
|
|
option, promptErr := promptCard(p, p.Hand[i])
|
|
if option >= 0 || IsSeriousError(promptErr) {
|
|
return CardEnactable, i, option, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func cls() {
|
|
fmt.Println("\033[H\033[2J")
|
|
}
|
|
|
|
func getResponse() string {
|
|
var input string
|
|
fmt.Scanln(&input)
|
|
input = strings.TrimSpace(input)
|
|
input = strings.ToLower(input)
|
|
return input
|
|
}
|
|
|
|
func divider() {
|
|
fmt.Println()
|
|
fmt.Println(ChapterBreak.String())
|
|
fmt.Println()
|
|
}
|
|
|
|
func lightDivider() {
|
|
fmt.Println()
|
|
fmt.Println(SectionBreak.String())
|
|
fmt.Println()
|
|
}
|
|
|
|
func confirmQuit() {
|
|
divider()
|
|
fmt.Println("Are you sure you want to quit? (Y/N) > ")
|
|
s := getResponse()
|
|
if s == "y" || s == "yes" {
|
|
fmt.Println("Bye!")
|
|
os.Exit(0)
|
|
}
|
|
}
|
|
|
|
func displayOnePanel[C StatsCollection](p *Player[C], panel InfoPanel[C]) error {
|
|
ts := panel.Title(p).String()
|
|
if len(ts) > 0 {
|
|
fmt.Println(ts)
|
|
fmt.Println(strings.Repeat("-", len(ts)))
|
|
fmt.Println()
|
|
}
|
|
m, err := panel.Info(p)
|
|
if IsSeriousError(err) {
|
|
return err
|
|
}
|
|
display(MultiMessage(m))
|
|
return err
|
|
}
|
|
|
|
func displayMessageSection[C StatsCollection](p *Player[C]) bool {
|
|
if len(p.TemporaryMessages) == 0 {
|
|
return false
|
|
}
|
|
for _, m := range p.TemporaryMessages {
|
|
if m != nil {
|
|
display(m)
|
|
} else {
|
|
fmt.Println()
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func displayStatsMenu[C StatsCollection](p *Player[C]) int {
|
|
if len(p.InfoPanels) == 0 {
|
|
return 0
|
|
}
|
|
fmt.Println("Info Panels")
|
|
fmt.Println("-----------")
|
|
displayNumberedTitles(p, p.InfoPanels, 0)
|
|
return len(p.InfoPanels)
|
|
}
|
|
|
|
func displayPermanentActionsMenu[C StatsCollection](p *Player[C], offset int) int {
|
|
if len(p.PermanentActions) == 0 {
|
|
return offset
|
|
}
|
|
fmt.Println("Always Available")
|
|
fmt.Println("----------------")
|
|
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("----")
|
|
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))
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
func promptCard[C StatsCollection](p *Player[C], card Card[C]) (optionIdx int, err error) {
|
|
// Iterate until the player makes a valid choice.
|
|
for {
|
|
opts, valid, err := displayCard(p, card, true)
|
|
if IsSeriousError(err) {
|
|
return -1, err
|
|
}
|
|
fmt.Println()
|
|
if valid {
|
|
fmt.Printf("Go (B)ack, (Q)uit, or enact a choice (1 - %d)? > ", len(opts))
|
|
} else {
|
|
fmt.Print("Go (B)ack or (Q)uit? > ")
|
|
}
|
|
read := getResponse()
|
|
switch read {
|
|
case "b", "back":
|
|
return -1, err
|
|
case "q", "quit":
|
|
confirmQuit()
|
|
default:
|
|
i, convErr := strconv.Atoi(read)
|
|
if convErr != nil {
|
|
fmt.Println("Sorry, I don't understand.")
|
|
wait()
|
|
} else if !valid {
|
|
fmt.Println("You can't enact anything here.")
|
|
wait()
|
|
} else if i <= 0 || i > len(opts) {
|
|
fmt.Println("That's not one of the options.")
|
|
wait()
|
|
} else if !opts[i-1].Enabled(p) {
|
|
fmt.Println("That option is not available to you right now.")
|
|
wait()
|
|
} else {
|
|
return i - 1, err
|
|
}
|
|
}
|
|
// Invalid selection made -- loop to prompt again.
|
|
}
|
|
}
|
|
|
|
func displayCard[C StatsCollection](p *Player[C], card Card[C], canAct bool) ([]CardOption[C], bool, error) {
|
|
cls()
|
|
t := card.Title(p).String()
|
|
urgent := card.Urgent(p)
|
|
if urgent {
|
|
t = "[URGENT!] " + t
|
|
}
|
|
fmt.Println(t)
|
|
fmt.Println(strings.Repeat("-", len(t)))
|
|
fmt.Println()
|
|
event, err := card.EventText(p)
|
|
if IsSeriousError(err) {
|
|
return nil, false, err
|
|
}
|
|
var errs ErrorCollector
|
|
errs.Add(err)
|
|
fmt.Println(event.String())
|
|
fmt.Println()
|
|
fmt.Println(SectionBreak.String())
|
|
fmt.Println()
|
|
if !urgent && p.HasUrgentCards() {
|
|
fmt.Println("<You have more urgent matters to attend to! You cannot act on this right now.>")
|
|
fmt.Println()
|
|
canAct = false
|
|
}
|
|
opts, optErr := card.Options(p)
|
|
errs.Add(optErr)
|
|
if IsSeriousError(optErr) {
|
|
return nil, false, errs.Emit()
|
|
}
|
|
valid := false
|
|
for i, opt := range opts {
|
|
pfx := "[xx]:"
|
|
if opt.Enabled(p) {
|
|
if canAct {
|
|
pfx = fmt.Sprintf("[%2d]:", i+1)
|
|
valid = true
|
|
} else {
|
|
pfx = "[--]:"
|
|
}
|
|
}
|
|
t, err := opt.OptionText(p)
|
|
errs.Add(err)
|
|
if IsSeriousError(err) {
|
|
return nil, false, errs.Emit()
|
|
}
|
|
fmt.Println(pfx, t.String())
|
|
}
|
|
return opts, valid, errs.Emit()
|
|
}
|
|
|
|
func statsMode[C StatsCollection](p *Player[C]) error {
|
|
var errs ErrorCollector
|
|
for {
|
|
cls()
|
|
n := displayStatsMenu(p)
|
|
if n <= 0 {
|
|
fmt.Println("No info panels are available.")
|
|
wait()
|
|
return errs.Emit()
|
|
}
|
|
fmt.Println()
|
|
fmt.Printf("Go (B)ack, (Q)uit, or view an info panel (1-%d)? > ", n)
|
|
s := getResponse()
|
|
switch s {
|
|
case "b", "back":
|
|
return errs.Emit()
|
|
case "q", "quit":
|
|
confirmQuit()
|
|
default:
|
|
v, err := strconv.Atoi(s)
|
|
if err != nil {
|
|
fmt.Println("Sorry, I don't understand.")
|
|
wait()
|
|
} else if v <= 0 || v > n {
|
|
fmt.Println("There's no info panel with that index.")
|
|
} else {
|
|
cls()
|
|
err := displayOnePanel(p, p.InfoPanels[v-1])
|
|
errs.Add(err)
|
|
if IsSeriousError(err) {
|
|
return errs.Emit()
|
|
}
|
|
wait()
|
|
}
|
|
}
|
|
// Loop to re-display info panels menu.
|
|
}
|
|
}
|
|
|
|
func actionsMode[C StatsCollection](p *Player[C], canAct bool) (actionType EnactableType, cardIdx, choiceIdx int, err error) {
|
|
var errs ErrorCollector
|
|
for {
|
|
cls()
|
|
dOff := displayDebugActionsMenu(p, 0)
|
|
if dOff > 0 {
|
|
fmt.Println()
|
|
}
|
|
pOff := displayPermanentActionsMenu(p, dOff)
|
|
if pOff > dOff {
|
|
fmt.Println()
|
|
}
|
|
max := displayHandMenu(p, pOff)
|
|
|
|
if max <= 0 {
|
|
fmt.Println("There are no actions available and no cards in hand.")
|
|
fmt.Println("That's a problem. The game is stuck.")
|
|
confirmQuit()
|
|
errs.Add(WarningStalemate)
|
|
return NothingEnactable, -1, -1, errs.Emit()
|
|
}
|
|
|
|
fmt.Println()
|
|
if canAct {
|
|
fmt.Printf("Go (B)ack, (Q)uit, or consider an action (1-%d)? > ", max)
|
|
} else {
|
|
fmt.Printf("Go (B)ack, (Q)uit, or view an action (1-%d)? > ", max)
|
|
}
|
|
input := getResponse()
|
|
switch input {
|
|
case "b", "back":
|
|
return NothingEnactable, -1, -1, errs.Emit()
|
|
case "q", "quit":
|
|
confirmQuit()
|
|
default:
|
|
v, err := strconv.Atoi(input)
|
|
if err != nil {
|
|
fmt.Println("Sorry, I don't understand.")
|
|
wait()
|
|
} else if v < 1 || v > max {
|
|
fmt.Println("That's not a card or action.")
|
|
wait()
|
|
} 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 PermanentActionEnactable, v, optIdx, errs.Emit()
|
|
}
|
|
} else {
|
|
_, _, err := displayCard(p, p.PermanentActions[v], false)
|
|
errs.Add(err)
|
|
if IsSeriousError(err) {
|
|
return PermanentActionEnactable, -1, -1, errs.Emit()
|
|
}
|
|
wait()
|
|
}
|
|
} else {
|
|
v = v - pOff - 1
|
|
if canAct {
|
|
optIdx, err := promptCard(p, p.Hand[v])
|
|
errs.Add(err)
|
|
if optIdx >= 0 || IsSeriousError(err) {
|
|
return CardEnactable, v, optIdx, errs.Emit()
|
|
}
|
|
} else {
|
|
_, _, err := displayCard(p, p.Hand[v], false)
|
|
errs.Add(err)
|
|
if IsSeriousError(err) {
|
|
return CardEnactable, -1, -1, errs.Emit()
|
|
}
|
|
wait()
|
|
}
|
|
}
|
|
}
|
|
// Re-prompt to get a valid choice.
|
|
}
|
|
}
|
|
|
|
func review[C StatsCollection](p *Player[C]) error {
|
|
var errs ErrorCollector
|
|
for {
|
|
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)
|
|
input := getResponse()
|
|
switch input {
|
|
// Special cases
|
|
case "m", "msg", "message", "messages":
|
|
cls()
|
|
if displayMessageSection(p) {
|
|
divider()
|
|
}
|
|
displayOnePanel(p, p.Prompt)
|
|
wait()
|
|
case "s", "stat", "stats", "i", "info", "p", "panel", "panels", "infopanel", "infopanels":
|
|
err := statsMode(p)
|
|
errs.Add(err)
|
|
if IsSeriousError(err) {
|
|
return errs.Emit()
|
|
}
|
|
case "a", "act", "actions":
|
|
_, _, _, err := actionsMode(p, false)
|
|
errs.Add(err)
|
|
if IsSeriousError(err) {
|
|
return errs.Emit()
|
|
}
|
|
case "q", "quit", "exit":
|
|
confirmQuit()
|
|
case "c", "continue", "ok":
|
|
return errs.Emit()
|
|
default:
|
|
i, err := strconv.Atoi(input)
|
|
if err != nil {
|
|
fmt.Println("Sorry, I don't understand.")
|
|
} 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.")
|
|
} else if i <= debugOffset {
|
|
cls()
|
|
displayOnePanel(p, p.InfoPanels[i-1])
|
|
} 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)
|
|
errs.Add(err)
|
|
if IsSeriousError(err) {
|
|
return errs.Emit()
|
|
}
|
|
} else {
|
|
i = i - handOffset - 1
|
|
_, _, err := displayCard(p, p.Hand[i], false)
|
|
errs.Add(err)
|
|
if IsSeriousError(err) {
|
|
return errs.Emit()
|
|
}
|
|
}
|
|
wait()
|
|
}
|
|
}
|
|
}
|