19 Commits

Author SHA1 Message Date
74a2493ef4 Clear screen before displaying panel in stats mode. 2023-04-02 19:37:55 -07:00
2c2e68ff93 Fix shadowing bug in rules engine. 2023-04-02 19:36:28 -07:00
d13e04e2f4 Debug utility function and comments 2023-04-02 19:35:07 -07:00
57348f7ebf Add a bunch of logging. 2023-04-02 19:33:44 -07:00
9796c2e970 First round of substantial bug fixes 2023-04-02 19:25:53 -07:00
0f21020647 More fencepost errors! 2023-04-02 19:08:26 -07:00
e96d81a7b4 Fencepost error!
Also the title is visible.
2023-04-02 19:07:05 -07:00
3a7bf9c2fb Don't adjust twice. 2023-04-02 19:05:15 -07:00
00ea284cbc Nil is not an error, much less a serious one. 2023-04-02 19:03:39 -07:00
159f6b6b5f Fix type. 2023-04-02 19:02:43 -07:00
2480a1631b Implement a very crude "game" as a test. Also updates Player. 2023-04-02 19:01:40 -07:00
2875dc5af8 Implement review mode.
This finishes the UI.
2023-04-02 13:58:56 -07:00
74ca51b21d actions mode, prompt cleanup
Fixes some off-by-one errors. A 1-indexed UI in a 0-indexed language is always going to be prone to those and I will no doubt find more when I have enough of a test program to experiment with...
2023-04-02 13:19:26 -07:00
5a2158f525 Implement Stats Mode.
Also rewords some prompts. Might as well be thorough in accepting reasonable inputs.
2023-04-02 12:54:52 -07:00
25a9eed3f0 Prompt for player choices on cards.
Also handles errors in the display/prompt logic somewhat better.
2023-04-02 12:44:29 -07:00
592c877852 More display components. 2023-04-02 00:43:17 -07:00
e1eac9de0f displayStatsMenu 2023-04-02 00:36:12 -07:00
7371cddab3 Add more dividers 2023-04-02 00:28:34 -07:00
aecd8683b2 Restructure loop, basic display functions 2023-04-02 00:26:34 -07:00
12 changed files with 825 additions and 111 deletions

View File

