Tests follow the allocator: warm-up, free thruster power, heat-detuned drives

The allocator reads Item.Active every tick, so fixtures that fire a thruster on their
first update warm its performance first; flight fixtures take the thrusters' energy draw
to zero so a brownout does not scale the turn; the Duel drives are mismatched after they
heat apart, and degraded by heat (the Large Drive has no durability term); the
consumer sits at (2,1), the Skiff's interior centre column. ReachIsWhatTheShipCanDo's
Longinus ceiling is 1.3 (measured 1.195 nose-on, 1.249 broadside).

Previous test commits do not build or pass; this one is unverified until the full run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-10 14:24:05 +02:00
commit ab450e72d3
7 changed files with 32 additions and 9 deletions

View file

@ -111,8 +111,7 @@ public sealed class BrownoutTests : IDisposable
Assert.True(ship.TryEquip(Mint(cache, items, reactorData)));
// The Skiff's centre column: a thruster there has no torque, so the allocator can fire it alone. These tests pin power,
// not flight, and a lone off-centre thruster could not fire without turning.
Assert.True(ship.TryEquip(Mint(cache, items, cache.GetByName<GearData>("Consumer")), new int2(2, 0)));
Assert.True(ship.GetBehavior<Thruster>().Torque == 0f, "the lone thruster turns nothing");
Assert.True(ship.TryEquip(Mint(cache, items, cache.GetByName<GearData>("Consumer")), new int2(2, 1)));
zone.Entities.Add(ship);
// Ship.Turn stays 0, so Ship.Update commands no rotation: no incidental torque thrust from the
@ -132,6 +131,8 @@ public sealed class BrownoutTests : IDisposable
Thrust = Curved(100, exponent), Visibility = Constant(0), Heat = Constant(0), EnergyUsage = Constant(100)
});
var ship = BuildShip(cache, reactorCharge);
Assert.True(ship.GetBehavior<Thruster>().Torque == 0f, "the lone thruster turns nothing");
ship.Equipment.Single(e => e.Data.Name == "Consumer").UpdatePerformance(); // live from its first performance update
ship.MovementDirection = float2(0, -1); // reverse-thruster axis += -MovementDirection.y == 1
ship.Update(1f);
return (-ship.Velocity.y, ship.GetBehavior<Thruster>());
@ -339,9 +340,11 @@ public sealed class BrownoutTests : IDisposable
var energyUsage = Curved(100, exponent: 1);
using var cache = OpenCatalog(new ThrusterData
{
Thrust = Constant(0), Visibility = Constant(0), Heat = Constant(0), EnergyUsage = energyUsage
Thrust = Constant(100), Visibility = Constant(0), Heat = Constant(0), EnergyUsage = energyUsage
});
var ship = BuildShip(cache, reactorCharge: 25); // demand 100 (nominal), generation 25 -> ratio .25
// The allocator fires a thruster that can push; it is live from its first performance update.
ship.Equipment.Single(e => e.Data.Name == "Consumer").UpdatePerformance();
ship.MovementDirection = float2(0, -1);
ship.Update(1f);
var thruster = ship.GetBehavior<Thruster>();

View file

@ -94,13 +94,13 @@ public sealed class ConditionRatioTests : IDisposable
Assert.True(ship.TryEquip(Mint(cache, items, reactorData)));
// The Skiff's centre column: a thruster there has no torque, so the allocator can fire it alone. These tests pin power,
// not flight, and a lone off-centre thruster could not fire without turning.
Assert.True(ship.TryEquip(Mint(cache, items, cache.GetByName<GearData>("Consumer")), new int2(2, 0)));
Assert.True(ship.GetBehavior<Thruster>().Torque == 0f, "the lone thruster turns nothing");
Assert.True(ship.TryEquip(Mint(cache, items, cache.GetByName<GearData>("Consumer")), new int2(2, 1)));
zone.Entities.Add(ship);
ship.Aim = float3(0, 0, 1); // Turn stays 0, as in BrownoutTests
ship.Activate();
consumer = ship.Equipment.Single(e => e.Data.Name == "Consumer");
Assert.True(ship.GetBehavior<Thruster>().Torque == 0f, "the lone thruster turns nothing");
// Settle at the plateau's center: two ticks so UpdatePerformance's deltaTemp term reads 0 before a test
// drives its own change, matching HeatResponseTests' own settle pattern.

View file

@ -273,7 +273,11 @@ public sealed partial class RunStartTests
// The Djinni's measured motion is further below its reach (+53% nose-on, +27% broadside at the last measurement): its
// strafers are off-axis and Ship.Update cancels their torque (M24), which the envelope, summed from what each thruster
// can push, does not model. Stated deviation; the bound still pins that the reach covers what the ship does.
var ceiling = name == "djinni" ? 1.6f : 1.15f;
// The Longinus's reach sits 19.5% (nose-on) and 24.9% (broadside) above what the allocator's flight measures
// (ruling allocator-divergence-accepted): the allocator holds the heading and pays for the drive torque out of
// the flank thrusters, so the envelope's box support (cut envelope-from-columns reads the allocator's columns)
// promises more than a controlled flight spends.
var ceiling = name == "djinni" ? 1.6f : 1.3f;
Assert.InRange(reach.noseOn, measured.noseOn * .9f, measured.noseOn * ceiling);
Assert.InRange(reach.broadside, measured.broadside * .9f, measured.broadside * ceiling);
}

View file

@ -268,6 +268,16 @@ public sealed class RestoredHullsTests
}
}
// For a fixture that pins flight, not brownout: the thrusters draw no power. A ship with every thruster firing asks
// for more than its reactor gives, and a brownout scales the thrusters' thrust unevenly (follow-up
// allocator-brownout-columns), so the turn or heading a test reads would be the brownout's.
internal static void FreeThrusterPower(CultCache cache)
{
foreach (var gear in cache.GetAll<GearData>())
foreach (var thruster in gear.Behaviors.OfType<ThrusterData>())
thruster.EnergyUsage = new PerformanceStat { Min = 0, Max = 0 };
}
// A restored hull with every thruster hardpoint (and its reactor) filled, activated and warmed up: the
// rotation thrusters the helm tests need. The caller owns the cache.
internal static Ship BuildThrustedShip(CultCache cache, string name)

View file

@ -290,6 +290,7 @@ public sealed class SteeringTests
public void ATurnIsThatFractionOfTheHullsTurn(float turn)
{
using var cache = RestoredHullsTests.OpenCatalog();
RestoredHullsTests.FreeThrusterPower(cache);
var ship = RestoredHullsTests.BuildThrustedShip(cache, Hull);
var settings = RestoredHullsTests.Settings();
const float dt = 1f / 60f;

View file

@ -89,6 +89,8 @@ public sealed class ThrottleLockTests : IDisposable
zone.Entities.Add(ship);
ship.Aim = float3(0, 0, 1);
ship.Activate();
// A thruster is live from its first performance update; the allocator reads that every tick.
foreach (var item in ship.Equipment) item.UpdatePerformance();
var thrusters = ship.GetBehaviors<Thruster>().ToArray();
return new Fixture

View file

@ -43,10 +43,11 @@ public sealed partial class RunStartTests
{
var (_, player) = AllocDuel();
var drives = AllocDrives(player);
Assert.True(drives.Length >= 2 && drives[^1].NominalThrust > drives[0].NominalThrust * 1.05f, "fixture: the drives are mismatched");
player.MovementDirection = float2(0, 1);
player.Turn = 0;
Assert.True(abs(AllocFly(player, 120)) < .1f);
// The drives start alike and heat apart as they fire: by now they are mismatched and both pushing.
Assert.True(drives.Length >= 2 && drives[^1].NominalThrust > drives[0].NominalThrust * 1.05f, "fixture: the drives are mismatched");
Assert.All(drives, d => Assert.True(d.Axis > .5f, "fixture: the drives are pushing"));
}
@ -88,11 +89,12 @@ public sealed partial class RunStartTests
player.Turn = 0;
Assert.True(abs(AllocFly(player, 30)) < .1f);
// The drive is heated a little off its plateau: its live thrust moves, and a column cached before would not.
var weakened = drives[^1];
var before = weakened.Thrust;
weakened.Item.EquippableItem.Durability *= .5f;
foreach (var cell in weakened.Item.InsetShape.Coordinates) player.Temperature[cell.x, cell.y] += 5f;
weakened.Item.UpdatePerformance();
Assert.True(weakened.Thrust < before * .95f, "fixture: halving durability weakens the drive");
Assert.True(weakened.Thrust < before * .95f && weakened.Item.Active.Value, "fixture: the heated drive pushes measurably less");
Assert.True(abs(AllocFly(player, 60)) < .1f);
}
@ -100,6 +102,7 @@ public sealed partial class RunStartTests
public void ATinyImbalanceStillHolds()
{
using var cache = RestoredHullsTests.OpenCatalog();
RestoredHullsTests.FreeThrusterPower(cache);
var ship = RestoredHullsTests.BuildThrustedShip(cache, "Longinus");
var drives = AllocDrives(ship);
Assert.True(drives.Length >= 2);