Files
factorio-calc/tests/FactorioCalc.Tests/SolverTests.cs
T
mikhail eae7c066d1 feat: Factorio Space Age Production Calculator
- Domain: immutable entities (Resource, Recipe, Machine, Module, etc.)
- Data: JSON recipe repository (34 resources, 26 recipes, Space Age)
- Solver: DFS-based production planner with module support
- Reporting: Console, JSON, Excel exporters
- CLI: solve/list commands with Spectre.Console
- Tests: 20 unit tests (xUnit) all passing
- Tech: C# .NET 10, MathNet.Numerics, ClosedXML

Model: GPT-4.1 (OpenAI) — executed by pi coding agent
2026-07-02 13:08:25 +03:00

220 lines
7.7 KiB
C#

using FactorioCalc.Domain;
using FactorioCalc.Solver;
using Xunit;
namespace FactorioCalc.Tests;
public class SolverTests
{
private IRecipeRepository CreateRepository()
{
var resources = new Dictionary<int, Resource>
{
{ 1, new Resource(1, "Iron Ore") },
{ 7, new Resource(7, "Iron Plate") },
{ 9, new Resource(9, "Steel Plate") },
{ 5, new Resource(5, "Coal") },
{ 8, new Resource(8, "Copper Plate") },
{ 2, new Resource(2, "Copper Ore") },
{ 10, new Resource(10, "Copper Cable") },
{ 11, new Resource(11, "Electronic Circuit") },
};
var machines = new Dictionary<int, Machine>
{
{ 3, new Machine(3, "Smelter", 0.5, 3.0, 2, new[] { "smelting" }) },
{ 5, new Machine(5, "Assembler 1", 0.5, 3.0, 2, new[] { "basic-crafting" }) },
{ 6, new Machine(6, "Assembler 2", 0.5, 4.0, 2, new[] { "basic-crafting" }) },
};
// Iron Plate: 1 Iron Ore -> 1 Iron Plate (3s)
// Steel Plate: 2 Iron Plate + 1 Coal -> 1 Steel Plate (5s)
// Copper Cable: 1 Copper Plate -> 5 Copper Cable (0.5s)
// Electronic Circuit: 2 Copper Cable + 1 Iron Plate -> 1 Electronic Circuit (3s)
var recipes = new Dictionary<int, Recipe>
{
{
1, new Recipe(1, "Iron Plate", "smelting", 3.0, "smelting",
new[] { new Ingredient(1, 1) },
new[] { new Product(7, 1) })
},
{
2, new Recipe(2, "Steel Plate", "smelting", 5.0, "smelting",
new[] { new Ingredient(7, 2), new Ingredient(5, 1) },
new[] { new Product(9, 1) })
},
{
3, new Recipe(3, "Copper Cable", "basic-crafting", 0.5, "basic-crafting",
new[] { new Ingredient(8, 1) },
new[] { new Product(10, 5) })
},
{
4, new Recipe(4, "Electronic Circuit", "basic-crafting", 3.0, "basic-crafting",
new[] { new Ingredient(10, 2), new Ingredient(7, 1) },
new[] { new Product(11, 1) })
},
};
var modules = new Dictionary<int, Module>();
return new TestRepository(recipes, resources, machines, modules);
}
[Fact]
public void Solve_SingleTarget_ReturnsExecution()
{
var repo = CreateRepository();
var solver = new ProductionSolver(repo);
// Target: 10 Iron Plate/sec
var targets = new[] { new ProductionTarget(7, 10) };
var result = solver.Solve(targets);
Assert.Single(result.Executions);
var exec = result.Executions.First();
Assert.Equal(1, exec.RecipeId); // Iron Plate recipe
Assert.Equal(3, exec.MachineId); // Smelter
}
[Fact]
public void Solve_ChainedRecipe_ResolveDependencies()
{
var repo = CreateRepository();
var solver = new ProductionSolver(repo);
// Target: 2 Steel Plate/sec -> needs Iron Plate -> needs Iron Ore
var targets = new[] { new ProductionTarget(9, 2) };
var result = solver.Solve(targets);
// Should have Steel Plate + Iron Plate executions
Assert.Equal(2, result.Executions.Count);
var steelExec = result.Executions.First(e => e.RecipeId == 2);
Assert.NotNull(steelExec);
}
[Fact]
public void Solve_ElectronicCircuit_FullChain()
{
var repo = CreateRepository();
var solver = new ProductionSolver(repo);
// Target: 10 Electronic Circuit/sec -> needs Copper Cable + Iron Plate
var targets = new[] { new ProductionTarget(11, 10) };
var result = solver.Solve(targets);
// Should have EC + Copper Cable + Iron Plate executions
Assert.Equal(3, result.Executions.Count);
}
[Fact]
public void Solve_WithSpeedModules_IncreasesMachineCount()
{
var repo = CreateRepository();
var solver = new ProductionSolver(repo);
var speedModule = new Module(1, "Speed Module 1", ModuleType.Speed, 0.10, 0, -0.05);
var targets = new[] { new ProductionTarget(7, 10) };
var resultNoModules = solver.Solve(targets);
var resultWithModules = solver.SolveWithModules(targets, new[] { speedModule });
// With speed modules, the rate per machine increases, so we might need fewer machines
// or the same number with higher throughput
Assert.NotEmpty(resultWithModules.Executions);
}
[Fact]
public void Solve_WithProductivityModules_PrefersProductivityRecipe()
{
// This test verifies that when productivity modules are present,
// the solver prefers productivity recipes if available
var resources = new Dictionary<int, Resource>
{
{ 1, new Resource(1, "Iron Ore") },
{ 7, new Resource(7, "Iron Plate") },
};
var machines = new Dictionary<int, Machine>
{
{ 3, new Machine(3, "Smelter", 0.5, 3.0, 2, new[] { "smelting" }) },
};
var prodModule = new Module(4, "Productivity Module 1", ModuleType.Productivity, -0.10, 0.10, -0.05);
var recipes = new Dictionary<int, Recipe>
{
{
1, new Recipe(1, "Iron Plate", "smelting", 3.0, "smelting",
new[] { new Ingredient(1, 1) },
new[] { new Product(7, 1) })
},
{
2, new Recipe(2, "Iron Plate (Productivity)", "smelting", 4.0, "smelting",
new[] { new Ingredient(1, 2) },
new[] { new Product(7, 2) })
},
};
var modules = new Dictionary<int, Module> { { 4, prodModule } };
var repo = new TestRepository(recipes, resources, machines, modules);
var solver = new ProductionSolver(repo);
var targets = new[] { new ProductionTarget(7, 10) };
var result = solver.SolveWithModules(targets, new[] { prodModule });
// Should prefer the productivity recipe
var exec = result.Executions.First();
Assert.Equal(2, exec.RecipeId); // Productivity recipe
}
[Fact]
public void Solve_RawResource_NoExecution()
{
var repo = CreateRepository();
var solver = new ProductionSolver(repo);
// Iron Ore has no recipe — it's a raw resource
var targets = new[] { new ProductionTarget(1, 100) };
var result = solver.Solve(targets);
Assert.Empty(result.Executions);
}
[Fact]
public void Solve_RequiredInputs_ContainsRawResources()
{
var repo = CreateRepository();
var solver = new ProductionSolver(repo);
// Steel Plate needs Iron Ore and Coal (both raw)
var targets = new[] { new ProductionTarget(9, 1) };
var result = solver.Solve(targets);
// Iron Ore (id=1) and Coal (id=5) should be in required inputs
Assert.Contains(1, result.RequiredInputs.Keys);
Assert.Contains(5, result.RequiredInputs.Keys);
}
}
/// <summary>
/// In-memory test repository.
/// </summary>
public sealed class TestRepository : IRecipeRepository
{
public IReadOnlyDictionary<int, Recipe> Recipes { get; }
public IReadOnlyDictionary<int, Resource> Resources { get; }
public IReadOnlyDictionary<int, Machine> Machines { get; }
public IReadOnlyDictionary<int, Module> Modules { get; }
public TestRepository(
IReadOnlyDictionary<int, Recipe> recipes,
IReadOnlyDictionary<int, Resource> resources,
IReadOnlyDictionary<int, Machine> machines,
IReadOnlyDictionary<int, Module> modules)
{
Recipes = recipes;
Resources = resources;
Machines = machines;
Modules = modules;
}
}