Economy tests: role inputs persist, are read by every rule as any filler, offered recipe-less designs demand their roles
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3fb5e0e0e6
commit
ccd9792d2f
5 changed files with 179 additions and 3 deletions
|
|
@ -53,6 +53,10 @@ public sealed class EconomyChecksTests
|
|||
yield return Case("upkeep that is not a commodity", f => f.EditRecipe("commodity:plate", r => r.Upkeep.Add(new CommodityCount { Commodity = EconomyFixture.Item("hull:skiff"), Count = 1 })), "recipe:commodity:plate");
|
||||
yield return Case("input naming nothing", f => f.EditRecipe("commodity:plate", r => r.Inputs.Add(new CommodityCount { Commodity = EconomyFixture.Item("commodity:ghost"), Count = 1 })), "recipe:commodity:plate");
|
||||
yield return Case("inputs on a role recipe", f => f.EditRecipe("gear:laser", r => r.Inputs.Add(new CommodityCount { Commodity = EconomyFixture.Item(Ore), Count = 1 })), "recipe:gear:laser");
|
||||
yield return Case("role input count zero", f => f.EditRecipe("commodity:plate", r => r.RoleInputs.Add(new ItemRole { Name = "Plate", Count = 0 })), "recipe:commodity:plate", says: "Role 'Plate' count");
|
||||
yield return Case("role input with no filler", f => f.EditRecipe("commodity:plate", r => r.RoleInputs.Add(new ItemRole { Name = "Mystery", Count = 1 })), "recipe:commodity:plate", says: "Role 'Mystery' has no commodity");
|
||||
yield return Case("role input with an empty name", f => f.EditRecipe("commodity:plate", r => r.RoleInputs.Add(new ItemRole { Name = " ", Count = 1 })), "recipe:commodity:plate", says: "empty name");
|
||||
yield return Case("role inputs on a role recipe", f => f.EditRecipe("gear:laser", r => r.RoleInputs.Add(new ItemRole { Name = "Plate", Count = 1 })), "recipe:gear:laser", says: "no RoleInputs");
|
||||
yield return Case("a role with no filler", f => f.EditItem<GearData>("gear:laser", g => g.Roles.Add(new ItemRole { Name = "Mystery" })), "recipe:gear:laser");
|
||||
yield return Case("a role count of zero", f => f.EditItem<GearData>("gear:laser", g => g.Roles[0].Count = 0), "recipe:gear:laser");
|
||||
yield return Case("fills on a design", f => f.EditRecipe("gear:laser", r => r.Fills.Add("Plate")), "recipe:gear:laser");
|
||||
|
|
|
|||
|
|
@ -254,6 +254,12 @@ public sealed class EconomyCommandTests
|
|||
Assert.Contains("commodity:ore <- nothing | Mill, cycle 1, workforce 0 | a good", text);
|
||||
Assert.Contains("commodity:plate <- 2 x commodity:ore | Mill, cycle 1, workforce 0 | a good", text);
|
||||
Assert.Contains("gear:laser <- 2 x Plate | Mill", text);
|
||||
f.Good("ore2", fills: new[] { "OreRole" });
|
||||
f.Good("steel", roleInputs: new[] { ("OreRole", 2) });
|
||||
var withRole = new StringWriter();
|
||||
EconomyCommand.Report(f.Cache, withRole);
|
||||
Assert.Contains("commodity:steel <- 2 x OreRole | Mill", withRole.ToString());
|
||||
Assert.Contains("gear:laser <- 2 x Plate | Mill", withRole.ToString());
|
||||
Assert.True(text.IndexOf("commodity:ore <-", StringComparison.Ordinal) < text.IndexOf("commodity:plate <-", StringComparison.Ordinal));
|
||||
Assert.True(text.IndexOf("commodity:plate <-", StringComparison.Ordinal) < text.IndexOf("gear:laser <-", StringComparison.Ordinal));
|
||||
f.EditRecipe("commodity:plate", r => r.LoreHook = "");
|
||||
|
|
|
|||
|
|
@ -52,9 +52,11 @@ public sealed class EconomyFixture : IDisposable
|
|||
private void Sell(CultRecordKey design) =>
|
||||
Cache.Upsert(new FactionProductData { Name = design.Value, Design = new CultRecordRef<CraftedItemData>(design), Manufacturer = Maker });
|
||||
|
||||
// A commodity and its recipe. Roles make it a consumer good (a role recipe); Inputs make it a compound commodity.
|
||||
// A commodity and its recipe. Roles make it a consumer good (a role recipe); Inputs make it a compound commodity; roleInputs are
|
||||
// the roles its recipe takes, any filler will do.
|
||||
public string Good(string id, (string Id, int Count)[] inputs = null, string[] fills = null, int outputCount = 1, float cycle = 1,
|
||||
int workforce = 0, (string Id, int Count)[] upkeep = null, string lore = "a good", (string Role, int Count)[] roles = null, CultRecordRef<StationKindData>? kind = null)
|
||||
int workforce = 0, (string Id, int Count)[] upkeep = null, string lore = "a good", (string Role, int Count)[] roles = null, CultRecordRef<StationKindData>? kind = null,
|
||||
(string Role, int Count)[] roleInputs = null)
|
||||
{
|
||||
var key = new CultRecordKey(Key(id));
|
||||
var roleList = (roles ?? Array.Empty<(string, int)>()).Select(r => new ItemRole { Name = r.Role, Count = r.Count }).ToList();
|
||||
|
|
@ -70,7 +72,8 @@ public sealed class EconomyFixture : IDisposable
|
|||
Upkeep = (upkeep ?? Array.Empty<(string, int)>()).Select(i => new CommodityCount { Commodity = Item(Key(i.Id)), Count = i.Count }).ToList(),
|
||||
StationKind = kind ?? Mill,
|
||||
LoreHook = lore,
|
||||
Fills = (fills ?? Array.Empty<string>()).ToList()
|
||||
Fills = (fills ?? Array.Empty<string>()).ToList(),
|
||||
RoleInputs = (roleInputs ?? Array.Empty<(string, int)>()).Select(r => new ItemRole { Name = r.Role, Count = r.Count }).ToList()
|
||||
}, RecipeData.KeyOf(key));
|
||||
return key.Value;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,12 +12,14 @@ public sealed class EconomyNullGuardTests
|
|||
if (list == "Fills") r.Fills = null;
|
||||
if (list == "Inputs") r.Inputs = null;
|
||||
if (list == "Upkeep") r.Upkeep = null;
|
||||
if (list == "RoleInputs") r.RoleInputs = null;
|
||||
});
|
||||
|
||||
[Theory]
|
||||
[InlineData("Fills")]
|
||||
[InlineData("Inputs")]
|
||||
[InlineData("Upkeep")]
|
||||
[InlineData("RoleInputs")]
|
||||
public void ANullListIsAnLRecipeFaultNotAThrow(string list)
|
||||
{
|
||||
using var f = EconomyFixture.Healthy();
|
||||
|
|
@ -30,6 +32,7 @@ public sealed class EconomyNullGuardTests
|
|||
[InlineData("Fills")]
|
||||
[InlineData("Inputs")]
|
||||
[InlineData("Upkeep")]
|
||||
[InlineData("RoleInputs")]
|
||||
public void EveryRuleSurvivesANullList(string list)
|
||||
{
|
||||
using var f = EconomyFixture.Healthy();
|
||||
|
|
@ -49,5 +52,9 @@ public sealed class EconomyNullGuardTests
|
|||
f.EditRecipe("commodity:plate", r => r.Inputs.Add(null));
|
||||
var report = EconomyChecks.Check(f.Cache);
|
||||
Assert.Contains(report.Faults, x => x.Invariant == EconomyChecks.LRecipe && x.Record == "recipe:commodity:plate" && x.Message.Contains("Inputs"));
|
||||
|
||||
f.EditRecipe("commodity:plate", r => { r.Inputs.Remove(null); r.RoleInputs.Add(null); });
|
||||
report = EconomyChecks.Check(f.Cache);
|
||||
Assert.Contains(report.Faults, x => x.Invariant == EconomyChecks.LRecipe && x.Record == "recipe:commodity:plate" && x.Message.Contains("RoleInputs"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
156
tests/Aetheria.Shared.Tests/RecipeRoleInputTests.cs
Normal file
156
tests/Aetheria.Shared.Tests/RecipeRoleInputTests.cs
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using GameCult.Caching;
|
||||
using Xunit;
|
||||
|
||||
// A commodity's recipe takes roles as well as named commodities (aetheria-living-world, cut recipe-role-inputs): RecipeData.RoleInputs,
|
||||
// the one reader of role slots (Recipes.RoleInputsOf), and the checks reading a role input as any commodity that fills it.
|
||||
public sealed class RecipeRoleInputTests
|
||||
{
|
||||
private static string Iron => EconomyFixture.Key("iron");
|
||||
|
||||
// hematite and ilmenite are raw ores filling iron-ore (ilmenite also titanium-ore); iron takes 2 iron-ore.
|
||||
private static EconomyFixture Smelter()
|
||||
{
|
||||
var f = new EconomyFixture();
|
||||
f.Good("hematite", fills: new[] { "iron-ore" });
|
||||
f.Good("ilmenite", fills: new[] { "iron-ore", "titanium-ore" });
|
||||
f.Good("iron", roleInputs: new[] { ("iron-ore", 2) }, fills: new[] { "Plate" });
|
||||
f.Gear("laser", new[] { ("Plate", 2) });
|
||||
return f;
|
||||
}
|
||||
|
||||
private static string[] Records(System.Collections.Generic.IEnumerable<EconomyFault> faults, string invariant) =>
|
||||
faults.Where(x => x.Invariant == invariant).Select(x => x.Record).ToArray();
|
||||
|
||||
[Fact]
|
||||
public void ShippedRecipesReadAnEmptyRoleInputList()
|
||||
{
|
||||
using var cache = AetheriaStores.Open(TestCatalog.Repo, registry: TestCatalog.Registry());
|
||||
var recipes = cache.GetAll<RecipeData>().ToArray();
|
||||
Assert.NotEmpty(recipes);
|
||||
Assert.All(recipes, r => { Assert.NotNull(r.RoleInputs); Assert.Empty(r.RoleInputs); });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ARoleInputSurvivesReopen()
|
||||
{
|
||||
using var f = Smelter();
|
||||
f.Reopen();
|
||||
var role = Assert.Single(f.Cache.Get<RecipeData>(RecipeData.KeyOf(new CultRecordKey(Iron))).RoleInputs);
|
||||
Assert.Equal(("iron-ore", 2), (role.Name, role.Count));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoleInputsOfReadsTheDesignOrTheRecipe()
|
||||
{
|
||||
using var f = Smelter();
|
||||
var laser = Assert.Single(Recipes.RoleInputsOf(f.Cache, f.Cache.Get<RecipeData>(RecipeData.KeyOf(new CultRecordKey("gear:laser")))));
|
||||
Assert.Equal(("Plate", 2), (laser.Name, laser.Count));
|
||||
var ironRecipe = f.Cache.Get<RecipeData>(RecipeData.KeyOf(new CultRecordKey(Iron)));
|
||||
var ore = Assert.Single(Recipes.RoleInputsOf(f.Cache, ironRecipe));
|
||||
Assert.Equal(("iron-ore", 2), (ore.Name, ore.Count));
|
||||
Assert.False(Recipes.IsRaw(f.Cache, ironRecipe));
|
||||
Assert.True(Recipes.IsRaw(f.Cache, f.Cache.Get<RecipeData>(RecipeData.KeyOf(new CultRecordKey(EconomyFixture.Key("hematite"))))));
|
||||
Assert.False(Recipes.IsConsumerGood(f.Cache, ironRecipe));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DepthReadsARoleInputAsItsShallowestFiller()
|
||||
{
|
||||
using var f = new EconomyFixture();
|
||||
f.Good("hematite", fills: new[] { "iron-ore" });
|
||||
f.Good("grit");
|
||||
f.Good("pulp", inputs: new[] { ("grit", 1) });
|
||||
f.Good("sludge", inputs: new[] { ("pulp", 1) }, fills: new[] { "iron-ore" });
|
||||
f.Good("iron", roleInputs: new[] { ("iron-ore", 1) });
|
||||
Assert.Equal(1, EconomyChecks.Deep(f.Cache)[Iron]);
|
||||
f.EditRecipe(EconomyFixture.Key("hematite"), r => r.Fills.Clear());
|
||||
Assert.Equal(3, EconomyChecks.Deep(f.Cache)[Iron]);
|
||||
f.EditRecipe(EconomyFixture.Key("sludge"), r => r.Fills.Clear());
|
||||
Assert.Null(EconomyChecks.Deep(f.Cache)[Iron]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SourceComesThroughAnyFillerOfARoleInput()
|
||||
{
|
||||
using var f = new EconomyFixture();
|
||||
f.Good("hematite", fills: new[] { "iron-ore" });
|
||||
f.Good("loopa", inputs: new[] { ("loopb", 1) }, fills: new[] { "iron-ore" });
|
||||
f.Good("loopb", inputs: new[] { ("loopa", 1) });
|
||||
f.Good("iron", roleInputs: new[] { ("iron-ore", 1) });
|
||||
bool NoSource() => EconomyChecks.SourceSink(f.Cache).Any(x => x.Record == "recipe:" + Iron && x.Message.Contains("no source"));
|
||||
Assert.False(NoSource());
|
||||
f.EditRecipe(EconomyFixture.Key("hematite"), r => r.Fills.Clear());
|
||||
Assert.True(NoSource());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AGoodFillingOnlyARoleInputHasASink()
|
||||
{
|
||||
using var f = Smelter();
|
||||
f.Good("titanium", roleInputs: new[] { ("titanium-ore", 1) });
|
||||
bool NoSink() => EconomyChecks.SourceSink(f.Cache).Any(x => x.Record == "recipe:" + EconomyFixture.Key("ilmenite") && x.Message.Contains("no sink"));
|
||||
Assert.False(NoSink());
|
||||
f.EditRecipe(Iron, r => r.RoleInputs.Clear());
|
||||
f.EditRecipe(EconomyFixture.Key("titanium"), r => r.RoleInputs.Clear());
|
||||
Assert.True(NoSink());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProductiveReadsTheWorstFillerOfARoleInput()
|
||||
{
|
||||
using var f = new EconomyFixture();
|
||||
f.Good("hematite", fills: new[] { "iron-ore" });
|
||||
f.Good("iron", roleInputs: new[] { ("iron-ore", 2) });
|
||||
Assert.Empty(EconomyChecks.Productive(f.Cache));
|
||||
f.Good("scrap", inputs: new[] { ("iron", 2) }, fills: new[] { "iron-ore" });
|
||||
var fault = Assert.Single(EconomyChecks.Productive(f.Cache));
|
||||
Assert.Contains("commodity:iron", fault.Message);
|
||||
Assert.Contains("commodity:scrap", fault.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConsumerDemandWalksThroughARoleInput()
|
||||
{
|
||||
using var f = new EconomyFixture();
|
||||
f.Good("rawx");
|
||||
f.Good("acid", inputs: new[] { ("rawx", 1) }, fills: new[] { "reagent" });
|
||||
f.Good("g1", roleInputs: new[] { ("reagent", 1) }, fills: new[] { "Barrel" });
|
||||
f.Good("g2", roleInputs: new[] { ("reagent", 1) }, fills: new[] { "Plate" });
|
||||
f.Gear("cannon", new[] { ("Barrel", 1) });
|
||||
f.Hull("tug", HullType.Ship, new[] { ("Plate", 1) });
|
||||
Assert.Equal(new[] { "recipe:" + EconomyFixture.Key("acid") }, Records(EconomyChecks.ConsumerDemand(f.Cache), EconomyChecks.LDemand));
|
||||
f.Good("toaster", roles: new[] { ("Barrel", 1) });
|
||||
Assert.Empty(EconomyChecks.ConsumerDemand(f.Cache));
|
||||
}
|
||||
|
||||
private static EconomyFixture Casings(Action<EconomyFixture> design)
|
||||
{
|
||||
var f = EconomyFixture.Healthy();
|
||||
f.Good("cap", inputs: new[] { ("ore", 1) }, fills: new[] { "Casing" });
|
||||
f.Good("alloy", inputs: new[] { ("ore", 1) }, fills: new[] { "Casing", "Plate" });
|
||||
design(f);
|
||||
return f;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnOfferedDesignWithoutARecipeStillNamesItsRoles()
|
||||
{
|
||||
using (var offered = Casings(f => f.Gear("shell", new[] { ("Casing", 1) }, recipe: false)))
|
||||
{
|
||||
Assert.DoesNotContain(EconomyChecks.SourceSink(offered.Cache), x => x.Record == "recipe:" + EconomyFixture.Key("cap") && x.Message.Contains("no sink"));
|
||||
Assert.Equal(new[] { "recipe:" + EconomyFixture.Key("alloy") }, Records(EconomyChecks.ConsumerDemand(offered.Cache), EconomyChecks.LDemand));
|
||||
}
|
||||
foreach (var design in new Action<EconomyFixture>[]
|
||||
{
|
||||
f => f.Gear("shell", new[] { ("Casing", 1) }, offered: false, recipe: false),
|
||||
f => f.Hull("outpost", HullType.Station, new[] { ("Casing", 1) }, recipe: false)
|
||||
})
|
||||
using (var f = Casings(design))
|
||||
{
|
||||
Assert.Contains(EconomyChecks.SourceSink(f.Cache), x => x.Record == "recipe:" + EconomyFixture.Key("cap") && x.Message.Contains("no sink"));
|
||||
Assert.Empty(EconomyChecks.ConsumerDemand(f.Cache));
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue