Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 /
Expand Down
136 changes: 136 additions & 0 deletions tools/Nova.SimRunner.Tests/StartFieldLongevityTests.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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).
/// <para>
/// 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.
/// </para>
/// </summary>
[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;

/// <summary>Ticks until the start field is exhausted with <paramref name="harvesterCount"/> harvesters working it from tick 0.</summary>
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");
}
}
}
Loading