Tests: zone-weighted and zero-weight product draws pinned; every scenario brand pin guarded under eight seeds

ProductChoiceTests adopts Soul's probes: a nearer maker is favored (non-null zone), an allegiance weighs the draw, and an all-zero
weight draws uniformly. RigBrandTests becomes a theory over every scenario whose fits name products: each pinned design must come
out as exactly its brand on every fitted ship under eight stage seeds.

Not yet run: the commit is unverified.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-17 02:41:44 +02:00
commit 94570ca0be
2 changed files with 94 additions and 25 deletions

View file

@ -35,12 +35,13 @@ public sealed class ProductChoiceTests : IDisposable
public CultCache Cache;
public ItemManager Items;
public Galaxy Galaxy;
public Faction OutA, OutZ, OutBoth, OutNone;
public Faction OutA, OutZ, OutBoth, OutEven, OutNone;
public LoadoutGenerator Outfitter(Faction faction, uint seed)
// zone is the index of the zone the outfitter stands in; -1 stands in none, so distance does not weigh.
public LoadoutGenerator Outfitter(Faction faction, uint seed, int zone = -1)
{
var random = new Random(seed);
return new LoadoutGenerator(ref random, Items, faction == null ? null : Galaxy, null, faction, .5f);
return new LoadoutGenerator(ref random, Items, faction == null ? null : Galaxy, faction == null || zone < 0 ? null : Galaxy.Zones[zone], faction, .5f);
}
public FactionProductData Product(string name) => Cache.GetAll<FactionProductData>().Single(p => p.Name == name);
@ -76,6 +77,9 @@ public sealed class ProductChoiceTests : IDisposable
seed.Upsert(new Faction { Name = "OutA", ShortName = "OTA", Allegiance = { { alpha, 1f } } });
seed.Upsert(new Faction { Name = "OutZ", ShortName = "OTZ", Allegiance = { { zeta, 1f } } });
seed.Upsert(new Faction { Name = "OutBoth", ShortName = "OTB", Allegiance = { { alpha, 1f }, { zeta, .1f } } });
seed.Upsert(new Faction { Name = "OutEven", ShortName = "OTE", Allegiance = { { alpha, 1f }, { zeta, 1f } } });
seed.Upsert(new Faction { Name = "OutLoose", ShortName = "OTL", Allegiance = { { alpha, 1f }, { zeta, .25f } } });
seed.Upsert(new Faction { Name = "OutZero", ShortName = "OTO", Allegiance = { { alpha, 0f }, { zeta, 0f } } });
seed.Upsert(new Faction { Name = "OutNone", ShortName = "OTN" });
var shape = new Shape(3, 3);
foreach (var cell in shape.AllCoordinates) shape[cell] = true;
@ -102,9 +106,10 @@ public sealed class ProductChoiceTests : IDisposable
var outZ = cache.GetByName<Faction>("OutZ");
var game = new SavedGame
{
Factions = new[] { cache.RefOf(outA), cache.RefOf(outZ) },
Relationships = new[] { FactionRelationship.Neutral, FactionRelationship.Neutral },
HomeZones = new Dictionary<int, int> { { 0, 0 }, { 1, 0 } },
// Alpha's headquarters are in zone 0 and Zeta's in zone 1.
Factions = new[] { cache.RefOf(outA), cache.RefOf(outZ), cache.RefOf(cache.GetByName<Faction>("Alpha")), cache.RefOf(cache.GetByName<Faction>("Zeta")) },
Relationships = new[] { FactionRelationship.Neutral, FactionRelationship.Neutral, FactionRelationship.Neutral, FactionRelationship.Neutral },
HomeZones = new Dictionary<int, int> { { 0, 0 }, { 1, 0 }, { 2, 0 }, { 3, 1 } },
BossZones = new Dictionary<int, int>(),
DiscoveredZones = new[] { 0 },
ActionBarBindings = new SavedActionBarBinding[0],
@ -112,12 +117,13 @@ public sealed class ProductChoiceTests : IDisposable
};
var zones = new[]
{
new SavedZone { Name = "Zone 0", Position = new float2(0, 0), AdjacentZones = Array.Empty<int>(), Factions = new[] { 0, 1 }, Owner = 0, Contents = null }
new SavedZone { Name = "Zone 0", Position = new float2(0, 0), AdjacentZones = new[] { 1 }, Factions = new[] { 0, 1 }, Owner = 0, Contents = null },
new SavedZone { Name = "Zone 1", Position = new float2(1, 0), AdjacentZones = new[] { 0 }, Factions = new[] { 0, 1 }, Owner = 0, Contents = null }
};
RunSave.Commit(cache, game, zones, new ProvenanceLedger());
return new Rig
{
Cache = cache, OutA = outA, OutZ = outZ, OutBoth = cache.GetByName<Faction>("OutBoth"), OutNone = cache.GetByName<Faction>("OutNone"),
Cache = cache, OutA = outA, OutZ = outZ, OutBoth = cache.GetByName<Faction>("OutBoth"), OutEven = cache.GetByName<Faction>("OutEven"), OutNone = cache.GetByName<Faction>("OutNone"),
Items = new ItemManager(cache, new ProvenanceLedger(), RunSaveTests.TestSettings(), _ => { }),
Galaxy = new Galaxy(cache, cache.GetGlobal<SavedGame>(), _ => { })
};
@ -227,6 +233,41 @@ public sealed class ProductChoiceTests : IDisposable
Assert.Equal("slot 0,0: product Skiff by Alpha is not a product of Lamp", Assert.Single(failures));
}
private static int MakerDraws(Rig rig, Faction faction, int zone, string maker, int n)
{
var lamp = rig.Cache.GetByName<GearData>("Lamp");
return Enumerable.Range(1, n).Count(i => rig.Outfitter(faction, Seed(i), zone).ChooseProduct(lamp).Name == maker);
}
// Two makers an even allegiance weighs the same differ only by distance: standing in a zone, the maker whose
// headquarters is nearer comes up about twice as often (1 against 1/2).
[Fact]
public void ANearerMakerIsFavored()
{
var rig = Build("lamp-z", "lamp-a");
Assert.InRange(MakerDraws(rig, rig.OutEven, 0, "lamp-a", 300), 165, 235);
Assert.InRange(MakerDraws(rig, rig.OutEven, 1, "lamp-a", 300), 65, 135);
}
// A weight that read the allegiance alone would give 1 : .25 here, an 80% draw; the zone and allegiance together
// give 1 : .125 for alpha at its own zone, 89%.
[Fact]
public void AnAlliedMakerWeighsByItsAllegiance()
{
var rig = Build("lamp-z", "lamp-a");
Assert.InRange(MakerDraws(rig, rig.Cache.GetByName<Faction>("OutLoose"), 0, "lamp-a", 300), 245, 290);
}
// When every reachable maker weighs 0 the draw is uniform over them, not a pick of the first or last.
[Fact]
public void WhenEveryWeightIsZeroTheDrawIsUniform()
{
var rig = Build("lamp-z", "lamp-a");
var zero = rig.Cache.GetByName<Faction>("OutZero");
Assert.InRange(MakerDraws(rig, zero, 0, "lamp-a", 300), 100, 200);
Assert.InRange(MakerDraws(rig, zero, 0, "lamp-z", 300), 100, 200);
}
// The wire positions of the new fields: a preset written by one build is read by the next.
[Fact]
public void TheProductRefsSitAtTheirWirePositions()

