From 2875dc5af87a5050fffbea3129f19bf72aede177 Mon Sep 17 00:00:00 2001 From: Kistaro Windrider Date: Sun, 2 Apr 2023 13:58:56 -0700 Subject: [PATCH] Implement review mode. This finishes the UI. --- cardsim/player.go | 2 +- cardsim/terminalui.go | 199 +++++++++++++++++++++++++++++++++--------- 2 files changed, 161 insertions(+), 40 deletions(-) diff --git a/cardsim/player.go b/cardsim/player.go index a5a15e9..6576554 100644 --- a/cardsim/player.go +++ b/cardsim/player.go @@ -227,7 +227,7 @@ func (p *Player[C]) Simulate() error { if p.DebugLevel > 0 && !errs.IsEmpty() { p.ChapterBreak() - p.TemporaryMessages = append(p.TemporaryMessages, Msgf("%d ERRORS AND WARNINGS:", len(errs.Errs))) + p.TemporaryMessages = append(p.TemporaryMessages, Msgf("%d ERRORS AND WARNINGS while simulating turn:", len(errs.Errs))) for i, e := range errs.Errs { yikes := " " if IsSeriousError(e) { diff --git a/cardsim/terminalui.go b/cardsim/terminalui.go index aa01686..b6dc9b2 100644 --- a/cardsim/terminalui.go +++ b/cardsim/terminalui.go @@ -41,17 +41,26 @@ func RunSimpleTerminalUI[C StatsCollection](p *Player[C]) error { wait() } - review(p) + // 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 } } - return nil + // loop forever until the game ends or the player quits } func display(m Message) { @@ -67,27 +76,33 @@ func wait() { 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 { - 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) + actionsOffset, handOffset, max := displayMainMenu(p) divider() - fmt.Printf("Show just (M)essages, (I)nfo Panels, (A)ctions, or consider an action (1-%d), or (Q)uit? > ", max) + 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 @@ -102,7 +117,7 @@ func pickNextAction[C StatsCollection](p *Player[C]) (isCard bool, cardIdx int, statsMode(p) case "a", "act", "actions": var committed bool - isCard, cardIdx, choiceIdx, committed, err = actionsMode(p) + isCard, cardIdx, choiceIdx, committed, err = actionsMode(p, true) if committed { return } @@ -150,6 +165,12 @@ func getResponse() string { } func divider() { + fmt.Println() + fmt.Println(ChapterBreak.String()) + fmt.Println() +} + +func lightDivider() { fmt.Println() fmt.Println(SectionBreak.String()) fmt.Println() @@ -184,7 +205,16 @@ func displayMessageSection[C StatsCollection](p *Player[C]) bool { if len(p.TemporaryMessages) == 0 { return false } - display(MultiMessage(p.TemporaryMessages)) + hasPrevious := false + for _, m := range p.TemporaryMessages { + if m != nil { + if hasPrevious { + lightDivider() + } + display(m) + hasPrevious = true + } + } return true } @@ -229,7 +259,7 @@ func displayHandMenu[C StatsCollection](p *Player[C], offset int) int { 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) + opts, valid, err := displayCard(p, card, true) if IsSeriousError(err) { return -1, err } @@ -267,7 +297,7 @@ func promptCard[C StatsCollection](p *Player[C], card Card[C]) (optionIdx int, e } } -func displayCard[C StatsCollection](p *Player[C], card Card[C]) ([]CardOption[C], bool, error) { +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) @@ -287,11 +317,10 @@ func displayCard[C StatsCollection](p *Player[C], card Card[C]) ([]CardOption[C] fmt.Println() fmt.Println(SectionBreak.String()) fmt.Println() - lockout := false if !urgent && p.HasUrgentCards() { fmt.Println("") fmt.Println() - lockout = true + canAct = false } opts, optErr := card.Options(p) errs.Add(optErr) @@ -301,9 +330,13 @@ func displayCard[C StatsCollection](p *Player[C], card Card[C]) ([]CardOption[C] valid := false for i, opt := range opts { pfx := "[xx]:" - if !lockout && opt.Enabled(p) { - pfx = fmt.Sprintf("[%2d]:", i+1) - valid = true + if opt.Enabled(p) { + if canAct { + pfx = fmt.Sprintf("[%2d]:", i+1) + valid = true + } else { + pfx = "[--]:" + } } t, err := opt.OptionText(p) errs.Add(err) @@ -326,7 +359,7 @@ func statsMode[C StatsCollection](p *Player[C]) error { return errs.Emit() } fmt.Println() - fmt.Printf("Go (B)ack, (Q)uit, or view an info panel (1-%d)? > ") + fmt.Printf("Go (B)ack, (Q)uit, or view an info panel (1-%d)? > ", n) s := getResponse() switch s { case "b", "back": @@ -353,7 +386,7 @@ func statsMode[C StatsCollection](p *Player[C]) error { } } -func actionsMode[C StatsCollection](p *Player[C]) (isCard bool, cardIdx, choiceIdx int, committed bool, err error) { +func actionsMode[C StatsCollection](p *Player[C], canAct bool) (isCard bool, cardIdx, choiceIdx int, committed bool, err error) { var errs ErrorCollector for { cls() @@ -372,7 +405,11 @@ func actionsMode[C StatsCollection](p *Player[C]) (isCard bool, cardIdx, choiceI } fmt.Println() - fmt.Printf("Go (B)ack, (Q)uit, or consider an action (1-%d)? > ", max) + 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": @@ -389,20 +426,104 @@ func actionsMode[C StatsCollection](p *Player[C]) (isCard bool, cardIdx, choiceI wait() } else if v <= pOff { v-- - optIdx, err := promptCard(p, p.PermanentActions[v]) - errs.Add(err) - if optIdx >= 0 || IsSeriousError(err) { - return false, v, optIdx, true, errs.Emit() + 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 - optIdx, err := promptCard(p, p.Hand[v]) - errs.Add(err) - if optIdx >= 0 || IsSeriousError(err) { - return true, v, optIdx, true, errs.Emit() + 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() + } + } + } +}