28 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
a62de999ea Panel display, MultiMessage. 2023-04-02 00:20:37 -07:00
3eb087201f more terminal utility functions 2023-04-02 00:09:17 -07:00
c73545fd07 Implement screen clear. 2023-04-02 00:02:25 -07:00
34e1f3166f Main display mode skeleton 2023-04-01 23:59:53 -07:00
af9d9a6579 Skeleton of prototypical UI. 2023-04-01 23:22:50 -07:00
9e659ecf41 InitPlayer
InitPlayer creates very not-ready Player with its basic data structures initialized to an empty, ready-for-data state.
2023-04-01 22:19:42 -07:00
09fdf19948 CardOption.Enable
Make it possible for a card to display an option but not actually allow it to be selected. It's up to the UI layer to decide how to display options that are not enabled. The option text should probably contiain a note on why the option cannot be selected...
2023-04-01 22:09:05 -07:00
45e1eeebf7 Play cards. 2023-04-01 21:18:48 -07:00
576a2dd69e Implement drawing and running a turn. 2023-04-01 20:52:46 -07:00
13 changed files with 1307 additions and 32 deletions

View File

@ -5,9 +5,8 @@ package cardsim
type Card[C StatsCollection] interface {
// Title is the short name of the card displayed in the hand
// and at the top of the card output. It receives the current
// player as an argument. If it returns an error that is not
// a warning, the game crashes.
Title(p *Player[C]) (Message, error)
// player as an argument.
Title(p *Player[C]) Message
// Urgent reports whether the card is considered urgent. If
// the player has any urgent cards in hand, they cannot choose to act
@ -50,8 +49,12 @@ type CardOption[C StatsCollection] interface {
// a warning, the game crashes.
//
// 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)
// Enabled returns whether this option can curently be enacted.
Enabled(p *Player[C]) bool
}
// A BasicCard is a Card with fixed title, text, options, and optional
@ -61,37 +64,45 @@ type BasicCard[C StatsCollection] struct {
IsUrgent bool
CardText Message
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
}
func (b *BasicCard[C]) Title(p *Player[C]) (Message, error) {
return b.CardTitle, nil
// Title implements Card.
func (b *BasicCard[C]) Title(_ *Player[C]) Message {
return b.CardTitle
}
// Urgent implements Card.
func (b *BasicCard[C]) Urgent(_ *Player[C]) bool {
return b.IsUrgent
}
func (b *BasicCard[C]) EventText(p *Player[C]) (Message, error) {
// EventText implements Card.
func (b *BasicCard[C]) EventText(_ *Player[C]) (Message, error) {
return b.CardText, nil
}
// Options implements Card.
func (b *BasicCard[C]) Options(_ *Player[C]) ([]CardOption[C], error) {
return b.CardOptions, nil
}
// Then implements Card.
func (b *BasicCard[C]) Then(p *Player[C], option CardOption[C]) error {
if b.AfterOption == nil {
return nil
}
return b.AfterOption(p, option)
return b.AfterOption(b, p, option)
}
func (b *BasicCard[C]) Drawn(p *Player[C]) bool {
// Drawn implements Card.
func (b *BasicCard[C]) Drawn(_ *Player[C]) bool {
return true
}
// A BasicOption is a CardOption with fixed text, effects, and output.
// It's always enabled.
type BasicOption[C StatsCollection] struct {
Text Message
Effect func(*Player[C]) error
@ -108,6 +119,11 @@ func (b *BasicOption[C]) Enact(p *Player[C]) (Message, error) {
return b.Output, b.Effect(p)
}
// Enabled implements CardOption.
func (b *BasicOption[C]) Enabled(p *Player[C]) bool {
return true
}
// OptionFunc attaches a fixed prompt to an Enact-like function. Unlike
// BasicOption, the enactment function provided to OptionFunc returns
// the text that should be output as a result of the action, so it is
@ -121,10 +137,17 @@ type optionFunc[C StatsCollection] struct {
f func(*Player[C]) (Message, error)
}
// OptionText implements CardOption.
func (o *optionFunc[C]) OptionText(p *Player[C]) (Message, error) {
return o.text, nil
}
// Enact implements CardOption.
func (o *optionFunc[C]) Enact(p *Player[C]) (Message, error) {
return o.f(p)
}
// Enabled implements CardOption.
func (o *optionFunc[C]) Enabled(p *Player[C]) bool {
return true
}

View File

@ -52,6 +52,15 @@ func Warningf(f string, args ...any) *Warning {
return &Warning{fmt.Errorf(f, args...)}
}
// IsSeriousError returns whether e is a non-warning error. If e is nil, this
// returns false.
func IsSeriousError(e error) bool {
if e == nil {
return false
}
return !errors.Is(e, AnyWarning)
}
// A Failure is an error that is definitely not a Warning. If a Warning is
// wrapped in a Failure, it stops being a Warning.
type Failure struct {
@ -144,8 +153,11 @@ func (f Failure) As(target any) bool {
// - Otherwise, it returns an error that combines all the errors it collected.
// The aggregated error is a Warning if and only if all collected errors
// were also warnings.
//
// An ErrorCollector's Errs should not be written by anything other than Add,
// or HasFailure may get out of sync.
type ErrorCollector struct {
errs []error
Errs []error
hasFailure bool
}
@ -157,12 +169,12 @@ func (ec *ErrorCollector) Add(e error) {
if !errors.Is(e, AnyWarning) {
ec.hasFailure = true
}
ec.errs = append(ec.errs, e)
ec.Errs = append(ec.Errs, e)
}
// IsEmpty reports whether ec has zero accumulated errors/warnings.
func (ec *ErrorCollector) IsEmpty() bool {
return len(ec.errs) == 0
return len(ec.Errs) == 0
}
// HasFailure reports whether ec has at least one non-warning error.
@ -181,16 +193,16 @@ func (ec *ErrorCollector) HasFailure() bool {
// errors.Is does not erroneously represent a failure as a warning because it
// contains a warning as a subcomponent.
func (ec *ErrorCollector) Emit() error {
if len(ec.errs) == 0 {
if len(ec.Errs) == 0 {
return nil
}
if len(ec.errs) == 1 {
return ec.errs[0]
if len(ec.Errs) == 1 {
return ec.Errs[0]
}
if ec.HasFailure() {
return aggregateFailure(ec.errs)
return aggregateFailure(ec.Errs)
}
return &aggregateError{ec.errs} // all these are recognizable warnings
return &aggregateError{ec.Errs} // all these are recognizable warnings
}
// An aggregateError is a collection of errors that is itself an error.

View File

@ -11,7 +11,7 @@ import (
type InfoPanel[C StatsCollection] interface {
// Title returns the title of this InfoPanel, which is also used as the
// 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
// 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`.
func (b *BasicStatsPanel[C]) Title(p *Player[C]) (Message, error) {
return b.Name, nil
func (b *BasicStatsPanel[C]) Title(p *Player[C]) Message {
return b.Name
}
// Info implements `InfoPanel[C]` by dumpiing p.Stats, showing those items for

View File

@ -1,6 +1,9 @@
package cardsim
import "fmt"
import (
"fmt"
"strings"
)
// Message is an opaque interface representing a displayable message.
// Using an interface here allows for implementation of new message display
@ -25,3 +28,50 @@ func MsgStr(s string) Message {
func Msgf(f string, args ...any) Message {
return stringMessage(fmt.Sprintf(f, args...))
}
// A SpecialMessage is a specific, uniquely identifiable message.
type SpecialMessage struct {
msg Message
}
// String implements Message.
func (s *SpecialMessage) String() string {
if s == nil {
return ""
}
return s.msg.String()
}
// Messages that various display surfaces or other components may have a special interpretation of
// and identify specifically. These are largely sentinel values. Nil is a paragraph break.
var (
SectionBreak = &SpecialMessage{MsgStr(" -------------------------------------------------------------------- ")}
ChapterBreak = &SpecialMessage{MsgStr(" ==================================================================== ")}
)
// IsSpecialMessage returns whether a provided Message is a specific SpecialMessage.
func IsSpecialMessage(m Message, s *SpecialMessage) bool {
if m == nil {
return s == nil
}
if s2, ok := m.(*SpecialMessage); ok {
return s == s2
}
return false
}
// MultiMessage is a sequence of messages treated like one message.
type MultiMessage []Message
func (m MultiMessage) String() string {
s := make([]string, len(m))
for i, msg := range m {
if msg == nil {
s[i] = ""
continue
}
s[i] = msg.String()
continue
}
return strings.Join(s, "\n")
}

View File

@ -1,6 +1,21 @@
package cardsim
import "math/rand"
import (
"errors"
"fmt"
"math/rand"
"time"
)
var (
ErrUncooperativeCards = errors.New("a milion cards refused to join the hand")
ErrInvalidCard = errors.New("invalid card specified")
ErrInvalidChoice = errors.New("invalid choice specified")
ErrNotUrgent = errors.New("action not urgent when urgent card is available")
ErrNoActions = errors.New("no actions remaining")
WarningStalemate = errors.New("no actions can be taken")
)
// Player stores all gameplay state for one player at a specific point in time.
// Game-specific data is stored in Stats.
@ -59,7 +74,7 @@ type Player[C StatsCollection] struct {
Name string
// Rand is a source of randomness that other components can use.
Rand rand.Rand
Rand *rand.Rand
Deck *Deck[C]
Hand []Card[C]
@ -83,7 +98,10 @@ type Player[C StatsCollection] struct {
ActionsPerTurn int
ActionsRemaining int
// PermanentActions are an "extra hand" of cards that are not discarded when used.
// PermanentActions are an "extra hand" of cards that are not discarded when
// used. An Urgent PermanentAction does not block non-urgent actions and
// cards in hand from being used, but it can be used even when an urgent
// card is in the hand.
PermanentActions []Card[C]
// InfoPanels lists informational views available to the player. The Prompt
@ -134,7 +152,331 @@ const (
GameStalled
)
// InitPlayer returns a mostly-uninitialized Player with the fields
// that require specific initialization already configured and the
// provided StatsCollection (if any) already assigned to its Stats.
// Most fields are not configured and need to be assigned after this.
//
// The Player is initialized with an empty deck, empty rule collection,
// a hand limit of 1, an actions-per-turn limit of 1, and a random
// number generator seeded with the nanosecond component of the current time.
// The Deck shares this random number generator.
func InitPlayer[C StatsCollection](stats C) *Player[C] {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
return &Player[C]{
Stats: stats,
Rand: r,
Deck: &Deck[C]{rand: r},
HandLimit: 1,
ActionsPerTurn: 1,
Rules: NewRuleCollection[C](),
}
}
// Over returns whether this state represents a game that is over.
func (g GameState) Over() bool {
return g == GameLost || g == GameWon || g == GameCrashed || g == GameStalled
}
// ChapterBreak apends a chapter break to p.TemporaryMessages, unless it is
// empty or the most recent non-nil message is already a chapter break.
func (p *Player[C]) ChapterBreak() {
for i := len(p.TemporaryMessages) - 1; i >= 0; i-- {
m := p.TemporaryMessages[i]
if IsSpecialMessage(m, ChapterBreak) {
return
}
if p.TemporaryMessages[i] != nil {
p.TemporaryMessages = append(p.TemporaryMessages, ChapterBreak)
return
}
}
// No non-nil messages -- nothing to do.
}
// SectionBreak apends a section break to p.TemporaryMessages, unless it is
// empty or the most recent non-nil message is already a section/chapter break.
func (p *Player[C]) SectionBreak() {
for i := len(p.TemporaryMessages) - 1; i >= 0; i-- {
m := p.TemporaryMessages[i]
if IsSpecialMessage(m, ChapterBreak) || IsSpecialMessage(m, SectionBreak) {
return
}
if m != nil {
p.TemporaryMessages = append(p.TemporaryMessages, SectionBreak)
return
}
}
// No non-nil messages -- nothing to do.
}
// Simulate executes the simulation up to the start of the next turn. If the
// simulation crashes, the game state becomes GameCrashed. This returns any
// generated errors; if the debugging mode is 0 or greater, they also become
// temporary messages for the next turn.
func (p *Player[C]) Simulate() error {
var errs ErrorCollector
p.TemporaryMessages = nil
p.TemporaryPanels = nil
errs.Add(p.Rules.Run(p))
errs.Add(p.StartNextTurn())
if errs.HasFailure() {
p.State = GameCrashed
}
if p.DebugLevel > 0 && !errs.IsEmpty() {
p.ChapterBreak()
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) {
yikes = "!"
}
p.TemporaryMessages = append(p.TemporaryMessages, Msgf("%s[%d]:\t%v", yikes, i, e))
}
}
return errs.Emit()
}
// StartNextTurn increments the turn counter, resets the action counter,
// and draws back up to full. If the player cannot take any actions after
// drawing is complete, the game stalls. (If a drawn card would like to
// force the player to take no actions for a turn, the best approach is to
// make that card Urgent and make it reset ActionsRemaining to 0 after it runs.)
func (p *Player[C]) StartNextTurn() error {
var errs ErrorCollector
p.TurnNumber++
p.ActionsRemaining = p.ActionsPerTurn
errs.Add(p.FillHand())
if len(p.Hand)+len(p.PermanentActions) == 0 {
p.State = GameStalled
errs.Add(Warningf("%w: no cards in hand, no permanent actions", WarningStalemate))
}
if p.ActionsRemaining == 0 {
p.State = GameStalled
errs.Add(Warningf("%w: 0 actions available in the turn", WarningStalemate))
}
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
// 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]) FillHand() error {
var lastErr error
for p.Deck.Len() > 0 && len(p.Hand) < p.HandLimit {
lastErr = p.Draw()
if IsSeriousError(lastErr) {
return lastErr
}
}
if len(p.Hand) >= p.HandLimit {
return nil
}
return WarningTooFewCards
}
// HasUrgentCards returns whether any cards in the Hand think they are Urgent.
func (p *Player[C]) HasUrgentCards() bool {
for _, c := range p.Hand {
if c.Urgent(p) {
return true
}
}
return false
}
// EnactCardUnchecked executes a card choice, removes it from the hand, and
// decrements the ActionsRemaining. It does not check for conflicting Urgent
// cards or already being out of actions. If no such card or card choice
// exists, or the specified choice is not enabled, this returns nil and
// ErrInvalidCard/ErrInvalidChoice without changing anything. Otherwise, this
// returns the result of enacting the card. If enacting the card causes a
// serious error, the State becomes GameCrashed.
func (p *Player[C]) EnactCardUnchecked(cardIdx, choiceIdx int) (Message, error) {
if cardIdx < 0 || cardIdx >= len(p.Hand) {
return nil, fmt.Errorf("%w: no card #%d when %d cards in hand", ErrInvalidCard, cardIdx, len(p.Hand))
}
card := p.Hand[cardIdx]
var errs ErrorCollector
options, err := card.Options(p)
if IsSeriousError(err) {
p.State = GameCrashed
return nil, err
}
errs.Add(err)
if choiceIdx < 0 || choiceIdx > len(options) {
errs.Add(fmt.Errorf("%w: no option #%d on card #%d with %d options", ErrInvalidChoice, choiceIdx, cardIdx, len(options)))
return nil, errs.Emit()
}
chosen := options[choiceIdx]
if !chosen.Enabled(p) {
errs.Add(fmt.Errorf("%w: option %d on card %d was not enabled", ErrInvalidChoice, choiceIdx, cardIdx))
return nil, errs.Emit()
}
p.Hand = DeleteFrom(p.Hand, cardIdx)
p.ActionsRemaining--
ret, err := options[choiceIdx].Enact(p)
errs.Add(err)
if IsSeriousError(err) {
p.State = GameCrashed
return ret, errs.Emit()
}
err = card.Then(p, options[choiceIdx])
errs.Add(err)
if IsSeriousError(err) {
p.State = GameCrashed
}
return ret, errs.Emit()
}
// EnactCard executes a card choice, removes it from the hand, and decrements
// the ActionsRemaining. If the card is not Urgent but urgent cards are
// available, or the player is out of actions, this returns ErrNotUrgent or
// ErrNoActions. Otherwise, this acts like EnactCardUnchecked.
func (p *Player[C]) EnactCard(cardIdx, choiceIdx int) (Message, error) {
if p.ActionsRemaining <= 0 {
return nil, ErrNoActions
}
if cardIdx < 0 || cardIdx >= len(p.Hand) {
return nil, fmt.Errorf("%w: no card #%d when %d cards in hand", ErrInvalidCard, cardIdx, len(p.Hand))
}
if !p.Hand[cardIdx].Urgent(p) && p.HasUrgentCards() {
return nil, ErrNotUrgent
}
return p.EnactCardUnchecked(cardIdx, choiceIdx)
}
// EnactPermanentActionUnchecked executes a permanently-available action and
// decrements the ActionsRemaining. It does not check for conflicting Urgent
// cards or already being out of actions. If no such action or card option
// exists, or the option is not enabled, this returns nil and ErrInvalidCard
// or ErrInvalidChoice without changing anything. Otherwise, this returns the
// result of enacting the permanent action. If enacting the card causes a
// serious error, the State becomes GameCrashed.
func (p *Player[C]) EnactPermanentActionUnchecked(actionIdx, choiceIdx int) (Message, error) {
if actionIdx < 0 || actionIdx >= len(p.PermanentActions) {
return nil, fmt.Errorf("%w: no action #%d when %d permanent actions exist", ErrInvalidCard, actionIdx, len(p.PermanentActions))
}
card := p.PermanentActions[actionIdx]
var errs ErrorCollector
options, err := card.Options(p)
if IsSeriousError(err) {
p.State = GameCrashed
return nil, err
}
errs.Add(err)
if choiceIdx < 0 || choiceIdx > len(options) {
errs.Add(fmt.Errorf("%w: no option #%d on permanent action #%d with %d options", ErrInvalidChoice, choiceIdx, actionIdx, len(options)))
return nil, errs.Emit()
}
chosen := options[choiceIdx]
if !chosen.Enabled(p) {
errs.Add(fmt.Errorf("%w: option #%d on permanent action #%d is not enabled", ErrInvalidChoice, choiceIdx, actionIdx))
return nil, errs.Emit()
}
p.ActionsRemaining--
ret, err := chosen.Enact(p)
errs.Add(err)
if IsSeriousError(err) {
p.State = GameCrashed
return ret, errs.Emit()
}
err = card.Then(p, chosen)
errs.Add(err)
if IsSeriousError(err) {
p.State = GameCrashed
}
return ret, errs.Emit()
}
// EnactPermanentAction executes a permanently-available card and decrements
// the ActionsRemaining. If the action is not Urgent but urgent cards are
// available, or the player is out of actions, this returns ErrNotUrgent or
// ErrNoActions. Otherwise, this acts like EnactPermanentActionUnchecked.
func (p *Player[C]) EnactPermanentAction(actionIdx, choiceIdx int) (Message, error) {
if p.ActionsRemaining <= 0 {
return nil, ErrNoActions
}
if actionIdx < 0 || actionIdx >= len(p.PermanentActions) {
return nil, fmt.Errorf("%w: no action #%d when %d permanent actions available", ErrInvalidCard, actionIdx, len(p.PermanentActions))
}
if !p.PermanentActions[actionIdx].Urgent(p) && p.HasUrgentCards() {
return nil, ErrNotUrgent
}
return p.EnactPermanentActionUnchecked(actionIdx, choiceIdx)
}
// ReportError adds an error to the temporary messages, depending on
// its severity and debug settings:
//
// - If the error is nil, this never does anything.
// - If the error is serious, this emits the error if the debug level is
// -1 or greater.
// - If the error is only a warning, this emits the error if the debug
// level is 0 or greater.
func (p *Player[C]) ReportError(e error) {
if e == nil || p.DebugLevel < -1 {
return
}
if p.DebugLevel < 0 && !IsSeriousError(e) {
return
}
p.ChapterBreak()
severity := "[Warning]"
if IsSeriousError(e) {
severity = "[ERROR]"
}
p.TemporaryMessages = append(p.TemporaryMessages, Msgf("%s: %v", severity, e))
}
// CanAct returns whether the player has actions theoretically available.
func (p *Player[C]) CanAct() bool {
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
s := r.byStep[k.Step()]
if s == nil {
if len(s) == 0 {
r.steps = nil
}
s = append(s, k.id)
@ -326,7 +326,7 @@ func (r *RuleCollection[C]) Run(p *Player[C]) error {
steps := r.steps
if steps == nil {
// Step set changed, recalculate.
steps := make([]int, 0, len(r.byStep))
steps = make([]int, 0, len(r.byStep))
for step := range r.byStep {
steps = append(steps, step)
}
@ -334,18 +334,21 @@ func (r *RuleCollection[C]) Run(p *Player[C]) error {
r.steps = steps
}
p.Debug(2, Msgf("Executing steps: %v", steps))
var errs ErrorCollector
for _, step := range steps {
stepRules := r.byStep[step]
p.Rand.Shuffle(len(stepRules), func(i, j int) {
stepRules[i], stepRules[j] = stepRules[j], stepRules[i]
})
p.Debug(3, Msgf("Executing step %d; length %d", step, len(stepRules)))
ShuffleAll(stepRules, p.Rand)
var remove []RuleID
halt := false
for _, id := range stepRules {
rule := r.rules[id]
p.Debug(4, Msgf("Executing rule %x (labeled %q)", id, rule.Label()))
err := rule.Enact(p)
if err != nil {
p.Debug(2, Msgf("Rule %x (%q): error: %v", id, rule.Label(), err))
ignore := false
if errors.Is(err, ErrDeleteRule) {
remove = append(remove, id)
@ -377,10 +380,14 @@ func (r *RuleCollection[C]) Run(p *Player[C]) error {
}
}
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() {

531
cardsim/terminalui.go Normal file
View File

@ -0,0 +1,531 @@
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("<press ENTER to continue>")
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("<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)
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()
}
}
}
}

6
go.mod
View File

@ -1,3 +1,9 @@
module cardSimEngine
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)
}