View file

@ -11,26 +11,54 @@ public sealed partial class RunStartTests
private IEnumerable<string> FittedBrands(Entity entity, string design) =>
entity.Equipment.Where(item => _items.GetData(item.EquippableItem).Name == design).Select(item => ProductOf(item.EquippableItem));
// Each rig is staged under eight seeds: an open design would draw its brand from the seed, so a pin that holds under
// all of them is a pin, not a draw that happened to land on the same product.
[Fact]
public void TheRigsKeepTheirBrands()
private const string Sensor = "Compact Sensor", Fca = "Fire Control Array", Missile = "Guided Missile Launcher", Cell = "Cell Blaster", Cargo = "Cargo Bay";
// Every product a scenario's fits name, as (design, brand). A rig whose fits name a design where a brand stood would
// draw its brand from the stage's seed; so each scenario is staged under eight seeds, and every pinned design must
// come out as exactly its brand on every ship the scenario fits. A hull that is not a fit's (a generated or bare ship)
// is left out: its gear is not pinned.
public static IEnumerable<object[]> PinnedRigs() => new[]
{
Rig(new AiFusedDiscipline(), (Sensor, "not if i see you first"), (Fca, "Panopticon Prime")),
Rig(new Arcs(), (Missile, "GT 3K"), (Cell, "FastBlast+-"), ("Autocannon", "🫳🔫"), ("Charged Scatter Blaster", "ChargeBlast SG"), ("Light Machine Gun", "ClearPath"), (Sensor, "not if i see you first"), (Fca, "Panopticon Prime")),
Rig(new Belt(), (Missile, "GT 3K"), ("Lightning Projector", "DragOnBreath"), (Cell, "FastBlast+-"), (Sensor, "not if i see you first"), (Fca, "Panopticon Prime")),
Rig(new Crossing(), (Missile, "GT 3K"), ("Lightning Projector", "DragOnBreath"), (Cell, "FastBlast+-"), (Sensor, "not if i see you first"), (Fca, "Panopticon Prime")),
Rig(new Duel(), (Missile, "GT 3K"), (Cell, "FastBlast+-"), (Cargo, "Store-All Plus"), (Sensor, "not if i see you first"), (Fca, "Panopticon Prime")),
Rig(new FusedRounds(), ("Cluster Cannon", "DeathCluster"), (Sensor, "not if i see you first"), (Fca, "Panopticon Prime")),
Rig(new LauncherAngles(), (Missile, "GT 3K"), ("Swarm Rocket Pod", "Leonid"), (Cell, "FastBlast+-"), (Sensor, "not if i see you first"), (Fca, "Panopticon Prime")),
Rig(new LockOn(), (Missile, "GT 3K"), (Cell, "FastBlast+-"), (Cargo, "Store-All Plus"), (Sensor, "not if i see you first"), (Fca, "Panopticon Prime")),
Rig(new Pursuit(), ("Mine Launcher", "parting gift"), ("Swarm Rocket Pod", "Leonid"), (Missile, "GT 3K"), (Cell, "FastBlast+-"), (Cargo, "Store-All Plus"), (Sensor, "not if i see you first"), (Fca, "Panopticon Prime")),
Rig(new RefusedRounds(), ("Autocannon", "🫳🔫"), (Sensor, "not if i see you first"), (Fca, "Panopticon Prime")),
Rig(new StarvedReactor(), ("Light Laser", "Spectra"), (Sensor, "not if i see you first"), (Fca, "Panopticon Prime")),
Rig(new WeaponFeel(), ("Charged Scatter Blaster", "ChargeBlast SG"), ("Autocannon", "🫳🔫"), (Cell, "FastBlast+-"), (Sensor, "not if i see you first"), (Fca, "Panopticon Prime")),
};
private static object[] Rig(Scenario scenario, params (string design, string brand)[] pins) => new object[] { scenario, pins };
[Theory]
[MemberData(nameof(PinnedRigs))]
public void TheRigsKeepTheirBrands(Scenario scenario, (string design, string brand)[] pins)
{
for (uint seed = 1; seed <= 8; seed++)
{
var pursuit = Launch(new Scripted(false, new Pursuit().Stage, seed * 7919));
Assert.Empty(pursuit.failures);
Assert.Equal(new[] { "parting gift" }, FittedBrands(pursuit.staged.Player, "Mine Launcher"));
Assert.Equal(new[] { "Leonid" }, FittedBrands(pursuit.staged.Player, "Swarm Rocket Pod"));
var duel = Launch(new Scripted(false, new Duel().Stage, seed * 7919));
Assert.Empty(duel.failures);
var rival = Assert.Single(duel.staged.Entities);
Assert.Equal(new[] { "Spectra" }, Held(rival).Select(item => _items.Brand(item).Product.Name));
var arcs = Launch(new Scripted(false, new Arcs().Stage, seed * 7919));
Assert.Empty(arcs.failures);
Assert.Equal(new[] { "🫳🔫" }, FittedBrands(arcs.staged.Player, "Autocannon"));
var (_, _, staged, failures) = Launch(new Scripted(false, scenario.Stage, seed * 7919, scenario.Generate));
Assert.Empty(failures);
var ships = staged.Entities.Append(staged.Player).Where(ship => _items.GetData(ship.Hull).Name is "Longinus" or "Smoke Test Hull");
foreach (var (design, brand) in pins)
{
var brands = ships.SelectMany(ship => FittedBrands(ship, design)).Distinct().ToArray();
Assert.True(brands.SequenceEqual(new[] { brand }), $"{scenario.Name} seed {seed}: {design} is built as [{string.Join(", ", brands)}], not {brand}");
}
}
}
// Duel's rival holds Spectra in its cargo.
[Fact]
public void TheDuelRivalHoldsItsBrand()
{
var duel = Launch(new Scripted(false, new Duel().Stage, 7919));
Assert.Empty(duel.failures);
var rival = Assert.Single(duel.staged.Entities);
Assert.Equal(new[] { "Spectra" }, Held(rival).Select(item => _items.Brand(item).Product.Name));
}
}