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("Saved scripts can be deleted", TestSavedScriptsCanBeDeleted); Run("Resource extraction and save data roundtrip", TestResourceSaveRoundtrip); Run("Endless resources extract slower", TestEndlessResourcesExtractSlower); Run("Robot save data roundtrip keeps robot state", TestRobotSaveRoundtrip); Run("Maintain node consumes matching gear", TestMaintainNodeConsumesMatchingGear); Run("Sacrifice node makes resource endless", TestSacrificeNodeMakesResourceEndless); Run("No robot recovery detects loss", TestNoRobotRecoveryDetectsLoss); Run("Robot item prevents no robot loss", TestRobotItemPreventsNoRobotLoss); Run("Save data captures and restores options", TestSaveDataRestoresOptions); 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("Start node succeeds immediately", TestStartNodeSucceedsImmediately); Run("Split node connections restore both branches", TestSplitNodeConnectionsRestoreBothBranches); Run("Linear node connection restores next node", TestLinearNodeConnectionRestoresNextNode); Run("Paused world does not drain survival", TestPausedWorldDoesNotDrainSurvival); Run("Open gate hides gate content", TestOpenGateHidesGateContent); Run("Layer generation succeeds above threshold", TestLayerGenerationSuccessRate); 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 TestEndlessResourcesExtractSlower() { GameResource resource = new GameResource("stone"); AssertTrue(resource.Extract(1.0), "normal resource should extract after one second"); resource.MakeEndless(); AssertFalse(resource.Extract(1.0), "endless resource should not extract after one second"); AssertTrue(resource.Extract(3.0), "endless resource should extract after four total seconds"); AssertTrue(resource.IsEndless(), "resource should be endless"); AssertTrue(resource.GetExtractionSpeed() > 1f, "endless extraction speed should be slower"); } 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 TestMaintainNodeConsumesMatchingGear() { Robot robot = new Robot { robotType = "copper_robot", maintenance = 50f, isBroken = true }; GameData.inventory.AddItem(new Item { data = GameData.availableItems["copper_gear"] }, 1); MaintainNode node = new MaintainNode(); AssertEqual(NodeResult.SUCCESS, node.Execute(robot, 0), "maintain should succeed"); AssertClose(60f, robot.maintenance, 0.001f, "maintenance repaired by 10"); AssertFalse(robot.isBroken, "robot should be usable again"); AssertEqual(0, GameData.inventory.GetItemAmount("copper_gear"), "gear should be consumed"); } private void TestSacrificeNodeMakesResourceEndless() { GameData.ruinSize = 1; GameData.layerSize = 1; GameData.map = new Layer[1]; GameData.map[0] = CreateTestLayer(0, "spawn"); GameData.map[0].tiles[0, 0].resource = new GameResource("stone"); GameData.map[0].tiles[0, 0].containsResource = true; Pathfinding.BuildAStarGraph(); Robot robot = new Robot { Position = Vector3.Zero }; GameData.robots.Add(robot); SacrificeNode node = new SacrificeNode(); AssertEqual(NodeResult.SUCCESS, node.Execute(robot, 0), "sacrifice should succeed"); AssertTrue(GameData.map[0].tiles[0, 0].resource.IsEndless(), "resource should become endless"); AssertEqual(0, GameData.robots.Count, "robot should be removed"); } private void TestNoRobotRecoveryDetectsLoss() { AssertTrue(GameData.HasNoRobotRecovery(), "no robots and no robot items should be a loss"); } private void TestRobotItemPreventsNoRobotLoss() { GameData.inventory.AddItem(new Item { data = GameData.availableItems["stone_robot"] }, 1); AssertFalse(GameData.HasNoRobotRecovery(), "robot item should prevent loss"); } 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 TestSaveDataRestoresOptions() { GameData.screenMode = 1; GameData.soundVolume = 0.35f; GameData.lightColor = new Color(0.2f, 0.4f, 0.6f, 1f); SaveGameData saveData = SaveGameManager.CreateSaveData(); GameData.screenMode = 2; GameData.soundVolume = 0.8f; GameData.lightColor = Colors.White; SaveGameManager.ApplyWorldData(saveData); AssertEqual(1, GameData.screenMode, "saved screen mode"); AssertClose(0.35f, GameData.soundVolume, 0.001f, "saved sound volume"); AssertClose(0.2f, GameData.lightColor.R, 0.001f, "saved light red"); AssertClose(0.4f, GameData.lightColor.G, 0.001f, "saved light green"); AssertClose(0.6f, GameData.lightColor.B, 0.001f, "saved light blue"); } 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 TestSavedScriptsCanBeDeleted() { string scriptName = "delete_test_script"; FileHandler.SaveProgram(scriptName, "{\"Nodes\":[],\"Connections\":[]}"); AssertTrue(FileHandler.LoadProgramNames().Contains(scriptName), "script should be listed"); AssertTrue(FileHandler.DeleteProgram(scriptName), "delete should succeed"); AssertEqual("", FileHandler.LoadProgram(scriptName), "deleted script should be empty"); AssertFalse(FileHandler.DeleteProgram(scriptName), "second delete should fail"); } 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 TestStartNodeSucceedsImmediately() { StartNode node = new StartNode(); AssertEqual(NodeResult.SUCCESS, node.Execute(null, 0), "start should succeed"); AssertEqual("Name: Start", node.Save(), "start save"); } private void TestSplitNodeConnectionsRestoreBothBranches() { MoveNode successNode = new MoveNode(); MoveNode negativeNode = new MoveNode(); System.Collections.Generic.Dictionary availableNodes = new System.Collections.Generic.Dictionary { { new StringName("success"), successNode }, { new StringName("negative"), negativeNode } }; List connections = new List { CreateConnection("split", 0, "success", 0), CreateConnection("split", 1, "negative", 0) }; IfNode ifNode = new IfNode(); ifNode.SetNextNode(connections, availableNodes); AssertTrue(ReferenceEquals(successNode, ifNode.nextNode), "if success branch"); AssertTrue(ReferenceEquals(negativeNode, ifNode.NegativeNode), "if negative branch"); WhileNode whileNode = new WhileNode(); whileNode.SetNextNode(connections, availableNodes); AssertTrue(ReferenceEquals(successNode, whileNode.nextNode), "while success branch"); AssertTrue(ReferenceEquals(negativeNode, whileNode.NegativeNode), "while negative branch"); ForNode forNode = new ForNode(); forNode.SetNextNode(connections, availableNodes); AssertTrue(ReferenceEquals(successNode, forNode.nextNode), "for success branch"); AssertTrue(ReferenceEquals(negativeNode, forNode.NegativeNode), "for negative branch"); } private void TestLinearNodeConnectionRestoresNextNode() { MoveNode targetNode = new MoveNode(); StartNode startNode = new StartNode(); System.Collections.Generic.Dictionary availableNodes = new System.Collections.Generic.Dictionary { { new StringName("target"), targetNode } }; List connections = new List { CreateConnection("start", 0, "target", 0) }; startNode.SetNextNode(connections, availableNodes); AssertTrue(ReferenceEquals(targetNode, startNode.nextNode), "linear next node"); AssertTrue(startNode.NegativeNode == null, "linear node should not have negative branch"); } private void TestPausedWorldDoesNotDrainSurvival() { GameData.isPaused = true; GameData.canMove = false; GameData.survival.energy = 100f; World world = new World(); world.UpdateGameLoop(100.0); AssertClose(100f, GameData.survival.energy, 0.001f, "energy should not drain while paused"); } private void TestOpenGateHidesGateContent() { Layer layer = CreateTestLayer(0, "gate"); layer.gateCoordinate = new Vector2I(0, 0); layer.tiles[0, 0].ContentNode = new Node3D { Visible = true }; layer.OpenGate(); AssertTrue(layer.isGateOpen, "gate should be open"); AssertFalse(layer.tiles[0, 0].ContentNode.Visible, "gate content should be hidden"); } private void TestLayerGenerationSuccessRate() { const int layerCount = 1000; const float minimumSuccessRate = 0.90f; GameData.layerSize = 20; WFC.FillAdjacencies(); Dictionary tileMeshes = ResourceLoader.LoadTiles(); int successfulLayers = 0; for (int i = 0; i < layerCount; i++) { Layer layer = new Layer(); layer._Ready(); layer.SetupLayer(GameData.layerSize, 0, tileMeshes, new Vector2I()); if (IsGeneratedLayerValid(layer)) { successfulLayers++; } layer.Free(); } float successRate = successfulLayers / (float)layerCount; GD.Print($"Layer generation success rate: {successfulLayers}/{layerCount} ({successRate:P2})"); AssertTrue( successRate >= minimumSuccessRate, $"layer generation success rate should be at least {minimumSuccessRate:P0}, got {successRate:P2}" ); } 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"); Dictionary dslNodes = ResourceLoader.LoadDSLNodes(); bool hasStartNode = false; foreach (ProgramNode node in dslNodes.Keys) { if (node is StartNode) { hasStartNode = true; break; } } AssertTrue(hasStartNode, "start node prefab loaded"); } }