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 } if err != nil { display(ErrorMessage(err)) display(MsgStr("")) } 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("") 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], DebugActionEnactable) 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], PermanentActionEnactable) if option >= 0 || IsSeriousError(promptErr) { return PermanentActionEnactable, i, option, promptErr } } else { i = i - handOffset - 1 option, promptErr := promptCard(p, p.Hand[i], CardEnactable) 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], cardType EnactableType) (optionIdx int, err error) { // Iterate until the player makes a valid choice. for { opts, valid, err := displayCard(p, card, cardType, 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], cardType EnactableType, canAct bool) ([]CardOption[C], bool, error) { cls() t := card.Title(p).String() urgent := card.Urgent(p) if urgent && cardType == CardEnactable { 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 && cardType != DebugActionEnactable && p.HasUrgentCards() { fmt.Println("") 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], DebugActionEnactable) errs.Add(err) if optIdx >= 0 || IsSeriousError(err) { return DebugActionEnactable, v, optIdx, errs.Emit() } } else { _, _, err := displayCard(p, p.DebugActions[v], DebugActionEnactable, 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], 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() } } else { v = v - pOff - 1 if canAct { optIdx, err := promptCard(p, p.Hand[v], CardEnactable) errs.Add(err) if optIdx >= 0 || IsSeriousError(err) { return CardEnactable, v, optIdx, errs.Emit() } } else { _, _, err := displayCard(p, p.Hand[v], CardEnactable, 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], DebugActionEnactable, false) errs.Add(err) if IsSeriousError(err) { return errs.Emit() } } else if i <= handOffset { i = i - actionsOffset - 1 _, _, err := displayCard(p, p.PermanentActions[i], PermanentActionEnactable, false) errs.Add(err) if IsSeriousError(err) { return errs.Emit() } } else { i = i - handOffset - 1 _, _, err := displayCard(p, p.Hand[i], CardEnactable, false) errs.Add(err) if IsSeriousError(err) { return errs.Emit() } } wait() } } }