Economy checks: the consumer walk follows upkeep edges; pin upkeep into a consumer good and a role filler

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
soul 2026-10-27 10:06:15 +01:00
commit da067a34a2
2 changed files with 22 additions and 3 deletions

View file

@ -411,7 +411,7 @@ public static class EconomyChecks
// A processed good owes a consumer use when it is shared (rulings consumer-demand-shared-only, draw-is-one-use). Its uses are
// the gear or hull roles, and the rounds some weapon draws, that the goods downstream of it (itself included) fill or are; a
// good used as upkeep is shared too. Shared means two or more such uses, or upkeep. A shared good must lead, through the
// chain, into a consumer good or a role one names. A good whose every use is one gear role is exempt like its final assembly.
// chain (an input edge or an upkeep edge), into a consumer good or a role one names. A good whose every use is one gear role is exempt like its final assembly.
// Derived from the recipe graph on every check; depth and distance play no part. Raw goods and the gate count for nothing.
public static List<EconomyFault> ConsumerDemand(CultCache catalog)
{
@ -424,7 +424,7 @@ public static class EconomyChecks
.SelectMany(r => Recipes.RolesOf(catalog.Get(r.Output))).Select(role => role.Name), StringComparer.Ordinal);
var upkeep = new HashSet<string>(recipes.Values.SelectMany(r => r.Upkeep).Select(c => c.Commodity.Key.Value), StringComparer.Ordinal);
var takenBy = recipes.Values.Where(IsCommodityRecipe)
.SelectMany(r => r.Inputs.Select(input => (Input: input.Commodity.Key.Value, Output: r.Output.Key.Value)))
.SelectMany(r => r.Inputs.Concat(r.Upkeep).Select(input => (Input: input.Commodity.Key.Value, Output: r.Output.Key.Value)))
.ToLookup(edge => edge.Input, edge => edge.Output, StringComparer.Ordinal);
var faults = new List<EconomyFault>();
foreach (var key in recipes.Keys.OrderBy(k => k, StringComparer.Ordinal))

View file

@ -383,13 +383,32 @@ public sealed class EconomyChecksTests
// Fuel is upkeep itself; coke is shared because a good it leads into is.
var faults = EconomyChecks.ConsumerDemand(f.Cache);
Assert.Equal(new[] { "recipe:commodity:coke", "recipe:commodity:fuel" }, faults.Select(x => x.Record));
Assert.Contains("'fuel' leads into upkeep but no use leads into a consumer good.", faults[1].Message);
Assert.Contains("'fuel' leads into Plate and upkeep but no use leads into a consumer good.", faults[1].Message);
f.Good("pot", roles: new[] { ("Casing", 1) });
f.EditRecipe("commodity:fuel", r => r.Fills.Add("Casing"));
Assert.Empty(EconomyChecks.ConsumerDemand(f.Cache));
}
[Fact]
public void ConsumerDemandFollowsUpkeepIntoAConsumerGoodAndIntoAFillerOfItsRole()
{
using var f = new EconomyFixture();
f.Good("ore");
f.Good("coolant", inputs: new[] { ("ore", 1) });
Assert.Equal(new[] { "recipe:commodity:coolant" }, DemandRecords(f));
// The refrigerator is a consumer good (a commodity made from roles) that takes coolant as upkeep: coolant leads into it.
f.Good("fridge", roles: new[] { ("Casing", 1) }, upkeep: new[] { ("coolant", 1) });
Assert.Empty(DemandRecords(f));
// Upkeep taken by a filler of a consumer role leads into that role the same way.
f.EditRecipe("commodity:fridge", r => r.Upkeep.Clear());
Assert.Equal(new[] { "recipe:commodity:coolant" }, DemandRecords(f));
f.Good("gasket", inputs: new[] { ("ore", 1) }, fills: new[] { "Casing" }, upkeep: new[] { ("coolant", 1) });
Assert.Empty(DemandRecords(f));
}
[Fact]
public void ConsumerDemandCountsADrawnRoundAsOneUse()
{