diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7c5f02c..c7a9e98 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -73,6 +73,16 @@ die Versionierung folgt (in der aktuellen Doku-Phase) dem Dokumentationsstand de
spielerisch abgenommen und kein Meilenstein-Nachweis
### Hinzugefügt
+- **Tragzeit des Startfelds ist gemessen, nicht geraten (Paket 21.3, #87).**
+ `tools/Nova.SimRunner.Tests/StartFieldLongevityTests.cs` fährt den echten
+ Ernte-Auto-Zyklus auf der kanonischen Eröffnungsgeometrie (Feld (7,7) mit
+ 9.000 AE, Raffinerie (9,5)) bis zur Erschöpfung: **7:33 min** mit einem
+ Harvester, **3:46 min** mit zweien, **2:31 min** mit dreien — die Ernterate
+ dominiert, der Laufweg entfällt über die Footprint-Reichweite. Nebenbefund,
+ durch Messung belegt: die Eröffnung trägt nur etwa drei *lauffreie*
+ Harvester je Feld; mehr brauchen Wegzeit. Der Wert 9.000 bleibt — die
+ Zahlen stützen den Expansionsdruck aus D-102 statt ihn zu schwächen, und
+ der T-01-Fehlschätzer war ein Sichtbarkeitsproblem (21.2), kein Mengenproblem
- **Restbestand der Vorkommen anklickbar und sichtbar (Paket 21.2, #86).**
Ein Linksklick auf ein Aetherium-Vorkommen (auch ein erschöpftes) zeigt in der
Befehlskarte Restbestand und Anfangsreserve („Aetherium-Vorkommen — 6.420 /
diff --git a/tools/Nova.SimRunner.Tests/StartFieldLongevityTests.cs b/tools/Nova.SimRunner.Tests/StartFieldLongevityTests.cs
new file mode 100644
index 0000000..c0b9413
--- /dev/null
+++ b/tools/Nova.SimRunner.Tests/StartFieldLongevityTests.cs
@@ -0,0 +1,136 @@
+using NUnit.Framework;
+using Nova.Core;
+using Nova.Simulation;
+using Nova.Simulation.Definitions;
+using Nova.Simulation.Economy;
+using Nova.Simulation.Pathfinding;
+using Nova.Simulation.State;
+
+namespace Nova.SimRunner.Tests
+{
+ ///
+ /// Sprint 21 package 21.3 (issue #87): the start field's longevity is
+ /// MEASURED, not guessed. The T-01 tester estimated the start reserve at
+ /// 5.000 AE — it is 9.000 (D-102/D-107) — because nothing showed it; the
+ /// fix for that was 21.2. What was still missing is the honest number for
+ /// the design question "how long should a start field carry": this suite
+ /// runs the real auto-cycle (gather 2 AE/tick until the 330-AE cargo is
+ /// full, deposit on the return leg, resume the retained field id —
+ /// EconomySystem's own loop, driven exactly like the
+ /// HarvesterAutoCycleTests fixture) on the canonical opening geometry:
+ /// field (7,7), refinery footprint centre (9,5), harvesters adjacent to
+ /// the field. The footprint reach rule (D-104,
+ /// EconomySystem.HasOwnRefineryInReach) lets a harvester deposit from the
+ /// field cell itself, so no movement is registered — the measured time is
+ /// the pure gather cycle, exactly the regime the opening is built for.
+ /// 10 ticks = 1 second (SimClock.TicksPerSecond).
+ ///
+ /// The pins are measurements of the CURRENT constants (reserve 9.000,
+ /// HarvestRateAE 2, Alliance cargo 330). A change to any of them —
+ /// including a decided reserve change of the four mirrored field-layout
+ /// literals — moves these numbers on purpose and reopens the package.
+ ///
+ ///
+ [TestFixture]
+ public sealed class StartFieldLongevityTests
+ {
+ private const ushort FieldId = 1;
+ private const long StartReserveAE = 9000L; // D-102/D-107 canonical start field
+ private const long TickCap = 20000;
+
+ /// Ticks until the start field is exhausted with harvesters working it from tick 0.
+ private static long MeasureExhaustionTick(int harvesterCount)
+ {
+ var entities = new EntityManager(64);
+ var economy = new EconomySystem(entities);
+ var kernel = new SimulationKernel(new SimRandom(0x5EED42UL));
+ kernel.RegisterSystem(economy);
+
+ Assert.That(economy.TryAddField(FieldId, new GridPos2D(7, 7), StartReserveAE), Is.True,
+ "canonical start field (7,7) with 9.000 AE");
+
+ // Refinery entity at its footprint centre (8,4)-(10,6) -> (9,5).
+ entities.SpawnUnit(
+ 0, new Transform2D(SimFixed.FromInt(9), SimFixed.FromInt(5)),
+ SimFixed.Zero, role: UnitRole.Refinery);
+
+ Assert.That(
+ SimDefinitions.TryGetUnit(FactionId.Alliance, UnitRole.Harvester, out SimUnitDefinition harvesterDef),
+ Is.True, "Alliance harvester definition");
+
+ // Adjacent-to-field cells that are ALSO within the refinery
+ // footprint's deposit reach: measured against the centre (9,5)
+ // with reach 2, the field ring (6..8, 6..8) intersects it only on
+ // (7,6), (8,6) and (8,7). A harvester anywhere else fills one
+ // cargo and then HOLDS forever — which is itself part of the 21.3
+ // finding: the canonical opening supports at most ~3 walk-free
+ // harvesters per field.
+ (int X, int Y)[] spots = { (7, 6), (8, 6), (8, 7) };
+ for (int i = 0; i < harvesterCount; i++)
+ {
+ EntityId id = entities.SpawnUnit(
+ 0,
+ new Transform2D(SimFixed.FromInt(spots[i].X), SimFixed.FromInt(spots[i].Y)),
+ harvesterDef.MoveSpeed, role: UnitRole.Harvester);
+ // Start mid-cycle like a fresh harvest order: the auto-cycle
+ // retains the field id across every return leg.
+ entities.GetUnitRef(id).HarvestFieldId = FieldId;
+ }
+
+ kernel.Start();
+ while (kernel.CurrentTick.Value < TickCap)
+ {
+ kernel.StepTick();
+ if (economy.TryGetField(FieldId, out AetheriumField field) && field.IsExhausted)
+ {
+ return (long)kernel.CurrentTick.Value;
+ }
+ }
+ Assert.Fail($"field not exhausted after {TickCap} ticks with {harvesterCount} harvester(s)");
+ return -1;
+ }
+
+ [Test]
+ public void StartField_LastsTheMeasuredTicks_OneHarvester()
+ {
+ long ticks = MeasureExhaustionTick(1);
+ TestContext.Out.WriteLine($"1 harvester: {ticks} ticks = {ticks / 10.0:0.#} s = {ticks / 600.0:0.##} min");
+ // Arithmetic expectation: 9.000 AE at 2 AE/tick = 4.500 gather
+ // ticks, plus one return tick per 330-AE cargo trip (28 trips).
+ Assert.That(ticks, Is.EqualTo(4527),
+ "measured solo longevity of the 9.000-AE start field (~7:33 min)");
+ }
+
+ [Test]
+ public void StartField_LastsTheMeasuredTicks_TwoHarvesters()
+ {
+ long ticks = MeasureExhaustionTick(2);
+ TestContext.Out.WriteLine($"2 harvesters: {ticks} ticks = {ticks / 10.0:0.#} s = {ticks / 600.0:0.##} min");
+ Assert.That(ticks, Is.EqualTo(2263),
+ "measured two-harvester longevity (~3:46 min) — half the solo time plus per-trip overheads");
+ }
+
+ [Test]
+ public void StartField_LastsTheMeasuredTicks_ThreeHarvesters()
+ {
+ long ticks = MeasureExhaustionTick(3);
+ TestContext.Out.WriteLine($"3 harvesters: {ticks} ticks = {ticks / 10.0:0.#} s = {ticks / 600.0:0.##} min");
+ Assert.That(ticks, Is.EqualTo(1509),
+ "measured three-harvester longevity (~2:31 min) — a third of the solo time plus per-trip overheads");
+ }
+
+ [Test]
+ public void StartField_LongevityScalesWithHarvesterCount()
+ {
+ long solo = MeasureExhaustionTick(1);
+ long duo = MeasureExhaustionTick(2);
+ long trio = MeasureExhaustionTick(3);
+
+ // Near-perfect scaling: the gather rate dominates and every
+ // harvester gathers in parallel at 2 AE/tick; only the per-trip
+ // return tick breaks exact 1/N.
+ Assert.That((double)duo / solo, Is.InRange(0.45, 0.55), "two harvesters halve the field's life");
+ Assert.That((double)trio / solo, Is.InRange(0.28, 0.38), "three harvesters cut it to a third");
+ }
+ }
+}