From 2480a1631bfa26ee863bf77fa982d592a82cf7c3 Mon Sep 17 00:00:00 2001 From: Kistaro Windrider Date: Sun, 2 Apr 2023 19:01:40 -0700 Subject: [PATCH] Implement a very crude "game" as a test. Also updates Player. --- cardsim/card.go | 8 +- cardsim/player.go | 34 ++++++--- smoketest/cards.go | 162 ++++++++++++++++++++++++++++++++++++++++ smoketest/collection.go | 25 +++++++ smoketest/main.go | 62 +++++++++++++++ smoketest/rules.go | 31 ++++++++ 6 files changed, 308 insertions(+), 14 deletions(-) create mode 100644 smoketest/cards.go create mode 100644 smoketest/collection.go create mode 100644 smoketest/main.go create mode 100644 smoketest/rules.go diff --git a/cardsim/card.go b/cardsim/card.go index a1eb4da..673bd3d 100644 --- a/cardsim/card.go +++ b/cardsim/card.go @@ -49,7 +49,8 @@ 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. @@ -63,7 +64,8 @@ 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 } // Title implements Card. @@ -91,7 +93,7 @@ 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) } // Drawn implements Card. diff --git a/cardsim/player.go b/cardsim/player.go index 6576554..7a7defc 100644 --- a/cardsim/player.go +++ b/cardsim/player.go @@ -260,18 +260,34 @@ func (p *Player[C]) StartNextTurn() error { 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 { - failureLimit := 1000000 - for failureLimit > 0 && p.Deck.Len() > 0 && len(p.Hand) < p.HandLimit { - c := p.Deck.Draw() - if c.Drawn(p) { - p.Hand = append(p.Hand, c) - } else { - failureLimit-- + var lastErr error + for p.Deck.Len() > 0 && len(p.Hand) < p.HandLimit { + lastErr = p.Draw() + if IsSeriousError(lastErr) { + return lastErr } } @@ -279,10 +295,6 @@ func (p *Player[C]) FillHand() error { return nil } - if failureLimit <= 0 { - return ErrUncooperativeCards - } - return WarningTooFewCards } diff --git a/smoketest/cards.go b/smoketest/cards.go new file mode 100644 index 0000000..f92b924 --- /dev/null +++ b/smoketest/cards.go @@ -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."), + }, + }, + }, + } +} diff --git a/smoketest/collection.go b/smoketest/collection.go new file mode 100644 index 0000000..7fcc17b --- /dev/null +++ b/smoketest/collection.go @@ -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[int] + 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 +} diff --git a/smoketest/main.go b/smoketest/main.go new file mode 100644 index 0000000..e27f6f3 --- /dev/null +++ b/smoketest/main.go @@ -0,0 +1,62 @@ +// Binary smoketest runs a very simple cardsim thing. +package main + +import ( + "cardSimEngine/cardsim" + "fmt" +) + +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."), + }, + } + 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("Prompt title -- should not be visible?") +} + +func (prompt) Info(p *cardsim.Player[*SmokeTestCollection]) ([]cardsim.Message, error) { + return []cardsim.Message{cardsim.MsgStr("Here, have some stuff.")}, nil +} diff --git a/smoketest/rules.go b/smoketest/rules.go new file mode 100644 index 0000000..ceffc7c --- /dev/null +++ b/smoketest/rules.go @@ -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) +}