@ -5,9 +5,8 @@ package cardsim
type Card[C StatsCollection] interface { type Card[C StatsCollection] interface {
// Title is the short name of the card displayed in the hand // Title is the short name of the card displayed in the hand
// and at the top of the card output. It receives the current // and at the top of the card output. It receives the current
// player as an argument. If it returns an error that is not // player as an argument.
// a warning, the game crashes. Title(p *Player[C]) Message
Title(p *Player[C]) (Message, error)
// Urgent reports whether the card is considered urgent. If // Urgent reports whether the card is considered urgent. If
// the player has any urgent cards in hand, they cannot choose to act // the player has any urgent cards in hand, they cannot choose to act
@ -50,7 +49,8 @@ type CardOption[C StatsCollection] interface {
// a warning, the game crashes. // a warning, the game crashes.
// //
// After an option is enacted, the card is deleted. If a card should be // After an option is enacted, the card is deleted. If a card should be
// repeatable, Enact must return it to the deck (on every option). // repeatable, Enact must return it to the deck (on every option) or
// the card needs to reinsert itself with its Then function.
Enact(p *Player[C]) (Message, error) Enact(p *Player[C]) (Message, error)
// Enabled returns whether this option can curently be enacted. // Enabled returns whether this option can curently be enacted.
@ -64,12 +64,13 @@ type BasicCard[C StatsCollection] struct {
IsUrgent bool IsUrgent bool
CardText Message CardText Message
CardOptions []CardOption[C] CardOptions []CardOption[C]
AfterOption func(p *Player[C], option CardOption[C]) error // AfterOption is given the card itself as its first argument.
AfterOption func(c Card[C], p *Player[C], option CardOption[C]) error
} }
// Title implements Card. // Title implements Card.
func (b *BasicCard[C]) Title(_ *Player[C]) (Message, error) { func (b *BasicCard[C]) Title(_ *Player[C]) Message {
return b.CardTitle, nil return b.CardTitle
} }
// Urgent implements Card. // Urgent implements Card.
@ -92,7 +93,7 @@ func (b *BasicCard[C]) Then(p *Player[C], option CardOption[C]) error {
if b.AfterOption == nil { if b.AfterOption == nil {
return nil return nil
} }
return b.AfterOption(p, option) return b.AfterOption(b, p, option)
} }
// Drawn implements Card. // Drawn implements Card.

View File

@ -55,6 +55,9 @@ func Warningf(f string, args ...any) *Warning {
// IsSeriousError returns whether e is a non-warning error. If e is nil, this // IsSeriousError returns whether e is a non-warning error. If e is nil, this
// returns false. // returns false.
func IsSeriousError(e error) bool { func IsSeriousError(e error) bool {
if e == nil {
return false
}
return !errors.Is(e, AnyWarning) return !errors.Is(e, AnyWarning)
} }

View File

@ -11,7 +11,7 @@ import (
type InfoPanel[C StatsCollection] interface { type InfoPanel[C StatsCollection] interface {
// Title returns the title of this InfoPanel, which is also used as the // Title returns the title of this InfoPanel, which is also used as the
// label presented to the player to access this panel. // label presented to the player to access this panel.
Title(p *Player[C]) (Message, error) Title(p *Player[C]) Message
// Info returns the displayable contents of this InfoPanel. A nil Message // Info returns the displayable contents of this InfoPanel. A nil Message
// in the output is interpreted as a paragraph break. // in the output is interpreted as a paragraph break.
@ -44,8 +44,8 @@ func VisibleOrDebug[C StatsCollection](p *Player[C], s Stat) bool {
} }
// Title implements `InfoPanel[C]` by returning b's `Name`. // Title implements `InfoPanel[C]` by returning b's `Name`.
func (b *BasicStatsPanel[C]) Title(p *Player[C]) (Message, error) { func (b *BasicStatsPanel[C]) Title(p *Player[C]) Message {
return b.Name, nil return b.Name
} }
// Info implements `InfoPanel[C]` by dumpiing p.Stats, showing those items for // Info implements `InfoPanel[C]` by dumpiing p.Stats, showing those items for

View File

@ -227,7 +227,7 @@ func (p *Player[C]) Simulate() error {
if p.DebugLevel > 0 && !errs.IsEmpty() { if p.DebugLevel > 0 && !errs.IsEmpty() {
p.ChapterBreak() 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 { for i, e := range errs.Errs {
yikes := " " yikes := " "
if IsSeriousError(e) { if IsSeriousError(e) {
@ -260,18 +260,34 @@ func (p *Player[C]) StartNextTurn() error {
return errs.Emit() return errs.Emit()
} }
// Draw draws a card into the hand, informing the card that it has been drawn.
// If more than a million cards refuse to enter the hand, this crashes with
// ErrUncooperativeCards. If the deck does not have enough cards, this
// returns WarningTooFewCards.
func (p *Player[C]) Draw() error {
for attempts := 0; attempts < 1000000; attempts++ {
if p.Deck.Len() == 0 {
return WarningTooFewCards
}
c := p.Deck.Draw()
if c.Drawn(p) {
p.Hand = append(p.Hand, c)
return nil
}
}
return ErrUncooperativeCards
}
// FillHand draws up to the hand limit, informing cards that they have been // FillHand draws up to the hand limit, informing cards that they have been
// drawn. If more than a million cards refuse to enter the hand, this crashes // drawn. If more than a million cards refuse to enter the hand, this crashes
// with ErrUncooperativeCards. If the deck does not have enough cards, this // with ErrUncooperativeCards. If the deck does not have enough cards, this
// returns WarningTooFewCards. // returns WarningTooFewCards.
func (p *Player[C]) FillHand() error { func (p *Player[C]) FillHand() error {
failureLimit := 1000000 var lastErr error
for failureLimit > 0 && p.Deck.Len() > 0 && len(p.Hand) < p.HandLimit { for p.Deck.Len() > 0 && len(p.Hand) < p.HandLimit {
c := p.Deck.Draw() lastErr = p.Draw()
if c.Drawn(p) { if IsSeriousError(lastErr) {
p.Hand = append(p.Hand, c) return lastErr
} else {
failureLimit--
} }
} }
@ -279,10 +295,6 @@ func (p *Player[C]) FillHand() error {
return nil return nil
} }
if failureLimit <= 0 {
return ErrUncooperativeCards
}
return WarningTooFewCards return WarningTooFewCards
} }
@ -451,3 +463,20 @@ func (p *Player[C]) ReportError(e error) {
func (p *Player[C]) CanAct() bool { func (p *Player[C]) CanAct() bool {
return p.ActionsRemaining > 0 && (len(p.Hand) > 0 || len(p.PermanentActions) > 0) return p.ActionsRemaining > 0 && (len(p.Hand) > 0 || len(p.PermanentActions) > 0)
} }
// Debug adds a message to the player's temporary messages if their debug level
// is at least the level specified.
func (p *Player[C]) Debug(minLevel int, msg Message) {
if p.DebugLevel < minLevel || msg == nil {
return
}
p.TemporaryMessages = append(p.TemporaryMessages, msg)
}
// Emit adds a message to the player's temporary messages.
func (p *Player[C]) Emit(msg Message) {
if msg == nil {
return
}
p.TemporaryMessages = append(p.TemporaryMessages, msg)
}

View File

@ -141,7 +141,7 @@ func (r *RuleCollection[C]) performInsert(k *keyedRule[C]) {
r.rules[k.id] = k r.rules[k.id] = k
s := r.byStep[k.Step()] s := r.byStep[k.Step()]
if s == nil { if len(s) == 0 {
r.steps = nil r.steps = nil
} }
s = append(s, k.id) s = append(s, k.id)
@ -326,7 +326,7 @@ func (r *RuleCollection[C]) Run(p *Player[C]) error {
steps := r.steps steps := r.steps
if steps == nil { if steps == nil {
// Step set changed, recalculate. // Step set changed, recalculate.
steps := make([]int, 0, len(r.byStep)) steps = make([]int, 0, len(r.byStep))
for step := range r.byStep { for step := range r.byStep {
steps = append(steps, step) steps = append(steps, step)
} }
@ -334,18 +334,21 @@ func (r *RuleCollection[C]) Run(p *Player[C]) error {
r.steps = steps r.steps = steps
} }
p.Debug(2, Msgf("Executing steps: %v", steps))
var errs ErrorCollector var errs ErrorCollector
for _, step := range steps { for _, step := range steps {
stepRules := r.byStep[step] stepRules := r.byStep[step]
p.Rand.Shuffle(len(stepRules), func(i, j int) { p.Debug(3, Msgf("Executing step %d; length %d", step, len(stepRules)))
stepRules[i], stepRules[j] = stepRules[j], stepRules[i] ShuffleAll(stepRules, p.Rand)
})
var remove []RuleID var remove []RuleID
halt := false halt := false
for _, id := range stepRules { for _, id := range stepRules {
rule := r.rules[id] rule := r.rules[id]
p.Debug(4, Msgf("Executing rule %x (labeled %q)", id, rule.Label()))
err := rule.Enact(p) err := rule.Enact(p)
if err != nil { if err != nil {
p.Debug(2, Msgf("Rule %x (%q): error: %v", id, rule.Label(), err))
ignore := false ignore := false
if errors.Is(err, ErrDeleteRule) { if errors.Is(err, ErrDeleteRule) {
remove = append(remove, id) remove = append(remove, id)
@ -377,10 +380,14 @@ func (r *RuleCollection[C]) Run(p *Player[C]) error {
} }
} }
if halt { if halt {
return errs.Emit() ret := errs.Emit()
p.Debug(2, Msgf("Rules stopping early. Result: %v", ret))
return ret
} }
} }
return errs.Emit() ret := errs.Emit()
p.Debug(2, Msgf("Rules complete. Result: %v", ret))
return ret
} }
func (r *RuleCollection[C]) applyDelayedUpdates() { func (r *RuleCollection[C]) applyDelayedUpdates() {

View File

@ -8,15 +8,23 @@ import (
) )
func RunSimpleTerminalUI[C StatsCollection](p *Player[C]) error { func RunSimpleTerminalUI[C StatsCollection](p *Player[C]) error {
for {
err := p.StartNextTurn() err := p.StartNextTurn()
if p.DebugLevel < 1 && IsSeriousError(err) { if p.DebugLevel < 1 && IsSeriousError(err) {
return err return err
} }
p.ReportError(err) p.ReportError(err)
for {
for p.CanAct() { for p.CanAct() {
isCard, cardIdx, choiceIdx := pickNextAction(p) isCard, cardIdx, choiceIdx, err := pickNextAction(p)
p.ReportError(err)
if IsSeriousError(err) {
if p.DebugLevel < 1 {
return err
}
continue
}
var msg Message var msg Message
if isCard { if isCard {
msg, err = p.EnactCard(cardIdx, choiceIdx) msg, err = p.EnactCard(cardIdx, choiceIdx)
@ -24,31 +32,43 @@ func RunSimpleTerminalUI[C StatsCollection](p *Player[C]) error {
msg, err = p.EnactPermanentAction(cardIdx, choiceIdx) msg, err = p.EnactPermanentAction(cardIdx, choiceIdx)
} }
p.ReportError(err) p.ReportError(err)
if p.DebugLevel < 1 && IsSeriousError(err) { if IsSeriousError(err) {
if p.DebugLevel < 1 {
return err return err
} }
displayAndWait(msg) continue
}
display(msg)
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() err = p.Simulate()
if p.DebugLevel < 1 && IsSeriousError(err) { if p.DebugLevel < 1 && IsSeriousError(err) {
return err return err
} }
// Simulation errors are already in messages; now add the review error.
p.ReportError(reviewErr)
if p.DebugLevel < 1 && p.State.Over() { if p.DebugLevel < 1 && p.State.Over() {
return nil return nil
} }
} }
return nil // loop forever until the game ends or the player quits
} }
func displayAndWait(m Message) { func display(m Message) {
if m == nil { if m == nil {
return return
} }
fmt.Println(m.String()) fmt.Println(m.String())
wait()
} }
func wait() { func wait() {
@ -57,75 +77,101 @@ func wait() {
fmt.Scanln() fmt.Scanln()
} }
func pickNextAction[C StatsCollection](p *Player[C]) (isCard bool, cardIdx int, choiceIdx int) { func displayMainMenu[C StatsCollection](p *Player[C]) (actionsOffset, handOffset, max int) {
cls() cls()
needsDivider := displayMessageSection(p) needsDivider := displayMessageSection(p)
if needsDivider { if needsDivider {
divider() divider()
} }
displayOnePanel(p, p.Prompt) displayOnePanel(p, p.Prompt)
actionsOffset := displayStatsMenu(p) divider()
handOffset := displayPermanentActionsMenu(p, actionsOffset) actionsOffset = displayStatsMenu(p)
max := displayHandMenu(p, handOffset) 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() divider()
var input string fmt.Printf("%d actions remaining.\n", p.ActionsRemaining)
fmt.Printf("Show just (M)essages, (S)tats, (A)ctions, make a choice (1-%d), or (Q)uit? >", max+1) fmt.Printf("Show just (M)essages, (I)nfo Panels, (A)ctions, or consider an item (1-%d), or (Q)uit? > ", max)
fmt.Scanln(&input) input := getResponse()
input = strings.TrimSpace(input)
input = strings.ToLower(input)
switch input { switch input {
// Special cases // Special cases
case "m": case "m", "msg", "message", "messages":
cls() cls()
displayMessageSection(p) if displayMessageSection(p) {
divider()
}
displayOnePanel(p, p.Prompt)
wait() wait()
case "s": case "s", "stat", "stats", "i", "info", "p", "panel", "panels", "infopanel", "infopanels":
statsMode(p) statsMode(p)
case "a": case "a", "act", "actions":
actionsMode(p) var committed bool
case "q": isCard, cardIdx, choiceIdx, committed, err = actionsMode(p, true)
if committed {
return
}
case "q", "quit", "exit":
confirmQuit() confirmQuit()
default: default:
i, err := strconv.Atoi(input) i, err := strconv.Atoi(input)
if err != nil { if err != nil {
fmt.Println("Sorry, I don't understand.") fmt.Println("Sorry, I don't understand.")
wait() wait()
return pickNextAction(p) } else if i > max {
} fmt.Println("That's not on this menu. If the menu is too big to read, choose a detail view.")
if i > max {
fmt.Println("That's not a valid action.")
wait() wait()
return pickNextAction(p) } else if i <= actionsOffset {
}
i -= 1
if i < actionsOffset {
cls() cls()
DisplayOnePanelAndWait(p, i) displayOnePanel(p, p.InfoPanels[i-1])
} else if i < handOffset { wait()
cls() } else if i <= handOffset {
i -= actionsOffset i = i - actionsOffset - 1
option, ok := promptPermanentAction(p, i) option, promptErr := promptCard(p, p.PermanentActions[i])
if ok { if option >= 0 || IsSeriousError(promptErr) {
return false, i, option return false, i, option, promptErr
} }
} else { } else {
cls() i = i - handOffset - 1
i -= handOffset option, promptErr := promptCard(p, p.Hand[i])
option, ok := promptCard(p, i) if option >= 0 || IsSeriousError(promptErr) {
if ok { return true, i, option, nil
return true, i, option }
} }
} }
} }
return pickNextAction(p)
} }
func cls() { func cls() {
fmt.Println("\033[H\033[2J") 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() { func divider() {
fmt.Println()
fmt.Println(ChapterBreak.String())
fmt.Println()
}
func lightDivider() {
fmt.Println() fmt.Println()
fmt.Println(SectionBreak.String()) fmt.Println(SectionBreak.String())
fmt.Println() fmt.Println()
@ -133,35 +179,353 @@ func divider() {
func confirmQuit() { func confirmQuit() {
divider() divider()
fmt.Println("Are you sure you want to quit? (Y/N) ") fmt.Println("Are you sure you want to quit? (Y/N) > ")
var s string s := getResponse()
fmt.Scanln(&s) if s == "y" || s == "yes" {
s = strings.TrimSpace(s)
s = strings.ToLower(s)
if strings.HasPrefix(s, "y") {
fmt.Println("Bye!") fmt.Println("Bye!")
os.Exit(0) os.Exit(0)
} }
} }
func displayOnePanel[C StatsCollection](p *Player[C], panel InfoPanel[C]) error { func displayOnePanel[C StatsCollection](p *Player[C], panel InfoPanel[C]) error {
var errs ErrorCollector ts := panel.Title(p).String()
t, err := panel.Title(p)
if IsSeriousError(err) {
return err
}
errs.Add(err)
ts := t.String()
if len(ts) > 0 { if len(ts) > 0 {
fmt.Println(ts) fmt.Println(ts)
fmt.Println(strings.Repeat("-", len(ts))) fmt.Println(strings.Repeat("-", len(ts)))
fmt.Println() fmt.Println()
} }
m, err := panel.Info(p) 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("<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) errs.Add(err)
if IsSeriousError(err) { if IsSeriousError(err) {
return errs.Emit() return errs.Emit()
} }
displayAndWait(MultiMessage(m)) wait()
return errs.Emit() }
}
// 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()
}
}
}
} }

6
go.mod
View File

@ -1,3 +1,9 @@
module cardSimEngine module cardSimEngine
go 1.20 go 1.20
require (
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
)

8
go.sum Normal file
View File

@ -0,0 +1,8 @@
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=

162
smoketest/cards.go Normal file
View File

@ -0,0 +1,162 @@
package main
import (
"cardSimEngine/cardsim"
"errors"
"fmt"
)
// Type aliases, unlike distinctly named types, are fully substitutable for
// the original type. This trims off some annoying-to-type things.
type player = cardsim.Player[*SmokeTestCollection]
type card = cardsim.Card[*SmokeTestCollection]
type cardOption = cardsim.CardOption[*SmokeTestCollection]
func makeAdditionCard(amt int) cardsim.Card[*SmokeTestCollection] {
c := &cardsim.BasicCard[*SmokeTestCollection]{
CardTitle: cardsim.Msgf("Additive %d", amt),
CardText: cardsim.Msgf("You can change the Number by %d.", amt),
CardOptions: []cardsim.CardOption[*SmokeTestCollection]{
&cardsim.BasicOption[*SmokeTestCollection]{
Text: cardsim.Msgf("Add %d", amt),
Effect: func(p *player) error {
p.Stats.Number.Value += amt
return nil
},
Output: cardsim.MsgStr("Added."),
},
&cardsim.BasicOption[*SmokeTestCollection]{
Text: cardsim.Msgf("Subtract %d", amt),
Effect: func(p *player) error {
p.Stats.Number.Value -= amt
return nil
},
Output: cardsim.MsgStr("Subtracted."),
},
},
AfterOption: func(c card, p *player, _ cardOption) error {
p.Deck.InsertRandomBottom(0.5, c)
return nil
},
}
return c
}
func makeMultiplicationCard(amt int) cardsim.Card[*SmokeTestCollection] {
c := &cardsim.BasicCard[*SmokeTestCollection]{
CardTitle: cardsim.Msgf("Multiplicative %d", amt),
CardText: cardsim.Msgf("You can multiply or divide the Number by %d, or maybe divide the Number by that.", amt),
CardOptions: []cardsim.CardOption[*SmokeTestCollection]{
&cardsim.BasicOption[*SmokeTestCollection]{
Text: cardsim.Msgf("Multiply by %d", amt),
Effect: func(p *player) error {
p.Stats.Number.Value *= amt
return nil
},
Output: cardsim.MsgStr("Multiplied."),
},
&cardsim.BasicOption[*SmokeTestCollection]{
Text: cardsim.Msgf("Integer divide by %d", amt),
Effect: func(p *player) error {
p.Stats.Number.Value /= amt
return nil
},
Output: cardsim.MsgStr("Divided."),
},
inverseDivision(amt),
},
AfterOption: func(c card, p *player, _ cardOption) error {
p.Deck.InsertRandomBottom(0.5, c)
return nil
},
}
return c
}
type inverseDivision int
func (i inverseDivision) OptionText(p *player) (cardsim.Message, error) {
if p.Stats.Number.Value == 0 {
return cardsim.MsgStr("You can't divide by zero!"), nil
}
return cardsim.Msgf("Divide %d by the Number", int(i)), nil
}
func (i inverseDivision) Enact(p *player) (cardsim.Message, error) {
if p.Stats.Number.Value == 0 {
return nil, errors.New("you can't divide by zero!")
}
p.Stats.Number.Value = int(i) / p.Stats.Number.Value
return cardsim.MsgStr("Inverse divided."), nil
}
func (i inverseDivision) Enabled(p *player) bool {
return p.Stats.Number.Value != 0
}
func initDeck(d *cardsim.Deck[*SmokeTestCollection]) {
addMe := []int{
0, 1, 2, 5, 10, 50, 100, 1000, 2500, 500000, 9876543,
}
for _, n := range addMe {
d.Insert(cardsim.BottomOfDeck, makeAdditionCard(n))
}
multiplyMe := []int{
2, 4, 8, 16, 32, 64, 128, 512, 1024, 9999, 84720413,
}
for _, n := range multiplyMe {
d.Insert(cardsim.BottomOfDeck, makeMultiplicationCard(n))
}
if err := d.Shuffle(); cardsim.IsSeriousError(err) {
panic(err)
} else if err != nil {
fmt.Printf("Error shuffling: %v\n", err)
}
}
func installPermanentActions(pa *[]card) {
*pa = []card{
&cardsim.BasicCard[*SmokeTestCollection]{
CardTitle: cardsim.MsgStr("Reset to 0"),
CardText: cardsim.MsgStr("Resets Number to 0."),
CardOptions: []cardOption{
&cardsim.BasicOption[*SmokeTestCollection]{
Text: cardsim.MsgStr("Reset to 0."),
Effect: func(p *player) error {
p.Stats.Number.Value = 0
return nil
},
Output: cardsim.MsgStr("Done."),
},
},
},
&cardsim.BasicCard[*SmokeTestCollection]{
CardTitle: cardsim.MsgStr("Reset to 1000000"),
CardText: cardsim.MsgStr("Resets Number to one million."),
CardOptions: []cardOption{
&cardsim.BasicOption[*SmokeTestCollection]{
Text: cardsim.MsgStr("Reset to 1,000,000"),
Effect: func(p *player) error {
p.Stats.Number.Value = 1000000
return nil
},
Output: cardsim.MsgStr("Done."),
},
},
},
&cardsim.BasicCard[*SmokeTestCollection]{
CardTitle: cardsim.MsgStr("Draw a card"),
CardText: cardsim.MsgStr("Draw an extra card."),
CardOptions: []cardOption{
&cardsim.BasicOption[*SmokeTestCollection]{
Text: cardsim.MsgStr("Draw an extra card."),
Effect: func(p *player) error {
return p.Draw()
},
Output: cardsim.MsgStr("Drawn. Probably."),
},
},
},
}
}

25
smoketest/collection.go Normal file
View File

@ -0,0 +1,25 @@
package main
import (
"cardSimEngine/cardsim"
)
// SmokeTestCollection is a stats collection for the simple test sim.
type SmokeTestCollection struct {
Number cardsim.Stored[int]
Total cardsim.Stored[int64]
Turns cardsim.Invisible[int]
Flavor cardsim.Stored[string]
}
func (c *SmokeTestCollection) Average() float64 {
return float64(c.Total.Value) / float64(c.Turns.Value)
}
func (c *SmokeTestCollection) Stats() []cardsim.Stat {
stats := cardsim.ExtractStats(c)
stats = append(stats, cardsim.StatFunc("Average", c.Average))
cardsim.SortStats(stats)
return stats
}

78
smoketest/main.go Normal file
View File

@ -0,0 +1,78 @@
// Binary smoketest runs a very simple cardsim thing.
package main
import (
"cardSimEngine/cardsim"
"fmt"
"github.com/kr/pretty"
)
func main() {
p := cardsim.InitPlayer(
&SmokeTestCollection{
Number: cardsim.Stored[int]{
Name: "Number",
Value: 0,
},
Total: cardsim.Stored[int64]{
Name: "Total",
Value: 0,
},
Turns: cardsim.Invisible[int]{
Name: "Turns",
Value: 0,
},
Flavor: cardsim.Stored[string]{
Name: "Flavor",
Value: "Lemon",
},
},
)
p.Name = "Dave"
p.HandLimit = 3
p.ActionsPerTurn = 2
installRules(p.Rules)
initDeck(p.Deck)
installPermanentActions(&p.PermanentActions)
p.InfoPanels = []cardsim.InfoPanel[*SmokeTestCollection]{
&cardsim.BasicStatsPanel[*SmokeTestCollection]{
Name: cardsim.MsgStr("Stats"),
Intro: cardsim.MsgStr("Hi! These are the smoke test stats."),
},
ruledumper{},
}
p.Prompt = prompt{}
p.DebugLevel = 5
err := cardsim.RunSimpleTerminalUI(p)
if err != nil {
fmt.Println("Terminated with error:")
fmt.Println(err)
} else {
fmt.Println("Terminated without error.")
}
}
type prompt struct{}
func (prompt) Title(p *cardsim.Player[*SmokeTestCollection]) cardsim.Message {
return cardsim.MsgStr("Smoke Test")
}
func (prompt) Info(p *cardsim.Player[*SmokeTestCollection]) ([]cardsim.Message, error) {
return []cardsim.Message{
cardsim.MsgStr("Here, have some stuff."),
cardsim.Msgf("It's turn %d according to the player and turn %d according to me.", p.TurnNumber, p.Stats.Turns.Value),
}, nil
}
type ruledumper struct{}
func (ruledumper) Title(p *player) cardsim.Message {
return cardsim.MsgStr("Rule Dumper")
}
func (ruledumper) Info(p *player) ([]cardsim.Message, error) {
return []cardsim.Message{cardsim.Msgf("%# v", pretty.Formatter(p.Rules))}, nil
}

31
smoketest/rules.go Normal file
View File

@ -0,0 +1,31 @@
package main
import (
"cardSimEngine/cardsim"
"math"
)
var (
updateTotal = cardsim.RuleFunc[*SmokeTestCollection]{
Name: "updateTotal",
Seq: 1,
F: func(p *cardsim.Player[*SmokeTestCollection]) error {
p.Stats.Total.Value += int64(p.Stats.Number.Value)
return nil
},
}
countTurn = cardsim.RuleFunc[*SmokeTestCollection]{
Name: "countTurn",
Seq: math.MinInt,
F: func(p *cardsim.Player[*SmokeTestCollection]) error {
p.Stats.Turns.Value++
return nil
},
}
)
func installRules(rules *cardsim.RuleCollection[*SmokeTestCollection]) {
rules.Insert(&updateTotal)
rules.Insert(&countTurn)
}