Assembly is an AssemblyDistribution: Faction key 19, optional per product, stated-quality mint
Replaces h1's AssemblyMean/AssemblyDeviation floats with one Faction.Assembly (key 19, null = not authored) and adds FactionProductData.Assembly (key 6, null = its maker's). CreateLot(product) draws from product.Assembly, else the maker's, else AssemblyDistribution defaults; CreateLot(product, quality) mints the lot and every fill at a stated quality and draws nothing. assembly-from-maker now seeds part mean + .1 and writes only a null Assembly. The shipped-catalog test is red until the catalog commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
57952dbfb7
commit
30597b0e45
8 changed files with 160 additions and 78 deletions
|
|
@ -64,14 +64,12 @@ public class Faction
|
|||
[Inspectable, JsonProperty("doctrine"), Key(17)]
|
||||
public FactionDoctrine Doctrine;
|
||||
|
||||
// The maker's final-assembly technology (mean) and quality control (deviation): the distribution every lot of
|
||||
// theirs draws its own quality from, and the hard ceiling on every one of its roles (quality-lean-assembly-hard-ceiling).
|
||||
// Key 18 is reserved for Livery and 16 for Relations.
|
||||
[Inspectable, JsonProperty("assemblyMean"), Key(19)]
|
||||
public float AssemblyMean = .5f;
|
||||
|
||||
[Inspectable, JsonProperty("assemblyDeviation"), Key(20)]
|
||||
public float AssemblyDeviation = .15f;
|
||||
// The maker's final assembly: the distribution every lot of theirs draws its own quality from, and the hard ceiling on each
|
||||
// of its roles (quality-lean-assembly-hard-ceiling). Null is not authored: a lot of theirs assembles at AssemblyDistribution's
|
||||
// defaults. assembly-from-maker derives it once at the part mean plus a tolerance margin (quality-min-tolerance-binned-assembly);
|
||||
// tune it in Studio after that. Key 16 is reserved for Relations and 18 for Livery.
|
||||
[Inspectable, JsonProperty("assembly"), Key(19)]
|
||||
public AssemblyDistribution Assembly;
|
||||
}
|
||||
|
||||
[CultDocument("aetheria.namefile", "1"), Inspectable, MessagePackObject]
|
||||
|
|
|
|||
|
|
@ -28,6 +28,11 @@ public class FactionProductData
|
|||
|
||||
[Inspectable, JsonProperty("roles"), Key(5)]
|
||||
public List<ProductRole> Roles = new List<ProductRole>();
|
||||
|
||||
// This line's own final test (maker-design-tiers-allowed): a binned or premium line is a better assembly as well as better
|
||||
// parts. Null assembles as its maker does.
|
||||
[Inspectable, JsonProperty("assembly"), Key(6)]
|
||||
public AssemblyDistribution Assembly;
|
||||
}
|
||||
|
||||
// How good this product's part is in one of its design's roles: a pseudo-gaussian distribution whose mean is
|
||||
|
|
@ -46,3 +51,15 @@ public class ProductRole
|
|||
[Inspectable, JsonProperty("standardDeviation"), Key(2)]
|
||||
public float StandardDeviation = .15f;
|
||||
}
|
||||
|
||||
// How well a maker puts the thing together: the technology and quality control of final assembly. A lot's own quality is drawn
|
||||
// from it, and it is the ceiling on every role the lot fills. Defaults are a middling maker.
|
||||
[MessagePackObject, JsonObject(MemberSerialization.OptIn)]
|
||||
public class AssemblyDistribution
|
||||
{
|
||||
[Inspectable, JsonProperty("mean"), Key(0)]
|
||||
public float Mean = .5f;
|
||||
|
||||
[Inspectable, JsonProperty("standardDeviation"), Key(1)]
|
||||
public float StandardDeviation = .15f;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,19 +118,24 @@ public class ItemManager
|
|||
}
|
||||
|
||||
// Builds one of a manufacturer's branded products (quality-is-assembly-over-supply-chain): the unit's own quality is its
|
||||
// final assembly, drawn from the maker's assembly distribution, and each of the design's roles is filled with a part whose
|
||||
// quality is drawn from the product's distribution for it. A maker the catalog cannot resolve assembles at a new Faction's
|
||||
// defaults.
|
||||
public int CreateLot(FactionProductData product)
|
||||
// final assembly, drawn from the product's assembly distribution, else its maker's, else AssemblyDistribution's defaults,
|
||||
// and each of the design's roles is filled with a part whose quality is drawn from the product's distribution for it.
|
||||
public int CreateLot(FactionProductData product) => Mint(product, null);
|
||||
|
||||
// The same lot at a stated quality: the assembly and every role fill are that quality and nothing is drawn, so a staged
|
||||
// fit does not ride the item stream (ScenarioStage.Fit).
|
||||
public int CreateLot(FactionProductData product, float quality) => Mint(product, clamp(quality, .01f, 1));
|
||||
|
||||
private int Mint(FactionProductData product, float? stated)
|
||||
{
|
||||
var design = ItemData.Get(product.Design);
|
||||
var maker = ItemData.Get(product.Manufacturer) ?? new Faction();
|
||||
var assembly = product.Assembly ?? ItemData.Get(product.Manufacturer)?.Assembly ?? new AssemblyDistribution();
|
||||
var lot = new Lot
|
||||
{
|
||||
Design = ItemData.RefOf<ItemData>(design),
|
||||
Origin = new Attributed { Faction = product.Manufacturer },
|
||||
Product = ItemData.RefOf(product),
|
||||
Quality = clamp(Random.NextGaussian(maker.AssemblyMean, maker.AssemblyDeviation), .01f, 1),
|
||||
Quality = stated ?? clamp(Random.NextGaussian(assembly.Mean, assembly.StandardDeviation), .01f, 1),
|
||||
Roles = new List<RoleFill>()
|
||||
};
|
||||
if (design.Roles != null)
|
||||
|
|
@ -140,7 +145,7 @@ public class ItemManager
|
|||
lot.Roles.Add(new RoleFill
|
||||
{
|
||||
Role = role.Name,
|
||||
Quality = clamp(Random.NextGaussian(build.Mean, build.StandardDeviation), .01f, 1)
|
||||
Quality = stated ?? clamp(Random.NextGaussian(build.Mean, build.StandardDeviation), .01f, 1)
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,32 +4,33 @@ using System.Reflection;
|
|||
using MessagePack;
|
||||
using Xunit;
|
||||
|
||||
// The shipped catalog carries each maker's derived assembly, and the Faction format pins its wire positions
|
||||
// The shipped catalog carries each maker's seeded assembly, and the Faction format pins its wire positions
|
||||
// (aetheria-release, cut assembly-from-maker).
|
||||
public sealed class AssemblyFromMakerCatalogTests
|
||||
{
|
||||
// Every maker of a roled product carries an Assembly the command seeded; no product overrides its maker's.
|
||||
[Fact]
|
||||
public void ShippedCatalogCarriesDerivation()
|
||||
public void ShippedCatalogCarriesAssembly()
|
||||
{
|
||||
using var cache = RestoredHullsTests.OpenCatalog();
|
||||
var products = cache.GetAll<FactionProductData>().Where(p => p.Roles.Count > 0).ToArray();
|
||||
var makers = products.Select(p => p.Manufacturer.Key).Distinct().ToArray();
|
||||
Assert.NotEmpty(makers);
|
||||
var makers = cache.GetAll<FactionProductData>().Where(p => p.Roles.Count > 0).Select(p => p.Manufacturer.Key).Distinct().ToArray();
|
||||
Assert.Equal(15, makers.Length);
|
||||
foreach (var key in makers)
|
||||
{
|
||||
var roles = products.Where(p => p.Manufacturer.Key.Equals(key)).SelectMany(p => p.Roles).ToArray();
|
||||
var faction = cache.Get<Faction>(key);
|
||||
Assert.Equal(Math.Round(roles.Average(r => r.Mean), 2), faction.AssemblyMean, 2);
|
||||
Assert.Equal(Math.Round(roles.Average(r => r.StandardDeviation), 2), faction.AssemblyDeviation, 2);
|
||||
var assembly = cache.Get<Faction>(key).Assembly;
|
||||
Assert.NotNull(assembly);
|
||||
Assert.InRange(assembly.Mean, .01f, 1f);
|
||||
}
|
||||
|
||||
Assert.All(cache.GetAll<FactionProductData>(), p => Assert.Null(p.Assembly));
|
||||
}
|
||||
|
||||
// 16 is reserved for Relations, 17 is Doctrine, 18 is reserved for Livery.
|
||||
// 16 is reserved for Relations, 17 is Doctrine, 18 is reserved for Livery, 20 for the place-names pass.
|
||||
[Fact]
|
||||
public void AssemblyFieldsSitAtKeys19And20()
|
||||
public void AssemblyRidesItsWireKeys()
|
||||
{
|
||||
int KeyOf(string field) => typeof(Faction).GetField(field).GetCustomAttribute<KeyAttribute>().IntKey.Value;
|
||||
Assert.Equal(19, KeyOf(nameof(Faction.AssemblyMean)));
|
||||
Assert.Equal(20, KeyOf(nameof(Faction.AssemblyDeviation)));
|
||||
int KeyOf(Type type) => type.GetField("Assembly").GetCustomAttribute<KeyAttribute>().IntKey.Value;
|
||||
Assert.Equal(19, KeyOf(typeof(Faction)));
|
||||
Assert.Equal(6, KeyOf(typeof(FactionProductData)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ using System.Linq;
|
|||
using GameCult.Caching;
|
||||
using Xunit;
|
||||
|
||||
// assembly-from-maker (aetheria-release, cut assembly-from-maker): each maker's assembly distribution is derived from the
|
||||
// roles of the products it manufactures.
|
||||
// assembly-from-maker (aetheria-release, cut assembly-from-maker): each maker's unauthored assembly is seeded from the roles
|
||||
// of the products it manufactures.
|
||||
public sealed class AssemblyFromMakerCommandTests : IDisposable
|
||||
{
|
||||
private readonly string _root = Path.Combine(Path.GetTempPath(), "aetheria-assembly-cmd-" + Guid.NewGuid().ToString("N"));
|
||||
|
|
@ -19,7 +19,7 @@ public sealed class AssemblyFromMakerCommandTests : IDisposable
|
|||
using var cache = AetheriaStores.Open(Catalog, catalogWritable: true);
|
||||
cache.Upsert(new VerseGrammar { Revision = 1 });
|
||||
var maker = cache.Upsert(new Faction { Name = "Maker" });
|
||||
cache.Upsert(new Faction { Name = "Bystander", AssemblyMean = .9f, AssemblyDeviation = .01f });
|
||||
cache.Upsert(new Faction { Name = "Bystander" });
|
||||
var gear = cache.Upsert(new GearData { Name = "Lamp", Hardpoint = HardpointType.Sensors, Shape = new Shape(), Price = 10 });
|
||||
var design = new CultRecordRef<CraftedItemData>(gear.Key);
|
||||
// Two products, three roles: the mean is over roles (.2, .4, .9 -> .5), the deviation likewise (.1, .1, .4 -> .2).
|
||||
|
|
@ -45,6 +45,19 @@ public sealed class AssemblyFromMakerCommandTests : IDisposable
|
|||
new ProductRole { Role = "z", Mean = .31f, StandardDeviation = .2f }
|
||||
}
|
||||
});
|
||||
// A maker whose parts are near the top: the margin is capped at 1. And one whose assembly was already authored in Studio.
|
||||
var top = cache.Upsert(new Faction { Name = "Top" });
|
||||
cache.Upsert(new FactionProductData
|
||||
{
|
||||
Name = "D", Design = design, Manufacturer = top,
|
||||
Roles = { new ProductRole { Role = "x", Mean = .95f, StandardDeviation = .05f } }
|
||||
});
|
||||
var tuned = cache.Upsert(new Faction { Name = "Tuned", Assembly = new AssemblyDistribution { Mean = .42f, StandardDeviation = .07f } });
|
||||
cache.Upsert(new FactionProductData
|
||||
{
|
||||
Name = "E", Design = design, Manufacturer = tuned,
|
||||
Roles = { new ProductRole { Role = "x", Mean = .1f, StandardDeviation = .3f } }
|
||||
});
|
||||
cache.FlushAsync().Wait();
|
||||
}
|
||||
|
||||
|
|
@ -57,8 +70,31 @@ public sealed class AssemblyFromMakerCommandTests : IDisposable
|
|||
return output.ToString();
|
||||
}
|
||||
|
||||
private Faction Maker(string name)
|
||||
{
|
||||
using var cache = AetheriaStores.Open(Catalog);
|
||||
return cache.GetAll<Faction>().Single(f => f.Name == name);
|
||||
}
|
||||
|
||||
// The assembly is the part mean plus the tolerance margin and the deviation the parts' mean; the margin is capped at 1; a
|
||||
// faction that makes no roled product stays unauthored.
|
||||
[Fact]
|
||||
public void DryRunWritesNothingApplyIsIdempotent()
|
||||
public void DerivesPartMeanPlusMargin()
|
||||
{
|
||||
Seed();
|
||||
Run(apply: true);
|
||||
Assert.Equal(.6f, Maker("Maker").Assembly.Mean, 3);
|
||||
Assert.Equal(.2f, Maker("Maker").Assembly.StandardDeviation, 3);
|
||||
Assert.Equal(.36f, Maker("Fine").Assembly.Mean, 3);
|
||||
Assert.Equal(.16f, Maker("Fine").Assembly.StandardDeviation, 3);
|
||||
Assert.Equal(1f, Maker("Top").Assembly.Mean, 3);
|
||||
Assert.Equal(.05f, Maker("Top").Assembly.StandardDeviation, 3);
|
||||
Assert.Null(Maker("Bystander").Assembly);
|
||||
}
|
||||
|
||||
// A value tuned in Studio survives every replay, a dry run writes nothing, and a second apply has nothing to do.
|
||||
[Fact]
|
||||
public void AuthoredAssemblyIsKept()
|
||||
{
|
||||
Seed();
|
||||
var before = File.ReadAllBytes(Catalog);
|
||||
|
|
@ -66,19 +102,10 @@ public sealed class AssemblyFromMakerCommandTests : IDisposable
|
|||
Assert.Equal(before, File.ReadAllBytes(Catalog));
|
||||
|
||||
Run(apply: true);
|
||||
Assert.Equal(.42f, Maker("Tuned").Assembly.Mean, 3);
|
||||
Assert.Equal(.07f, Maker("Tuned").Assembly.StandardDeviation, 3);
|
||||
using (var cache = AetheriaStores.Open(Catalog))
|
||||
{
|
||||
var maker = cache.GetAll<Faction>().Single(f => f.Name == "Maker");
|
||||
Assert.Equal(.5f, maker.AssemblyMean, 3);
|
||||
Assert.Equal(.2f, maker.AssemblyDeviation, 3);
|
||||
var fine = cache.GetAll<Faction>().Single(f => f.Name == "Fine");
|
||||
Assert.Equal(.26f, fine.AssemblyMean, 3);
|
||||
Assert.Equal(.16f, fine.AssemblyDeviation, 3);
|
||||
// A faction that manufactures nothing is written the field defaults.
|
||||
var bystander = cache.GetAll<Faction>().Single(f => f.Name == "Bystander");
|
||||
Assert.Equal(.5f, bystander.AssemblyMean, 3);
|
||||
Assert.Equal(.15f, bystander.AssemblyDeviation, 3);
|
||||
}
|
||||
Assert.All(cache.GetAll<FactionProductData>(), p => Assert.Null(p.Assembly));
|
||||
|
||||
var written = File.ReadAllBytes(Catalog);
|
||||
Assert.Contains("No changes", Run(apply: true));
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ public sealed class AssemblyFromMakerTests : IDisposable
|
|||
return _cache.GetAll<FactionProductData>().Single(p => p.Name == name);
|
||||
}
|
||||
|
||||
private static AssemblyDistribution A(float mean, float deviation) => new AssemblyDistribution { Mean = mean, StandardDeviation = deviation };
|
||||
|
||||
private float[] Mint(FactionProductData product, int count) =>
|
||||
Enumerable.Range(0, count).Select(_ => _items.Lots[_items.CreateLot(product)].Quality).ToArray();
|
||||
|
||||
|
|
@ -52,8 +54,8 @@ public sealed class AssemblyFromMakerTests : IDisposable
|
|||
[Fact]
|
||||
public void LotQualityFollowsMaker()
|
||||
{
|
||||
var low = Product("Low", new Faction { Name = "Low Maker", AssemblyMean = .3f, AssemblyDeviation = .02f }, .5f);
|
||||
var high = Product("High", new Faction { Name = "High Maker", AssemblyMean = .8f, AssemblyDeviation = .02f }, .5f);
|
||||
var low = Product("Low", new Faction { Name = "Low Maker", Assembly = A(.3f, .02f) }, .5f);
|
||||
var high = Product("High", new Faction { Name = "High Maker", Assembly = A(.8f, .02f) }, .5f);
|
||||
var lows = Mint(low, 300);
|
||||
var highs = Mint(high, 300);
|
||||
Assert.InRange(lows.Average(), .27f, .33f);
|
||||
|
|
@ -65,27 +67,54 @@ public sealed class AssemblyFromMakerTests : IDisposable
|
|||
[Fact]
|
||||
public void LotSpreadFollowsMakerDeviation()
|
||||
{
|
||||
var wide = Mint(Product("Wide", new Faction { Name = "Wide Maker", AssemblyMean = .5f, AssemblyDeviation = .2f }, .5f), 400);
|
||||
var tight = Mint(Product("Tight", new Faction { Name = "Tight Maker", AssemblyMean = .5f, AssemblyDeviation = .02f }, .5f), 400);
|
||||
var wide = Mint(Product("Wide", new Faction { Name = "Wide Maker", Assembly = A(.5f, .2f) }, .5f), 400);
|
||||
var tight = Mint(Product("Tight", new Faction { Name = "Tight Maker", Assembly = A(.5f, .02f) }, .5f), 400);
|
||||
double Spread(float[] lots) => Math.Sqrt(lots.Select(q => Math.Pow(q - lots.Average(), 2)).Average());
|
||||
Assert.InRange(Spread(wide), .15, .25);
|
||||
Assert.InRange(Spread(tight), .01, .04);
|
||||
}
|
||||
|
||||
// A maker the catalog cannot resolve assembles at a new Faction's defaults.
|
||||
// A maker the catalog cannot resolve, and a maker whose Assembly is not authored, assemble at AssemblyDistribution's defaults.
|
||||
[Fact]
|
||||
public void UnresolvedMakerAssemblesAtDefaults()
|
||||
public void UnauthoredAssemblyAssemblesAtDefaults()
|
||||
{
|
||||
var orphan = Product("Orphan", null, .5f);
|
||||
var defaults = new Faction();
|
||||
Assert.InRange(Mint(orphan, 400).Average(), defaults.AssemblyMean - .03f, defaults.AssemblyMean + .03f);
|
||||
var defaults = new AssemblyDistribution();
|
||||
Assert.InRange(Mint(Product("Orphan", null, .5f), 400).Average(), defaults.Mean - .03f, defaults.Mean + .03f);
|
||||
Assert.InRange(Mint(Product("Blank", new Faction { Name = "Blank Maker" }, .5f), 400).Average(), defaults.Mean - .03f, defaults.Mean + .03f);
|
||||
}
|
||||
|
||||
// A line's own final test (maker-design-tiers-allowed): a product carrying an Assembly mints at it, and a sibling product
|
||||
// of the same maker without one reads the maker's.
|
||||
[Fact]
|
||||
public void ProductAssemblyOverridesMaker()
|
||||
{
|
||||
var maker = new Faction { Name = "Binner", Assembly = A(.3f, .02f) };
|
||||
var plain = Product("Plain", maker, .5f);
|
||||
var premium = Product("Premium", maker, .5f);
|
||||
premium.Assembly = A(.85f, .02f);
|
||||
Assert.InRange(Mint(premium, 300).Average(), .82f, .88f);
|
||||
Assert.InRange(Mint(plain, 300).Average(), .27f, .33f);
|
||||
}
|
||||
|
||||
// A stated quality is every lot's and every fill's, clamped, and draws nothing from the item stream.
|
||||
[Fact]
|
||||
public void StatedQualityDrawsNothing()
|
||||
{
|
||||
var product = Product("Staged", new Faction { Name = "Staged Maker", Assembly = A(.3f, .2f) }, .5f, .2f);
|
||||
var before = _items.Random;
|
||||
var lot = _items.Lots[_items.CreateLot(product, .6f)];
|
||||
Assert.Equal(.6f, lot.Quality, 3);
|
||||
Assert.Equal(.6f, lot.Roles.Single().Quality, 3);
|
||||
Assert.Equal(before.NextFloat(), _items.Random.NextFloat());
|
||||
Assert.Equal(1f, _items.Lots[_items.CreateLot(product, 7f)].Quality, 3);
|
||||
Assert.Equal(.01f, _items.Lots[_items.CreateLot(product, -1f)].Roles.Single().Quality, 3);
|
||||
}
|
||||
|
||||
// The ceiling at the mint: a part drawn at .9 in an assembly of .4 reads .4; a part at .2 in the same assembly reads .2.
|
||||
[Fact]
|
||||
public void RoleCappedByAssembly()
|
||||
{
|
||||
var maker = new Faction { Name = "Capped", AssemblyMean = .4f, AssemblyDeviation = 0 };
|
||||
var maker = new Faction { Name = "Capped", Assembly = A(.4f, 0) };
|
||||
var over = _items.Lots[_items.CreateLot(Product("Over", maker, .9f))];
|
||||
var under = _items.Lots[_items.CreateLot(Product("Under", maker, .2f))];
|
||||
Assert.Equal(.4f, over.Quality, 3);
|
||||
|
|
@ -110,7 +139,7 @@ public sealed class AssemblyFromMakerTests : IDisposable
|
|||
[Fact]
|
||||
public void QualityTermReadsCeiling()
|
||||
{
|
||||
var maker = new Faction { Name = "Capped", AssemblyMean = .4f, AssemblyDeviation = 0 };
|
||||
var maker = new Faction { Name = "Capped", Assembly = A(.4f, 0) };
|
||||
var instance = (EquippableItem) _items.CreateInstance(_items.CreateLot(Product("Over", maker, .9f)));
|
||||
var stat = new PerformanceStat { Min = 0, Max = 1, Terms = { new StatTerm { Source = StatSource.Quality, Exponent = 1, Role = "lens" } } };
|
||||
Assert.Equal(.4f, _items.Evaluate(stat, instance), 3);
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ public sealed class RoleAuthoringTests : IDisposable
|
|||
public void TwoProductsOfOneDesignWithDifferentRoleMeansResolveDifferentValues()
|
||||
{
|
||||
using var cache = AetheriaStores.Open(Catalog, catalogWritable: true);
|
||||
var makerRef = cache.Upsert(new Faction { Name = "Maker", ShortName = "MKR", AssemblyMean = 1f, AssemblyDeviation = 0 });
|
||||
var makerRef = cache.Upsert(new Faction { Name = "Maker", ShortName = "MKR", Assembly = new AssemblyDistribution { Mean = 1f, StandardDeviation = 0 } });
|
||||
var lensGear = cache.Upsert(new GearData
|
||||
{
|
||||
Name = "Lensed", Hardpoint = HardpointType.Sensors, Shape = new Shape(), Price = 10,
|
||||
|
|
|
|||
|
|
@ -8,42 +8,45 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using GameCult.Caching;
|
||||
|
||||
// assembly-from-maker [apply] (aetheria-release, cut assembly-from-maker; rulings quality-is-assembly-over-supply-chain,
|
||||
// quality-lean-assembly-hard-ceiling): derives each maker's final-assembly distribution from the products it sells. A faction
|
||||
// that manufactures at least one product with roles gets AssemblyMean = the mean of those products' role means and
|
||||
// AssemblyDeviation = the mean of their standard deviations, rounded to 2 dp; every other faction gets the field defaults.
|
||||
// The derivation reads the products, so it replays last, after every command that adds products or factions. A faction that
|
||||
// already holds the derived values is not rewritten, so a rerun writes nothing. Dry run unless apply.
|
||||
// assembly-from-maker [apply] (aetheria-release, cut assembly-from-maker; rulings quality-lean-assembly-hard-ceiling,
|
||||
// quality-min-tolerance-binned-assembly): seeds each maker's final assembly from the products it sells. A faction with no
|
||||
// Assembly that manufactures at least one product with roles gets Mean = the mean of those products' role means plus Margin
|
||||
// (a maker assembles a little above its parts, capped at 1) and StandardDeviation = the mean of their standard deviations,
|
||||
// both rounded to 2 dp. It writes only an Assembly that is null, so a value tuned in Studio survives every replay, and it
|
||||
// never writes a product's Assembly. The derivation reads the products, so it replays last, after every command that adds
|
||||
// products or factions. Dry run unless apply.
|
||||
public static class AssemblyFromMakerCommand
|
||||
{
|
||||
// The distribution a maker's roled products imply: the mean over every role of every product, so a product with more
|
||||
// roles weighs more. Null when there is no role to average.
|
||||
public static (float Mean, float Deviation)? Derive(IEnumerable<FactionProductData> products)
|
||||
// How far above its part mean a maker's final assembly sits: the tolerance margin of the ceiling.
|
||||
public const float Margin = .1f;
|
||||
|
||||
// The assembly a maker's roled products imply: the mean over every role of every product, so a product with more roles
|
||||
// weighs more. Null when there is no role to average.
|
||||
public static AssemblyDistribution Derive(IEnumerable<FactionProductData> products)
|
||||
{
|
||||
var roles = products.SelectMany(p => p.Roles ?? new List<ProductRole>()).ToArray();
|
||||
if (roles.Length == 0) return null;
|
||||
return ((float) Math.Round(roles.Average(r => r.Mean), 2), (float) Math.Round(roles.Average(r => r.StandardDeviation), 2));
|
||||
return new AssemblyDistribution
|
||||
{
|
||||
Mean = (float) Math.Round(Math.Min(1, roles.Average(r => r.Mean) + Margin), 2),
|
||||
StandardDeviation = (float) Math.Round(roles.Average(r => r.StandardDeviation), 2)
|
||||
};
|
||||
}
|
||||
|
||||
public static int Run(CultCache cache, bool apply, TextWriter output)
|
||||
{
|
||||
var defaults = new Faction();
|
||||
var byMaker = cache.GetAll<FactionProductData>()
|
||||
.GroupBy(p => p.Manufacturer.Key)
|
||||
.Select(g => (Maker: g.Key, Derived: Derive(g)))
|
||||
.Where(m => m.Derived != null)
|
||||
.ToDictionary(m => m.Maker, m => m.Derived.Value);
|
||||
.ToDictionary(m => m.Maker, m => m.Derived);
|
||||
|
||||
var changed = new List<Faction>();
|
||||
foreach (var faction in cache.GetAll<Faction>().OrderBy(f => f.Name, StringComparer.Ordinal))
|
||||
{
|
||||
var (mean, deviation) = byMaker.TryGetValue(cache.RefOf(faction).Key, out var derived)
|
||||
? derived
|
||||
: (defaults.AssemblyMean, defaults.AssemblyDeviation);
|
||||
if (faction.AssemblyMean == mean && faction.AssemblyDeviation == deviation) continue;
|
||||
output.WriteLine($"{faction.Name}: assembly {faction.AssemblyMean:0.##}±{faction.AssemblyDeviation:0.##} -> {mean:0.##}±{deviation:0.##}");
|
||||
faction.AssemblyMean = mean;
|
||||
faction.AssemblyDeviation = deviation;
|
||||
if (faction.Assembly != null || !byMaker.TryGetValue(cache.RefOf(faction).Key, out var derived)) continue;
|
||||
output.WriteLine($"{faction.Name}: assembly -> {derived.Mean:0.##}±{derived.StandardDeviation:0.##}");
|
||||
faction.Assembly = derived;
|
||||
changed.Add(faction);
|
||||
}
|
||||
|
||||
|
|
@ -55,7 +58,8 @@ public static class AssemblyFromMakerCommand
|
|||
|
||||
if (!apply)
|
||||
{
|
||||
output.WriteLine($"\nDry run. Pass \"apply\" to write {changed.Count} factions.");
|
||||
output.WriteLine($"
|
||||
Dry run. Pass \"apply\" to write {changed.Count} factions.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -64,7 +68,8 @@ public static class AssemblyFromMakerCommand
|
|||
{
|
||||
foreach (var faction in changed) batch.Upsert(typeof(Faction), faction, cache.RefOf(faction).Key);
|
||||
});
|
||||
output.WriteLine($"\nWrote {changed.Count} factions in Aetheria.cc");
|
||||
output.WriteLine($"
|
||||
Wrote {changed.Count} factions in Aetheria.cc");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue