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() { isCard, cardIdx, choiceIdx, err := pickNextAction(p) p.ReportError(err) if IsSeriousError(err) { if p.DebugLevel < 1 { return err } continue } var msg Message if isCard { msg, err = p.EnactCard(cardIdx, choiceIdx) } else { msg, err = p.EnactPermanentAction(cardIdx, choiceIdx) } 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("") fmt.Scanln() } func displayMainMenu[C StatsCollection](p *Player[C]) (actionsOffset, handOffset, max int) { cls() needsDivider := displayMessageSection(p) if needsDivider { divider() } displayOnePanel(p, p.Prompt) divider() actionsOffset = displayStatsMenu(p) if actionsOffset > 0 { divider() } 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]) (isCard bool, cardIdx int, choiceIdx int, err error) { for { 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": var committed bool isCard, cardIdx, choiceIdx, committed, err = actionsMode(p, true) if committed { 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 <= actionsOffset { cls() displayOnePanel(p, p.InfoPanels[i-1]) wait() } 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 } } else { i = i - handOffset - 1 option, promptErr := promptCard(p, p.Hand[i]) if option >= 0 || IsSeriousError(promptErr) { return true, 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 } hasPrevious := false for _, m := range p.TemporaryMessages { if m != nil { if hasPrevious { lightDivider() } display(m) hasPrevious = true } } return true } func displayStatsMenu[C StatsCollection](p *Player[C]) int { if len(p.InfoPanels) == 0 { return 0 } fmt.Println("Info Panels") fmt.Println("-----------") for i, s := range p.InfoPanels { fmt.Printf("[%2d]: %s\n", i+1, s.Title(p).String()) } 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("----------------") for i, s := range p.PermanentActions { fmt.Printf("[%2d]: %s\n", i+offset+1, s.Title(p)) } return offset + len(p.PermanentActions) } 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 { 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 // 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("") 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 { 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) (isCard bool, cardIdx, choiceIdx int, committed bool, err error) { var errs ErrorCollector for { cls() pOff := displayPermanentActionsMenu(p, 0) if pOff > 0 { 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 false, -1, -1, true, 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 false, -1, -1, false, 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 <= pOff { v-- if canAct { optIdx, err := promptCard(p, p.PermanentActions[v]) errs.Add(err) if optIdx >= 0 || IsSeriousError(err) { return false, v, optIdx, true, errs.Emit() } } else { _, _, err := displayCard(p, p.PermanentActions[v], false) errs.Add(err) if IsSeriousError(err) { return false, -1, -1, true, 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 true, v, optIdx, false, errs.Emit() } } else { _, _, err := displayCard(p, p.Hand[v], false) errs.Add(err) if IsSeriousError(err) { return false, -1, -1, false, errs.Emit() } wait() } } } // Re-prompt to get a valid choice. } } func review[C StatsCollection](p *Player[C]) error { var errs ErrorCollector for { 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.") 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 { cls() displayOnePanel(p, p.InfoPanels[i-1]) wait() } else if i <= handOffset { i = i - actionsOffset - 1 _, _, err := displayCard(p, p.PermanentActions[i], false) errs.Add(err) if IsSeriousError(err) { return errs.Emit() } wait() } else { i = i - handOffset - 1 _, _, err := displayCard(p, p.Hand[i], false) errs.Add(err) if IsSeriousError(err) { return errs.Emit() } wait() } } } }