using Godot; using System; using System.Collections.Generic; public partial class TestRunner : Node { private int passedTests = 0; private int failedTests = 0; public override void _Ready() { Run("Inventory adds, stacks and removes items", TestInventoryStacksAndRemovesItems); Run("Inventory crafting checks stacked totals", TestInventoryCanCraftAcrossStacks); Run("Survival consumes stored food and water", TestSurvivalConsumesFoodAndWater); Run("Survival grace period prevents early death", TestSurvivalGracePeriodPreventsEarlyDeath); Run("Survival death disables movement", TestSurvivalDeathDisablesMovement); Run("Robot research effects change robot stats", TestRobotResearchEffects); Run("Research completion applies effects once", TestResearchCompletionAppliesEffectsOnce); Run("Research execution pays resources and finishes", TestResearchExecutionPaysResourcesAndFinishes); Run("Research cannot start without resources", TestResearchCannotStartWithoutResources); Run("Inventory add failure keeps inventory unchanged", TestInventoryAddFailureKeepsInventoryUnchanged); Run("Resource extraction and save data roundtrip", TestResourceSaveRoundtrip); Run("Robot save data roundtrip keeps robot state", TestRobotSaveRoundtrip); Run("Save data captures and restores global state", TestSaveDataRestoresGlobalState); Run("Save data restores survival timer", TestSaveDataRestoresSurvivalTimer); Run("Split save files store and load data", TestSplitSaveFilesRoundtrip); Run("Split save files include one file per saved layer", TestSplitSaveFilesIncludeLayerFiles); Run("If node evaluates inventory comparisons", TestIfNodeEvaluatesInventoryComparisons); Run("While node reports false conditions", TestWhileNodeReportsFalseConditions); Run("For node stops after configured amount", TestForNodeStopsAfterConfiguredAmount); Run("Item data readable names are stable", TestItemDataReadableNames); Run("Resource files load core game data", TestResourceFilesLoadCoreData); GD.Print($"Tests passed: {passedTests}, failed: {failedTests}"); GetTree().Quit(failedTests == 0 ? 0 : 1); } private void Run(string name, Action test) { try { GameData.ResetRunState(); test(); passedTests++; GD.Print("[PASS] " + name); } catch (Exception exception) { failedTests++; GD.PrintErr("[FAIL] " + name + ": " + exception.Message); } } private void TestInventoryStacksAndRemovesItems() { ItemData stone = GameData.availableItems["stone"]; GameData.inventory.AddItem(new Item { data = stone }, stone.StackSize + 5); AssertEqual(stone.StackSize + 5, GameData.inventory.GetItemAmount("stone"), "stone amount"); AssertEqual(2, GameData.inventory.items.Count, "stone stack count"); bool removed = GameData.inventory.TryRemoveItem("stone", stone.StackSize); AssertTrue(removed, "remove should succeed"); AssertEqual(5, GameData.inventory.GetItemAmount("stone"), "remaining stone amount"); } private void TestInventoryCanCraftAcrossStacks() { ItemData stone = GameData.availableItems["stone"]; stone.StackSize = 5; GameData.inventory.AddItem(new Item { data = stone }, 8); List ingredients = new List { new Ingredient { Item = "stone", Amount = 8 } }; AssertTrue(GameData.inventory.CanCraft(ingredients, 1), "crafting should see both stacks"); } private void TestSurvivalConsumesFoodAndWater() { GameData.inventory.AddItem(new Item { data = GameData.availableItems["mushroom"] }, 1); GameData.inventory.AddItem(new Item { data = GameData.availableItems["water"] }, 1); GameData.survival.hunger = 30f; GameData.survival.thirst = 30f; GameData.survival.Update(0.1); AssertTrue(GameData.survival.hunger > 60f, "hunger should recover"); AssertTrue(GameData.survival.thirst > 65f, "thirst should recover"); AssertEqual(0, GameData.inventory.GetItemAmount("mushroom"), "mushroom consumed"); AssertEqual(0, GameData.inventory.GetItemAmount("water"), "water consumed"); } private void TestSurvivalDeathDisablesMovement() { GameData.canMove = true; GameData.survival.gracePeriodEnabled = false; GameData.survival.energy = 0.01f; GameData.survival.Update(2.0); AssertTrue(GameData.survival.isDead, "survival should be dead"); AssertFalse(GameData.canMove, "movement should be disabled"); } private void TestSurvivalGracePeriodPreventsEarlyDeath() { GameData.canMove = true; GameData.survival.hunger = 0f; GameData.survival.thirst = 0f; GameData.survival.energy = 0f; GameData.survival.Update(1.0); AssertFalse(GameData.survival.isDead, "survival should not fail during grace period"); AssertTrue(GameData.canMove, "movement should stay enabled during grace period"); AssertTrue(GameData.survival.hunger > 0f, "hunger should be clamped above zero"); AssertTrue(GameData.survival.thirst > 0f, "thirst should be clamped above zero"); AssertTrue(GameData.survival.energy > 0f, "energy should be clamped above zero"); } private void TestRobotResearchEffects() { RobotStats stats = new RobotStats(); List effects = new List { new ResearchEffect { Stat = "robot_speed_bonus", Value = 0.25f }, new ResearchEffect { Stat = "robot_energy_use_reduction", Value = 0.20f }, new ResearchEffect { Stat = "robot_cooling_bonus", Value = 0.50f } }; stats.Apply(effects); AssertClose(12.5f, stats.GetMovementSpeed(10f), 0.001f, "movement speed"); AssertClose(8f, stats.GetEnergyUse(10f), 0.001f, "energy use"); AssertClose(15f, stats.GetCoolingRate(10f), 0.001f, "cooling"); } private void TestResearchCompletionAppliesEffectsOnce() { ResearchData data = new ResearchData { Id = "test_research", Inputs = new List(), Research = "basics", CraftTime = 1.0, Texture = "", Effects = new List { new ResearchEffect { Stat = "robot_speed_bonus", Value = 0.10f } } }; Research research = new Research(data); research.Complete(); research.Complete(); AssertClose(11f, GameData.robotStats.GetMovementSpeed(10f), 0.001f, "research effect applied once"); } private void TestResourceSaveRoundtrip() { GameResource resource = new GameResource("stone"); resource.Extract(2.0); ResourceSaveData saveData = resource.CreateSaveData(); GameResource loadedResource = GameResource.FromSaveData(saveData); AssertEqual(saveData.Name, loadedResource.CreateSaveData().Name, "resource name"); AssertEqual(saveData.CurrentAmount, loadedResource.CreateSaveData().CurrentAmount, "resource amount"); } private void TestRobotSaveRoundtrip() { Robot robot = new Robot { Name = "Ada", Position = new Vector3(1f, 2f, 3f), heat = 44f, maintenance = 55f, isBroken = true, isCoolingDown = true, currentProgram = "Mining", currentMessage = "Needs care", robotType = "bronze_robot" }; RobotSaveData saveData = robot.CreateSaveData(); Robot loadedRobot = new Robot(); loadedRobot.LoadSaveData(saveData); AssertEqual("Ada", loadedRobot.Name.ToString(), "robot name"); AssertEqual("Mining", loadedRobot.currentProgram, "robot program"); AssertEqual("bronze_robot", loadedRobot.robotType, "robot type"); AssertClose(44f, loadedRobot.heat, 0.001f, "robot heat"); AssertClose(55f, loadedRobot.maintenance, 0.001f, "robot maintenance"); AssertTrue(loadedRobot.isBroken, "robot broken state"); AssertTrue(loadedRobot.isCoolingDown, "robot cooling state"); } private void TestSaveDataRestoresGlobalState() { GameData.inventory.AddItem(new Item { data = GameData.availableItems["stone"] }, 12); GameData.survival.hunger = 42f; GameData.survival.thirst = 43f; GameData.survival.energy = 44f; GameData.lowestLayer = 3; GameData.availableResearch["stoneage"].Complete(); SaveGameData saveData = SaveGameManager.CreateSaveData(); GameData.ResetRunState(); SaveGameManager.ApplyWorldData(saveData); AssertEqual(12, GameData.inventory.GetItemAmount("stone"), "saved inventory"); AssertClose(42f, GameData.survival.hunger, 0.001f, "saved hunger"); AssertEqual(3, GameData.lowestLayer, "saved layer"); AssertEqual(ResearchState.RESEARCHED, GameData.availableResearch["stoneage"].state, "saved research"); } private void TestSaveDataRestoresSurvivalTimer() { GameData.survival.elapsedSeconds = 321.5; SaveGameData saveData = SaveGameManager.CreateSaveData(); GameData.ResetRunState(); SaveGameManager.ApplyWorldData(saveData); AssertClose(321.5f, (float)GameData.survival.elapsedSeconds, 0.001f, "saved survival timer"); } private void TestResearchExecutionPaysResourcesAndFinishes() { GameData.inventory.AddItem(new Item { data = GameData.availableItems["stone"] }, 5); ResearchData researchData = new ResearchData { Id = "stone_counterweight", Inputs = new List { new Ingredient { Item = "stone", Amount = 3 } }, Research = "basics", CraftTime = 1.0, Texture = "", Effects = new List() }; Research research = new Research(researchData); ResearchResult result = research.Execute(1.0); AssertEqual(ResearchResult.FINISHED, result, "research result"); AssertEqual(2, GameData.inventory.GetItemAmount("stone"), "research cost"); AssertEqual(ResearchState.RESEARCHED, research.state, "research state"); } private void TestInventoryAddFailureKeepsInventoryUnchanged() { GameData.maxRobotCount = 1; GameData.inventory.maxInventorySize = 1; ItemData stone = GameData.availableItems["stone"]; bool result = GameData.inventory.AddItem(new Item { data = stone }, stone.StackSize + 1); AssertFalse(result, "add should fail"); AssertEqual(0, GameData.inventory.GetItemAmount("stone"), "failed add should be atomic"); AssertEqual(0, GameData.inventory.items.Count, "failed add should not create stacks"); } private void TestResearchCannotStartWithoutResources() { ResearchData researchData = new ResearchData { Id = "missing_stones", Inputs = new List { new Ingredient { Item = "stone", Amount = 3 } }, Research = "basics", CraftTime = 1.0, Texture = "", Effects = new List() }; Research research = new Research(researchData); ResearchResult result = research.Execute(1.0); AssertFalse(research.CanStart(), "research should not be startable"); AssertEqual(ResearchResult.FAILED, result, "research should fail without resources"); AssertEqual(ResearchState.UNDEFINED, research.state, "research state should stay unchanged"); } private void TestSplitSaveFilesRoundtrip() { GameData.inventory.AddItem(new Item { data = GameData.availableItems["water"] }, 7); GameData.survival.energy = 77f; SaveGameManager.SaveGame(); SaveGameData saveData = SaveGameManager.LoadSaveData(); AssertTrue(SaveGameManager.SaveExists(), "save folder should exist"); AssertTrue(FileAccess.FileExists("user://savegame/gamedata.json"), "gamedata file"); AssertTrue(FileAccess.FileExists("user://savegame/robots.json"), "robots file"); AssertTrue(FileAccess.FileExists("user://savegame/research.json"), "research file"); AssertEqual(7, saveData.Inventory[0].Amount, "saved file inventory"); AssertClose(77f, saveData.Survival.Energy, 0.001f, "saved file energy"); } private void TestSplitSaveFilesIncludeLayerFiles() { GameData.ruinSize = 2; GameData.map = new Layer[2]; GameData.map[0] = CreateTestLayer(0, "spawn"); GameData.map[1] = CreateTestLayer(1, "gate"); SaveGameManager.SaveGame(); SaveGameData saveData = SaveGameManager.LoadSaveData(); AssertTrue(FileAccess.FileExists("user://savegame/layer_0.json"), "layer 0 file"); AssertTrue(FileAccess.FileExists("user://savegame/layer_1.json"), "layer 1 file"); AssertEqual(2, saveData.Layers.Count, "loaded layer count"); AssertEqual("spawn", saveData.Layers[0].Tiles[0].CollapsedMesh, "layer 0 tile"); AssertEqual("gate", saveData.Layers[1].Tiles[0].CollapsedMesh, "layer 1 tile"); } private void TestIfNodeEvaluatesInventoryComparisons() { GameData.inventory.AddItem(new Item { data = GameData.availableItems["stone"] }, 5); IfNode node = new IfNode { selectedItem = new Item { data = GameData.availableItems["stone"] }, amount = 3, comparator = "is bigger than" }; AssertEqual(NodeResult.SUCCESS, node.Execute(null, 0), "if condition true"); node.amount = 8; AssertEqual(NodeResult.CONDITIONFALSE, node.Execute(null, 0), "if condition false"); } private void TestWhileNodeReportsFalseConditions() { GameData.inventory.AddItem(new Item { data = GameData.availableItems["water"] }, 2); WhileNode node = new WhileNode { selectedItem = new Item { data = GameData.availableItems["water"] }, amount = 5, comparator = "is bigger than or equal to" }; AssertEqual(NodeResult.CONDITIONFALSE, node.Execute(null, 0), "while condition false"); } private void TestForNodeStopsAfterConfiguredAmount() { ForNode node = new ForNode { amount = 2 }; AssertEqual(NodeResult.SUCCESS, node.Execute(null, 0), "first iteration"); AssertEqual(NodeResult.SUCCESS, node.Execute(null, 0), "second iteration"); AssertEqual(NodeResult.CONDITIONFALSE, node.Execute(null, 0), "loop finished"); } private void TestItemDataReadableNames() { AssertEqual("Iron gear", ItemData.GetReadableName("iron_gear"), "readable name"); AssertEqual("iron_gear", ItemData.GetIndex("Iron Gear"), "index name"); } private void TestResourceFilesLoadCoreData() { AssertTrue(GameData.availableItems.ContainsKey("stone"), "stone item loaded"); AssertTrue(GameData.availableItems.ContainsKey("water"), "water item loaded"); AssertTrue(GameData.availableResearch.ContainsKey("basics"), "basics research loaded"); AssertTrue(GameData.availableResearch.ContainsKey("iron_robotics"), "iron robotics research loaded"); } private Layer CreateTestLayer(int level, string collapsedMesh) { Layer layer = new Layer { level = level, currentResources = new List { "stone" }, gateIngredients = new List(), tiles = new Tile[1, 1] }; Tile tile = new Tile { GridPosition = new Vector2I(0, 0), Position = Vector3.Zero, collapsedMesh = collapsedMesh, containsResource = true, resource = new GameResource("stone") }; layer.tiles[0, 0] = tile; return layer; } private void AssertTrue(bool value, string message) { if (!value) { throw new Exception(message); } } private void AssertFalse(bool value, string message) { if (value) { throw new Exception(message); } } private void AssertEqual(T expected, T actual, string message) { if (!EqualityComparer.Default.Equals(expected, actual)) { throw new Exception($"{message}: expected {expected}, got {actual}"); } } private void AssertClose(float expected, float actual, float tolerance, string message) { if (Math.Abs(expected - actual) > tolerance) { throw new Exception($"{message}: expected {expected}, got {actual}"); } } }