From 3eec5bfc7dfd6332a0150d1084b6c81aa66fee2a Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Mon, 1 May 2023 11:24:08 +0200 Subject: [PATCH] Added fuzzy tester. --- assets/scripts/status_effects.lua | 2 +- assets/scripts/story_teller.lua | 4 +- cmd/fuzzy-tester/main.go | 92 ++++++++++++++++++++++ cmd/fuzzy-tester/operations.go | 126 ++++++++++++++++++++++++++++++ game/artifact_test.go | 2 +- game/resources.go | 4 + game/session.go | 52 ++++++++++-- 7 files changed, 271 insertions(+), 11 deletions(-) create mode 100644 cmd/fuzzy-tester/main.go create mode 100644 cmd/fuzzy-tester/operations.go diff --git a/assets/scripts/status_effects.lua b/assets/scripts/status_effects.lua index 40b19c9..80557d9 100644 --- a/assets/scripts/status_effects.lua +++ b/assets/scripts/status_effects.lua @@ -14,7 +14,7 @@ register_status_effect("WEAKEN", { decay = DECAY_ALL, rounds = 1, callbacks = { - on_damage_calc = function() + on_damage_calc = function(ctx) if ctx.source == ctx.owner then return ctx.damage - ctx.stacks * 2 end diff --git a/assets/scripts/story_teller.lua b/assets/scripts/story_teller.lua index 8a9d17b..1fd239e 100644 --- a/assets/scripts/story_teller.lua +++ b/assets/scripts/story_teller.lua @@ -103,7 +103,7 @@ register_story_teller("STAGE_2", { -- BOSS end - return nil + return GAME_STATE_FIGHT end }) @@ -125,6 +125,6 @@ register_story_teller("STAGE_3", { -- BOSS end - return nil + return GAME_STATE_FIGHT end }) \ No newline at end of file diff --git a/cmd/fuzzy-tester/main.go b/cmd/fuzzy-tester/main.go new file mode 100644 index 0000000..af16642 --- /dev/null +++ b/cmd/fuzzy-tester/main.go @@ -0,0 +1,92 @@ +package main + +import ( + "flag" + "fmt" + "github.com/BigJk/end_of_eden/game" + "github.com/samber/lo" + "math/rand" + "os" + "runtime/debug" + "strings" + "sync" + "time" +) + +var endTime int64 +var seed int64 +var mods []string + +func main() { + fmt.Println("End Of Eden :: Fuzzy Tester") + fmt.Println("The fuzzy tester hits a game session with a random number of operations and tries to trigger a panic.") + fmt.Println() + + routines := flag.Int("n", 0, "number of goroutines") + timeout := flag.Duration("timeout", time.Minute, "length of testing") + baseSeed := flag.Int64("seed", 0, "random seed") + modsString := flag.String("mods", "", "mods to load and test, separated by ',' (e.g. mod1,mod2,mod3)") + flag.Parse() + + if *routines == 0 { + flag.PrintDefaults() + return + } + + seed = *baseSeed + endTime = time.Now().Add(*timeout).Unix() + + if len(*modsString) > 0 { + mods = strings.Split(*modsString, ",") + } + + if *baseSeed == 0 { + seed = rand.Int63() + } + + fmt.Println("N :", *routines) + fmt.Println("Seed :", seed) + fmt.Println("\nWorking...") + + wg := &sync.WaitGroup{} + for i := 0; i < *routines; i++ { + wg.Add(1) + tester(i, wg) + } + + wg.Wait() +} + +func tester(index int, wg *sync.WaitGroup) { + rnd := rand.New(rand.NewSource(seed + int64(index))) + opKeys := lo.Keys(Operations) + stack := [][]string{} + + defer func() { + if r := recover(); r != nil { + fmt.Println(stack) + fmt.Println(r) + fmt.Println(string(debug.Stack())) + os.Exit(-1) + } + }() + + for time.Now().Unix() < endTime { + s := game.NewSession(game.WithMods(mods)) + ops := 5 + rand.Intn(100) + stack = [][]string{} + s.SetOnLuaError(func(file string, line int, callback string, typeId string, err error) { + fmt.Println("File :", file) + fmt.Println("Line :", line) + fmt.Println("Callback :", callback) + fmt.Println("TypeId :", typeId) + fmt.Println("Err :", err) + panic("lua error") + }) + + for i := 0; i < ops; i++ { + next := lo.Shuffle(opKeys)[0] + stack = append(stack, []string{next, Operations[next](rnd, s)}) + } + } +} diff --git a/cmd/fuzzy-tester/operations.go b/cmd/fuzzy-tester/operations.go new file mode 100644 index 0000000..3fab075 --- /dev/null +++ b/cmd/fuzzy-tester/operations.go @@ -0,0 +1,126 @@ +package main + +import ( + "fmt" + "github.com/BigJk/end_of_eden/game" + "github.com/samber/lo" + "math/rand" +) + +func Shuffle[T any](rnd *rand.Rand, collection []T) []T { + rnd.Shuffle(len(collection), func(i, j int) { + collection[i], collection[j] = collection[j], collection[i] + }) + + return collection +} + +var Operations = map[string]func(rnd *rand.Rand, s *game.Session) string{ + "FinishPlayerTurn": func(rnd *rand.Rand, s *game.Session) string { + s.FinishPlayerTurn() + return "Finish player turn" + }, + "FinishFight": func(rnd *rand.Rand, s *game.Session) string { + s.FinishFight() + return "Finish fight" + }, + "CastCard": func(rnd *rand.Rand, s *game.Session) string { + guid := Shuffle(rnd, lo.Flatten([][]string{{""}, s.GetInstances(), s.GetActors()}))[0] + target := Shuffle(rnd, lo.Flatten([][]string{{""}, s.GetInstances(), s.GetActors()}))[0] + s.CastCard(guid, target) + return fmt.Sprintf("Cast card with guid '%s' on '%s'", guid, target) + }, + "AddActorFromEnemy": func(rnd *rand.Rand, s *game.Session) string { + res := s.GetResources() + enemyId := Shuffle(rnd, lo.Flatten([][]string{{""}, lo.Keys(res.Enemies)}))[0] + s.AddActorFromEnemy(enemyId) + return fmt.Sprintf("Added enemy '%s'", enemyId) + }, + "SetEvent": func(rnd *rand.Rand, s *game.Session) string { + res := s.GetResources() + eventId := Shuffle(rnd, lo.Flatten([][]string{{""}, lo.Keys(res.Events)}))[0] + s.SetEvent(eventId) + return fmt.Sprintf("Set event '%s'", eventId) + }, + "SetGameState": func(rnd *rand.Rand, s *game.Session) string { + res := s.GetResources() + eventId := Shuffle(rnd, lo.Flatten([][]string{{""}, lo.Keys(res.Events)}))[0] + s.SetGameState(Shuffle(rnd, []game.GameState{game.GameStateGameOver, game.GameStateMerchant, game.GameStateRandom, game.GameStateEvent, game.GameStateFight, game.GameState("")})[0]) + return fmt.Sprintf("Set event '%s'", eventId) + }, + "FinishEvent": func(rnd *rand.Rand, s *game.Session) string { + res := s.GetResources() + eventId := Shuffle(rnd, lo.Flatten([][]string{lo.Keys(res.Events)}))[0] + event := res.Events[eventId] + choice := rnd.Intn(len(event.Choices) + 1) + s.FinishEvent(choice) + return fmt.Sprintf("Finish event '%s' with choice %d", eventId, choice) + }, + "CleanUpFight": func(rnd *rand.Rand, s *game.Session) string { + s.CleanUpFight() + return "Clean up fight" + }, + "SetupFight": func(rnd *rand.Rand, s *game.Session) string { + s.SetupFight() + return "Setup fight" + }, + "SetupMerchant": func(rnd *rand.Rand, s *game.Session) string { + s.SetupMerchant() + return "Setup merchant" + }, + "LeaveMerchant": func(rnd *rand.Rand, s *game.Session) string { + s.LeaveMerchant() + return "Leave merchant" + }, + "GivePlayerGold": func(rnd *rand.Rand, s *game.Session) string { + gold := rnd.Intn(100) + s.GivePlayerGold(gold) + return fmt.Sprintf("Give %d gold to player", gold) + }, + "PlayerGiveActionPoints": func(rnd *rand.Rand, s *game.Session) string { + actionPoints := rnd.Intn(5) + s.PlayerGiveActionPoints(actionPoints) + return fmt.Sprintf("Give %d action points to player", actionPoints) + }, + "AddCard": func(rnd *rand.Rand, s *game.Session) string { + res := s.GetResources() + cardId := Shuffle(rnd, lo.Flatten([][]string{{""}, lo.Keys(res.Cards)}))[0] + s.GiveCard(cardId, game.PlayerActorID) + return fmt.Sprintf("Give '%s' card to player", cardId) + }, + "AddArtifact": func(rnd *rand.Rand, s *game.Session) string { + res := s.GetResources() + artifactId := Shuffle(rnd, lo.Flatten([][]string{{""}, lo.Keys(res.Artifacts)}))[0] + s.GiveArtifact(artifactId, game.PlayerActorID) + return fmt.Sprintf("Give '%s' artifact to player", artifactId) + }, + "PlayerBuyCard": func(rnd *rand.Rand, s *game.Session) string { + res := s.GetResources() + cardId := Shuffle(rnd, lo.Flatten([][]string{{""}, lo.Keys(res.Cards)}))[0] + s.PlayerBuyCard(cardId) + return fmt.Sprintf("Buy '%s' card as player", cardId) + }, + "PlayerBuyArtifact": func(rnd *rand.Rand, s *game.Session) string { + res := s.GetResources() + artifactId := Shuffle(rnd, lo.Flatten([][]string{{""}, lo.Keys(res.Artifacts)}))[0] + s.PlayerBuyArtifact(artifactId) + return fmt.Sprintf("Buy '%s' artifact as player", artifactId) + }, + "AddStatusEffect": func(rnd *rand.Rand, s *game.Session) string { + res := s.GetResources() + effectId := Shuffle(rnd, lo.Flatten([][]string{{""}, lo.Keys(res.StatusEffects)}))[0] + stacks := rnd.Intn(10) + s.GiveStatusEffect(effectId, game.PlayerActorID, stacks) + return fmt.Sprintf("Give '%s' status effect with %d stacks to player", effectId, stacks) + }, + "BuyUpgradeCard": func(rnd *rand.Rand, s *game.Session) string { + cardId := Shuffle(rnd, lo.Flatten([][]string{{""}, s.GetInstances()}))[0] + s.BuyUpgradeCard(cardId) + return fmt.Sprintf("Buy upgrading card '%s'", cardId) + }, + "BuyRemoveCard": func(rnd *rand.Rand, s *game.Session) string { + cardId := Shuffle(rnd, lo.Flatten([][]string{{""}, s.GetInstances()}))[0] + s.BuyRemoveCard(cardId) + return fmt.Sprintf("Buy removing card '%s'", cardId) + }, +} diff --git a/game/artifact_test.go b/game/artifact_test.go index adbc311..61ab819 100644 --- a/game/artifact_test.go +++ b/game/artifact_test.go @@ -35,7 +35,7 @@ register_artifact( func TestArtifact(t *testing.T) { s := lua.NewState() - man := NewResourcesManager(s, log.New(io.Discard, "", 0)) + man := NewResourcesManager(s, nil, log.New(io.Discard, "", 0)) // Evaluate lua if !assert.NoError(t, s.DoString(TestArtifactLua)) { diff --git a/game/resources.go b/game/resources.go index e72c667..4a6f7bb 100644 --- a/game/resources.go +++ b/game/resources.go @@ -201,6 +201,10 @@ func (man *ResourcesManager) luaDeleteEvent(l *lua.LState) int { } func (man *ResourcesManager) defineDocs(docs *ludoc.Docs) { + if docs == nil { + return + } + docs.Category("Content Registry", "These functions are used to define new content in the base game and in mods.", 100) docs.Function("register_artifact", fmt.Sprintf("Registers a new artifact.\n\n```lua\n%s\n```", `register_artifact("REPULSION_STONE", diff --git a/game/session.go b/game/session.go index 4b2383a..0abae08 100644 --- a/game/session.go +++ b/game/session.go @@ -264,6 +264,10 @@ func (s *Session) GobDecode(data []byte) error { return nil } +func (s *Session) GetResources() *ResourcesManager { + return s.resources +} + // // Internal // @@ -574,7 +578,7 @@ func (s *Session) FinishFight() bool { return false } -// FinishEvent finishes a event with the given choice. If the game state is not in the EVENT state this +// FinishEvent finishes an event with the given choice. If the game state is not in the EVENT state this // does nothing. func (s *Session) FinishEvent(choice int) { if len(s.currentEvent) == 0 || s.state != GameStateEvent { @@ -849,7 +853,7 @@ func (s *Session) GetInstances() []string { return lo.Keys(s.instances) } -// GetInstance returns a instance by guid. An instance is a CardInstance or ArtifactInstance. +// GetInstance returns an instance by guid. An instance is a CardInstance or ArtifactInstance. func (s *Session) GetInstance(guid string) any { return s.instances[guid] } @@ -983,6 +987,13 @@ func (s *Session) GiveStatusEffect(typeId string, owner string, stacks int) stri } status := s.resources.StatusEffects[typeId] + if status == nil { + return "" + } + + if _, ok := s.actors[owner]; !ok { + return "" + } // TODO: This should always be either 0 or 1 len, so the logic down below is a bit meh. same := lo.Filter(s.actors[owner].StatusEffects.ToSlice(), func(guid string, index int) bool { @@ -995,7 +1006,7 @@ func (s *Session) GiveStatusEffect(typeId string, owner string, stacks int) stri }) if len(same) > 1 { - log.Println("Error: status effect duplicate!") + panic("Error: status effect duplicate!") } // If it can't stack we delete all existing instances @@ -1035,7 +1046,11 @@ func (s *Session) GiveStatusEffect(typeId string, owner string, stacks int) stri // RemoveStatusEffect removes a status effect by guid. func (s *Session) RemoveStatusEffect(guid string) { - instance := s.instances[guid].(StatusEffectInstance) + instance, ok := s.instances[guid].(StatusEffectInstance) + if !ok { + return + } + if _, err := s.resources.StatusEffects[instance.TypeID].Callbacks[CallbackOnStatusRemove].Call(CreateContext("type_id", instance.TypeID, "guid", guid, "owner", instance.Owner)); err != nil { s.logLuaError(CallbackOnStatusRemove, instance.TypeID, err) } @@ -1056,7 +1071,11 @@ func (s *Session) GetActorStatusEffects(guid string) []string { // AddStatusEffectStacks increases the stacks of a certain status effect by guid. func (s *Session) AddStatusEffectStacks(guid string, stacks int) { - instance := s.instances[guid].(StatusEffectInstance) + instance, ok := s.instances[guid].(StatusEffectInstance) + if !ok { + return + } + instance.Stacks += stacks if instance.Stacks <= 0 { s.RemoveStatusEffect(guid) @@ -1067,7 +1086,11 @@ func (s *Session) AddStatusEffectStacks(guid string, stacks int) { // SetStatusEffectStacks sets the stacks of a certain status effect by guid. func (s *Session) SetStatusEffectStacks(guid string, stacks int) { - instance := s.instances[guid].(StatusEffectInstance) + instance, ok := s.instances[guid].(StatusEffectInstance) + if !ok { + return + } + instance.Stacks = stacks if instance.Stacks <= 0 { s.RemoveStatusEffect(guid) @@ -1136,6 +1159,10 @@ func (s *Session) GetArtifact(guid string) (*Artifact, ArtifactInstance) { // GiveArtifact gives an artifact to an actor. func (s *Session) GiveArtifact(typeId string, owner string) string { + if _, ok := s.resources.Artifacts[typeId]; !ok { + return "" + } + instance := ArtifactInstance{ TypeID: typeId, GUID: NewGuid("ARTIFACT"), @@ -1186,6 +1213,10 @@ func (s *Session) GetCard(guid string) (*Card, CardInstance) { } func (s *Session) GiveCard(typeId string, owner string) string { + if _, ok := s.resources.Cards[typeId]; !ok { + return "" + } + instance := CardInstance{ TypeID: typeId, GUID: NewGuid("CARD"), @@ -1353,6 +1384,10 @@ func (s *Session) UpgradeCard(guid string) bool { // func (s *Session) DealDamage(source string, target string, damage int, flat bool) int { + if _, ok := s.actors[source]; !ok { + return 0 + } + val, ok := s.actors[target] if !ok { return 0 @@ -1499,7 +1534,10 @@ func (s *Session) GetActors() []string { } func (s *Session) GetActor(id string) Actor { - return s.actors[id] + if val, ok := s.actors[id]; ok { + return val + } + return NewActor("") } func (s *Session) UpdateActor(id string, update func(actor *Actor) bool) {