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:
parent
98337213a4
commit
94570ca0be
2 changed files with 94 additions and 25 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue