package cardsim import ( "fmt" "os" "strconv" "strings" ) func RunSimpleTerminalUI[C StatsCollection](p *Player[C]) error { for { err := p.StartNextTurn() if p.DebugLevel < 1 && IsSeriousError(err) { return err } p.ReportError(err) 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() } review(p) err = p.Simulate() if p.DebugLevel < 1 && IsSeriousError(err) { return err } if p.DebugLevel < 1 && p.State.Over() { return nil } } return nil } func display(m Message) { if m == nil { return } fmt.Println(m.String()) } func wait() { fmt.Println() fmt.Println("") fmt.Scanln() } func pickNextAction[C StatsCollection](p *Player[C]) (isCard bool, cardIdx int, choiceIdx int, err error) { for { 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) divider() fmt.Printf("Show just (M)essages, (I)nfo Panels, (A)ctions, make a choice (1-%d), or (Q)uit? > ", max+1) input := getResponse() switch input { // Special cases case "m", "msg", "message", "messages": cls() displayMessageSection(p) wait() case "s", "stat", "stats", "i", "info", "p", "panel", "panels", "infopanel", "infopanels": statsMode(p) case "a", "act", "actions": actionsMode(p) case "q", "quit", "exit": confirmQuit() default: i, err := strconv.Atoi(input) if err != nil { fmt.Println("Sorry, I don't understand.") wait() return pickNextAction(p) } if i > max { fmt.Println("That's not a valid action.") wait() return pickNextAction(p) } i -= 1 if i < actionsOffset { cls() displayOnePanel(p, p.InfoPanels[i]) wait() } else if i < handOffset { cls() i -= actionsOffset option, promptErr := promptCard(p, p.PermanentActions[i]) if option >= 0 || IsSeriousError(promptErr) { return false, i, option, promptErr } } else { cls() i -= handOffset 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(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 } display(MultiMessage(p.TemporaryMessages)) 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) 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)+1) } 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]) ([]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() lockout := false if !urgent && p.HasUrgentCards() { fmt.Println("") fmt.Println() lockout = true } 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 !lockout && opt.Enabled(p) { pfx = fmt.Sprintf("[%2d]:", i+1) valid = true } 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)? > ") 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. } }