From a9d0ffb21347f02bae0fa3e587fad61ee0e7f566 Mon Sep 17 00:00:00 2001 From: Dennis Westermann Date: Sun, 9 Aug 2026 22:47:30 +0200 Subject: [PATCH] feat(economy): add five finite Aetherium fields --- .../Gameplay/CanonicalMatchSetupTests.cs | 72 ++++++++++---- .../Scripts/Gameplay/Match/MatchBootstrap.cs | 95 +++++++++++++++---- .../Maps/GlutrinneBlockoutView.cs | 49 ++++++---- .../Simulation/Economy/EconomySystem.cs | 6 +- CHANGELOG.md | 8 ++ docs/production/DecisionLog.md | 49 +++++++++- .../hashkrieg/16_Sprint_Wirtschaft.md | 16 ++-- .../CanonicalMatchSetupTests.cs | 62 +++++++++--- .../Determinism10000Scenario.cs | 61 ++++++++---- 9 files changed, 325 insertions(+), 93 deletions(-) diff --git a/Assets/Tests/EditMode/Gameplay/CanonicalMatchSetupTests.cs b/Assets/Tests/EditMode/Gameplay/CanonicalMatchSetupTests.cs index 024f32c..2633bc9 100644 --- a/Assets/Tests/EditMode/Gameplay/CanonicalMatchSetupTests.cs +++ b/Assets/Tests/EditMode/Gameplay/CanonicalMatchSetupTests.cs @@ -97,7 +97,21 @@ public sealed class CanonicalMatchSetupTests private const ushort MapWidth = 128; private const ushort MapHeight = 128; private const int EntityCapacity = 1024; - private const long FieldReserveAE = 2000000L; + private struct FieldLayout + { + public ushort Id; + public int X, Y; + public long ReserveAE; + } + + private static readonly FieldLayout[] FieldLayouts = + { + new FieldLayout { Id = 1, X = 7, Y = 7, ReserveAE = 9000L }, + new FieldLayout { Id = 2, X = 117, Y = 117, ReserveAE = 9000L }, + new FieldLayout { Id = 3, X = 24, Y = 40, ReserveAE = 9000L }, + new FieldLayout { Id = 4, X = 100, Y = 84, ReserveAE = 9000L }, + new FieldLayout { Id = 5, X = 62, Y = 62, ReserveAE = 15000L }, + }; // Faction-resolved opening placement ids (SimDefinitions id rule): // slot 0 Alliance (role value), slot 1 Legion (role value + 17). private const ushort DefHQAlliance = 3; @@ -170,46 +184,47 @@ private static ReferenceHost BuildReferenceHost(ulong seed) /// Fixed opening layout of one slot, in grid cells. private sealed class SlotLayout { - public ushort FieldId; - public int FieldX, FieldY; public int HqOriginX, HqOriginY; public int BuilderX, BuilderY; } private static readonly SlotLayout Slot0Layout = new SlotLayout { - FieldId = 1, FieldX = 7, FieldY = 7, HqOriginX = 4, HqOriginY = 4, BuilderX = 13, BuilderY = 7, }; private static readonly SlotLayout Slot1Layout = new SlotLayout { - FieldId = 2, FieldX = 119, FieldY = 119, HqOriginX = 120, HqOriginY = 120, - BuilderX = 113, BuilderY = 119, + BuilderX = 111, BuilderY = 117, }; /// /// Byte-exact mirror of Determinism10000Scenario.SetupMatch (D-077): - /// per slot one Aetherium field, a completed HQ and ONE Builder — - /// nothing else. Spawn ORDER is load-bearing: EntityManager hands - /// out ids from a deterministic free list, so any reordering shifts - /// every id and therefore every hash. Units spawn through SpawnUnit's - /// defaults (maxHealth 100 for all), exactly like the scenario — - /// NOT through SimDefinitions. + /// five finite Aetherium fields in canonical id order, then per slot a + /// completed HQ and ONE Builder — nothing else. Entity spawn order is + /// load-bearing: EntityManager hands out ids from a deterministic free + /// list, so any reordering shifts every id and therefore every hash. + /// Units spawn through SpawnUnit's defaults (maxHealth 100 for all), + /// exactly like the scenario — NOT through SimDefinitions. /// private static void ApplyOpeningPosition(ReferenceHost host) { // The slot factions are already bound: BuildReferenceHost mirrors // BuildHost, which assigns them before Kernel.Start() (the // SetSlotFaction guard requires it). + for (int i = 0; i < FieldLayouts.Length; i++) + { + FieldLayout field = FieldLayouts[i]; + Assert.That(host.Economy.TryAddField( + field.Id, new GridPos2D(field.X, field.Y), field.ReserveAE), + Is.True, "reference field registration"); + } + for (byte slot = 0; slot < 2; slot++) { SlotLayout c = slot == 0 ? Slot0Layout : Slot1Layout; - - Assert.That(host.Economy.TryAddField(c.FieldId, new GridPos2D(c.FieldX, c.FieldY), FieldReserveAE), - Is.True, "reference field registration"); Assert.That(host.Construction.PlaceCompletedBuilding(slot, slot == 0 ? DefHQAlliance : DefHQLegion, c.HqOriginX, c.HqOriginY).IsValid, Is.True, "reference HQ placement"); @@ -376,7 +391,15 @@ public void MatchBootstrap_PlacesTheCanonicalOpeningGeometry() bootstrap.StartGrayboxMatch(); Assert.That(bootstrap.LocalFieldCell, Is.EqualTo(new Vector2Int(7, 7))); - Assert.That(bootstrap.EnemyFieldCell, Is.EqualTo(new Vector2Int(119, 119))); + Assert.That(bootstrap.EnemyFieldCell, Is.EqualTo(new Vector2Int(117, 117))); + Assert.That(bootstrap.AllFieldCells, Is.EqualTo(new[] + { + new Vector2Int(7, 7), + new Vector2Int(117, 117), + new Vector2Int(24, 40), + new Vector2Int(100, 84), + new Vector2Int(62, 62), + })); Assert.That(bootstrap.LocalHqOrigin, Is.EqualTo(new Vector2Int(4, 4))); Assert.That(bootstrap.EnemyHqOrigin, Is.EqualTo(new Vector2Int(120, 120))); Assert.That(bootstrap.MapSize, Is.EqualTo(new Vector2Int(MapWidth, MapHeight))); @@ -398,6 +421,23 @@ public void MatchBootstrap_PlacesTheCanonicalOpeningGeometry() Is.EqualTo(3000L), "the D-077 start balance (EconomySystem.CanonicalMatchStartingCreditsAE)"); } + [Test] + public void ReferenceOpeningPosition_RegistersFiveFiniteFields() + { + ReferenceHost host = BuildReferenceHost(CanonicalSeed); + ApplyOpeningPosition(host); + + Assert.That(host.Economy.FieldCount, Is.EqualTo(FieldLayouts.Length)); + for (int i = 0; i < FieldLayouts.Length; i++) + { + FieldLayout expected = FieldLayouts[i]; + Assert.That(host.Economy.TryGetField(expected.Id, out AetheriumField actual), Is.True); + Assert.That(actual.GridPos.X, Is.EqualTo(expected.X)); + Assert.That(actual.GridPos.Y, Is.EqualTo(expected.Y)); + Assert.That(actual.RemainingAE, Is.EqualTo(expected.ReserveAE)); + } + } + [Test] public void ReferenceOpeningPosition_IsSeedSensitive() { diff --git a/Assets/_Project/Scripts/Gameplay/Match/MatchBootstrap.cs b/Assets/_Project/Scripts/Gameplay/Match/MatchBootstrap.cs index 4951166..4ac85e3 100644 --- a/Assets/_Project/Scripts/Gameplay/Match/MatchBootstrap.cs +++ b/Assets/_Project/Scripts/Gameplay/Match/MatchBootstrap.cs @@ -85,8 +85,9 @@ public NetworkJoinStatus( /// quality/content/mvp-v1.json startStatePerPlayer): per slot ONLY a /// completed HQ, ONE Builder and 3.000 AE starting credits /// (EconomySystem.CanonicalMatchStartingCreditsAE, plumbed in by - /// 's default) — - /// plus one Aetherium field per slot. The Refinery is NO longer + /// 's default) — plus the five + /// finite canonical Aetherium fields (two starts, two expansions and the + /// contested centre). The Refinery is NO longer /// pre-placed: the player builds it (it has no Power-plant prerequisite /// since D-077), and the completed Refinery — not the HQ — produces the /// Harvesters. @@ -94,9 +95,10 @@ public NetworkJoinStatus( /// /// It MIRRORS Determinism10000Scenario.SetupMatch /// (tools/Nova.SimRunner/Determinism10000Scenario.cs): identical seed, - /// identical map size, identical entity capacity, identical per-slot - /// layout AND identical spawn ORDER — field, HQ, Builder; slot 0 first, - /// then slot 1. + /// identical map size, identical entity capacity, identical field and + /// per-slot layout AND identical entity spawn order — HQ, Builder; slot 0 + /// first, then slot 1. Fields register first in canonical id order but do + /// not allocate entity ids. /// The order is load-bearing: the hands out /// entity ids from a deterministic free list, so any reordering shifts /// every id and therefore every state hash. An EditMode test asserts that @@ -139,8 +141,32 @@ public sealed class MatchBootstrap : MonoBehaviour /// Canonical scenario seed (DeterminismOptions.Seed). Required for InitialStateHash parity: the PRNG words are hashed into the kernel state block. public const ulong CanonicalSeed = 0xDE7E000000010271UL; - /// Aetherium reserve per field, in AE (Determinism10000Scenario.FieldReserveAE). - private const long FieldReserveAE = 2000000L; + /// One Aetherium field of the canonical map (16.7, C1). + private struct FieldLayout + { + public ushort Id; + public int X, Y; + public long ReserveAE; + } + + /// + /// The five canonical fields (16.7, C1 — MVPContentManifest section 5, + /// mirror of Determinism10000Scenario.FieldLayouts): two start fields + /// and two natural expansions at 9.000 AE each, one contested centre + /// at 15.000. Symmetry is binding: every slot-1 coordinate is the + /// point mirror of slot 0 through the map centre ((x, y) -> + /// (124 - x, 124 - y), the same mirror that maps HQ (4,4) to HQ + /// (120,120)), so both starts are equally far from their expansion + /// (36 cells) and from the centre (58). + /// + private static readonly FieldLayout[] FieldLayouts = + { + new FieldLayout { Id = 1, X = 7, Y = 7, ReserveAE = 9000L }, + new FieldLayout { Id = 2, X = 117, Y = 117, ReserveAE = 9000L }, + new FieldLayout { Id = 3, X = 24, Y = 40, ReserveAE = 9000L }, + new FieldLayout { Id = 4, X = 100, Y = 84, ReserveAE = 9000L }, + new FieldLayout { Id = 5, X = 62, Y = 62, ReserveAE = 15000L }, + }; /// maxHealth stamped by SpawnUnit when no definition stats are applied. private const int SpawnDefaultMaxHealth = 100; @@ -224,9 +250,27 @@ public sealed class MatchBootstrap : MonoBehaviour /// Aetherium field cell of the human player (7, 7). public Vector2Int LocalFieldCell => new Vector2Int(LocalPlayerLayout.FieldX, LocalPlayerLayout.FieldY); - /// Aetherium field cell of the opponent (119, 119). + /// Aetherium field cell of the opponent (117, 117 since 16.7 — the exact centre mirror). public Vector2Int EnemyFieldCell => new Vector2Int(EnemyPlayerLayout.FieldX, EnemyPlayerLayout.FieldY); + /// + /// All five registered field cells in canonical id order: start 0/1, + /// expansion 0/1, contested centre. Presentation iterates this list so + /// marker and scatter geometry cannot silently omit a field. + /// + public Vector2Int[] AllFieldCells + { + get + { + var cells = new Vector2Int[FieldLayouts.Length]; + for (int i = 0; i < FieldLayouts.Length; i++) + { + cells[i] = new Vector2Int(FieldLayouts[i].X, FieldLayouts[i].Y); + } + return cells; + } + } + /// Lower-left footprint origin of the human HQ (4, 4). public Vector2Int LocalHqOrigin => new Vector2Int(LocalPlayerLayout.HqOriginX, LocalPlayerLayout.HqOriginY); @@ -655,7 +699,10 @@ private void BuildOpening(MatchConfig config) // Global slot order is load-bearing for entity ids and snapshots: // BOTH clients build slot 0 first, then slot 1, regardless of - // which one the relay assigned locally. + // which one the relay assigned locally. Field registration + // spawns nothing and therefore does not shift ids, but it is part + // of the hashed initial state and must happen in one fixed order. + SetupFields(); SetupSlot(LocalLayout); SetupSlot(EnemyLayout); } @@ -865,17 +912,29 @@ private void OnDestroy() } /// - /// One slot's D-077 start state: an Aetherium field, a completed HQ - /// and one Builder near it — nothing else. Spawn order mirrors - /// SetupMatch exactly (field, HQ, Builder). + /// Registers all five canonical fields in ascending id order. This is + /// the exact field pass mirrored by Determinism10000Scenario and both + /// CanonicalMatchSetupTests lanes. /// - private void SetupSlot(SlotLayout c) + private void SetupFields() { - if (!Runner.Economy.TryAddField(c.FieldId, new GridPos2D(c.FieldX, c.FieldY), FieldReserveAE)) + for (int i = 0; i < FieldLayouts.Length; i++) { - throw new InvalidOperationException($"[MatchBootstrap] field {c.FieldId} could not be registered"); + FieldLayout field = FieldLayouts[i]; + if (!Runner.Economy.TryAddField(field.Id, new GridPos2D(field.X, field.Y), field.ReserveAE)) + { + throw new InvalidOperationException($"[MatchBootstrap] field {field.Id} could not be registered"); + } } + } + /// + /// One slot's D-077 start state: a completed HQ and one Builder near + /// it — nothing else. Fields are registered globally by + /// ; entity spawn order remains HQ, Builder. + /// + private void SetupSlot(SlotLayout c) + { FactionId faction = Runner.Economy.GetSlotFaction(c.Slot); ushort hqDefId = SimDefinitions.ToDefinitionId(faction, UnitRole.HQ); @@ -933,13 +992,13 @@ private sealed class SlotLayout BuilderX = 13, BuilderY = 7, }; - /// Opponent base, top-right: the 180-degree mirror of . + /// Opponent base, top-right: the exact centre mirror of . private static readonly SlotLayout EnemyLayout = new SlotLayout { Slot = EnemySlot, - FieldId = 2, FieldX = 119, FieldY = 119, + FieldId = 2, FieldX = 117, FieldY = 117, HqOriginX = 120, HqOriginY = 120, - BuilderX = 113, BuilderY = 119, + BuilderX = 111, BuilderY = 117, }; } } diff --git a/Assets/_Project/Scripts/Presentation/Maps/GlutrinneBlockoutView.cs b/Assets/_Project/Scripts/Presentation/Maps/GlutrinneBlockoutView.cs index 6800c63..16b5996 100644 --- a/Assets/_Project/Scripts/Presentation/Maps/GlutrinneBlockoutView.cs +++ b/Assets/_Project/Scripts/Presentation/Maps/GlutrinneBlockoutView.cs @@ -9,16 +9,13 @@ namespace Nova.Presentation.Maps /// what is rendered is exactly what the simulation registered: the /// procedural desert ground of the Glutrinne biome, scattered rock /// debris, a weathered edge band instead of a hard frame, and an - /// aetherium crystal cluster on each of the two fields the canonical - /// match actually registers. + /// aetherium crystal cluster on each of the five fields the canonical + /// match registers. /// /// Pure presentation: this component reads the bootstrap's layout /// properties and spawns primitive-only markers; it never writes into - /// simulation state. The full five-field manifest layout with the two - /// primary attack routes is G4 scope and deliberately NOT shown — the - /// blockout must not promise fields the match does not have - /// (docs/production/ScopeLedger.md, rows map.aetheriumFields / - /// map.primaryRouteCount). + /// simulation state. The five-field manifest layout is visible since + /// Sprint 16.7; primary-route dressing remains later map-art scope. /// /// /// KARTENBILD (D-085): everything here is generated at runtime with a @@ -92,11 +89,14 @@ private void Start() return; } + Vector2Int[] fieldCells = _bootstrap.AllFieldCells; TintGround(); - BuildScatterRocks(); + BuildScatterRocks(fieldCells); BuildWeatheredEdge(_bootstrap.MapSize); - BuildFieldMarker(_bootstrap.LocalFieldCell, "Local"); - BuildFieldMarker(_bootstrap.EnemyFieldCell, "Enemy"); + for (int i = 0; i < fieldCells.Length; i++) + { + BuildFieldMarker(fieldCells[i], $"Field_{i + 1}"); + } } /// @@ -140,11 +140,11 @@ private void TintGround() /// /// Rock debris: squashed-sphere boulders and pebbles, placed by a /// fixed-seed xorshift (no UnityEngine.Random), rejected inside the - /// exclusion zones around both start bases and both aetherium + /// exclusion zones around both start bases and all five aetherium /// fields, and NEVER carrying a collider — the debris is pure /// visual, the sim's grid pathing does not see it (and must not). /// - private void BuildScatterRocks() + private void BuildScatterRocks(Vector2Int[] fieldCells) { Vector2Int mapSize = _bootstrap.MapSize; var scatter = new GameObject("ScatterRocks"); @@ -157,7 +157,7 @@ private void BuildScatterRocks() { float x = 2f + Next01(ref rng) * (mapSize.x - 4f); float z = 2f + Next01(ref rng) * (mapSize.y - 4f); - if (IsExcluded(x, z)) continue; + if (IsExcluded(x, z, fieldCells)) continue; float sx = 0.25f + Next01(ref rng) * 0.70f; float sy = 0.15f + Next01(ref rng) * 0.40f; @@ -180,16 +180,25 @@ private void BuildScatterRocks() } /// Inside a start-base or aetherium-field exclusion zone the scatter stays out (the D-085 brief). - private bool IsExcluded(float x, float z) + private bool IsExcluded(float x, float z, Vector2Int[] fieldCells) { Vector2Int localHq = _bootstrap.LocalHqCenterCell; Vector2Int enemyHq = _bootstrap.EnemyHqCenterCell; - Vector2Int localField = _bootstrap.LocalFieldCell; - Vector2Int enemyField = _bootstrap.EnemyFieldCell; - return WithinRadius(x, z, localHq.x + 0.5f, localHq.y + 0.5f, BaseExclusionRadius) - || WithinRadius(x, z, enemyHq.x + 0.5f, enemyHq.y + 0.5f, BaseExclusionRadius) - || WithinRadius(x, z, localField.x + 0.5f, localField.y + 0.5f, FieldExclusionRadius) - || WithinRadius(x, z, enemyField.x + 0.5f, enemyField.y + 0.5f, FieldExclusionRadius); + if (WithinRadius(x, z, localHq.x + 0.5f, localHq.y + 0.5f, BaseExclusionRadius) + || WithinRadius(x, z, enemyHq.x + 0.5f, enemyHq.y + 0.5f, BaseExclusionRadius)) + { + return true; + } + + for (int i = 0; i < fieldCells.Length; i++) + { + Vector2Int field = fieldCells[i]; + if (WithinRadius(x, z, field.x + 0.5f, field.y + 0.5f, FieldExclusionRadius)) + { + return true; + } + } + return false; } private static bool WithinRadius(float x, float z, float cx, float cz, float radius) diff --git a/Assets/_Project/Scripts/Simulation/Economy/EconomySystem.cs b/Assets/_Project/Scripts/Simulation/Economy/EconomySystem.cs index 7f6359f..a7ae85c 100644 --- a/Assets/_Project/Scripts/Simulation/Economy/EconomySystem.cs +++ b/Assets/_Project/Scripts/Simulation/Economy/EconomySystem.cs @@ -113,7 +113,11 @@ public sealed class EconomySystem : IStatefulSimSystem, ISlotFactionLookup /// Format capacity for Aetherium fields (map content: 5 fields in mvp-v1). public const int MaxFields = 64; - /// Provisional harvest rate in AE per tick per harvester (Q-040 candidate). + /// + /// Harvest rate in AE per tick per Harvester. D-102 deliberately keeps + /// the provisional value at 2 for 16.7 and defers calibration until a + /// played timing curve provides evidence. + /// public const int HarvestRateAE = 2; /// diff --git a/CHANGELOG.md b/CHANGELOG.md index f149aaf..717a9fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,14 @@ die Versionierung folgt (in der aktuellen Doku-Phase) dem Dokumentationsstand de das Tor identisch zur Kopfzahl. Die Partie bestätigt damit die Neutralität; sie belegt keine Verbesserung. +### Geändert +- **16.7/C1: Fünf endliche Aetheriumfelder schaffen Knappheit (D-102)** — die + zwei praktisch endlosen Startfelder werden durch zwei symmetrische + Startfelder und zwei Expansionen mit je 9.000 AE sowie ein umkämpftes + Zentrum mit 15.000 AE ersetzt. Alle fünf Startaufstellungs-Spiegel und die + Kartenmarker folgen derselben Reihenfolge; `HarvestRateAE` bleibt bewusst + bei 2 AE/Tick, bis eine gespielte Balance-Kalibrierung belastbare Werte gibt + ### Behoben - **#54: Das Radar wird ein Gebäude (C3/D-096)** — die Minimap ist jetzt eine Radar-Funktion: `MinimapHud` zeichnet (Panel und Trefferfläche) nur noch, diff --git a/docs/production/DecisionLog.md b/docs/production/DecisionLog.md index 4c4e8b5..834b513 100644 --- a/docs/production/DecisionLog.md +++ b/docs/production/DecisionLog.md @@ -1,6 +1,6 @@ # Decision Log -**Version:** 1.33.0 | **Status:** aktiv (laufend) | **Verantwortungsbereich:** Game Director / Lead Technical Director / Project Owner | **Sprint:** 16 +**Version:** 1.34.0 | **Status:** aktiv (laufend) | **Verantwortungsbereich:** Game Director / Lead Technical Director / Project Owner | **Sprint:** 16 ## Zweck @@ -2992,6 +2992,52 @@ die Zusicherung dort schrumpft auf eine Zeile, der erklärende Kommentar verweist auf die neue Datei. Der Einheitenstrang ist zu informieren (Issue #75). Wer den Harness umbenennt, sagt es an. Keine Baseline-Datei ist berührt. +--- + +### D-102 | verbindlich | Sprint 16 (fünf endliche Aetheriumfelder; Ernterate getrennt kalibrieren) + +**Status:** Inhaberfreigabe vom 2026-08-09. + +**Kontext:** Die kanonische Glutrinne registriert bislang zwei Felder mit je +2.000.000 AE. Ein Sammler könnte daran rund 28 Stunden ununterbrochen ernten; +Expansion und Kartenmitte haben damit keinen wirtschaftlichen Zweck. Das +MVP-Manifest nennt fünf Felder mit 9.000 beziehungsweise 15.000 AE, während +`EconomySystem.HarvestRateAE = 2` ausdrücklich nur provisorisch ist. Für eine +neue Ernterate existiert weder ein verbindlicher Zielwert noch gespielte +Messung. + +**Alternativen:** (a) Feldlayout und Ernterate gleichzeitig nach Gefühl +ändern; (b) die endlichen Manifestfelder liefern und die Rate unverändert +lassen, bis eine gespielte Kurve kalibriert werden kann; (c) Knappheit bis zur +vollständigen Balance-Runde vertagen und die praktisch endlosen Felder +behalten. + +**Entscheidung:** (b). Die kanonische Karte registriert in dieser Reihenfolge: + +1. Start Slot 0 `(7,7)`, 9.000 AE; +2. Start Slot 1 `(117,117)`, 9.000 AE; +3. Expansion Slot 0 `(24,40)`, 9.000 AE; +4. Expansion Slot 1 `(100,84)`, 9.000 AE; +5. Zentrum `(62,62)`, 15.000 AE. + +Die Paarpositionen sind durch `(x,y) → (124-x,124-y)` punktgespiegelt. Felder +werden vor den HQ-/Builder-Entitäten in aufsteigender ID-Reihenfolge +registriert. `HarvestRateAE` bleibt für Paket 16.7 bei **2 AE/Tick**; ihre +Kalibrierung ist sichtbar vertagt und nicht als erledigt zu melden. + +**Begründung:** Die Manifestreserven beheben den belegten +Knappheitsdefekt, ohne eine unbelegte zweite Balancevariable zu verändern. +Symmetrie verhindert einen Startvorteil, die feste Reihenfolge hält beide +Lockstep-Hosts und den Headless-Harness identisch. + +**Konsequenzen:** Alle fünf Spiegel der Startaufstellung müssen gemeinsam +ziehen; die Präsentation markiert und schützt alle fünf Felder vor +Steinstreuung. Der Definitions-Hash und das Zustandsformat ändern sich nicht, +der gehashte Initialzustand aber schon. Geschützte Determinismus-Baselines +werden nur in einem getrennten Baseline-PR bewegt, falls der Guard dies +verlangt. Die Ernteraten-Kalibrierung braucht eine gespielte Runde mit +Zeitkurve und bleibt bis dahin offen. + ## Offene Punkte - Alle Sprint-4-Review-Befunde (105, davon 9 kritisch): 7 entscheidungsbedürftige kritische Befunde sind durch D-043–D-052 entschieden. @@ -3072,6 +3118,7 @@ Wer den Harness umbenennt, sagt es an. Keine Baseline-Datei ist berührt. | Version | Datum | Änderung | Autor | |---|---|---|---| +| 1.34.0 | 2026-08-09 | D-102 aufgenommen: fünf symmetrische endliche Aetheriumfelder werden geliefert, die mangels belastbarer Zielkurve unveränderte Ernterate von 2 AE/Tick wird ausdrücklich getrennt kalibriert | Project Owner / Agent | | 1.33.0 | 2026-08-09 | D-101 aufgenommen: der Ausgangspin der kanonischen KI-Partie (Entscheidungstick, Endzustand) wird vom Identitätspin getrennt und zieht in eine Maintainer-Datei; `tools/Nova.SimRunner.Tests/` bekommt erstmals eine Eigentümerzeile | Project Owner / Orchestrator | | 1.0.0 | 2026-07-21 | D-001 bis D-005 aus Sprint 0 protokolliert | Game Director | | 1.1.0 | 2026-07-21 | D-006 (Unity 6.3 LTS + URP bestätigt) aus Sprint-1-Validierung | Lead Technical Director | diff --git a/docs/production/hashkrieg/16_Sprint_Wirtschaft.md b/docs/production/hashkrieg/16_Sprint_Wirtschaft.md index 8b02f2d..2ecf9bc 100644 --- a/docs/production/hashkrieg/16_Sprint_Wirtschaft.md +++ b/docs/production/hashkrieg/16_Sprint_Wirtschaft.md @@ -1,6 +1,6 @@ # Sprint 16: Die Wirtschaft trägt sich selbst — kein Gebäude kostet Geld, ohne etwas zu tun -**Version:** 1.0.0 | **Status:** geplant | **Verantwortungsbereich:** Netzstrang (Maintainer) | **Sprint:** 16 | **Vorgänger:** [12_Sprint_Zu_Zweit.md](12_Sprint_Zu_Zweit.md) Strang C | **Parallel zu:** [13B](13B_Sprint_Einheitenverhalten.md) | **Regelwerk:** [13-15_Parallelbetrieb.md](13-15_Parallelbetrieb.md) | **UX-Gate:** human | **Leitsatz:** ein Gebäude, das Strom zieht und nichts tut, ist kein Platzhalter, sondern ein Schaden +**Version:** 1.1.0 | **Status:** geplant | **Verantwortungsbereich:** Netzstrang (Maintainer) | **Sprint:** 16 | **Vorgänger:** [12_Sprint_Zu_Zweit.md](12_Sprint_Zu_Zweit.md) Strang C | **Parallel zu:** [13B](13B_Sprint_Einheitenverhalten.md) | **Regelwerk:** [13-15_Parallelbetrieb.md](13-15_Parallelbetrieb.md) | **UX-Gate:** human | **Leitsatz:** ein Gebäude, das Strom zieht und nichts tut, ist kein Platzhalter, sondern ein Schaden ## Zweck @@ -197,14 +197,16 @@ und erst damit wird 16.5 spürbar, weil ein Stromausfall die Minimap mitnimmt. |---|---|---| | Feldreserve | 2.000.000 AE (≈ 28 h ununterbrochene Ernte eines Sammlers) | Manifestwerte **9.000 / 15.000 AE** | | Feldanzahl | 2 (je Slot eins) | **5** — 2 Start, 2 Expansion, 1 umkämpftes Zentrum | -| Ernterate | 2 AE/Tick, als Provisorium markiert | gegen die Zielkurve kalibriert | +| Ernterate | 2 AE/Tick, als Provisorium markiert | **bleibt in 16.7 bei 2 AE/Tick**; gespielte Kalibrierung bewusst vertagt (D-102) | **Symmetrie ist Pflicht.** Beide Startpositionen müssen gleich weit zu Expansion und Zentrum liegen — sonst entscheidet die Karte das erste Mensch-gegen-Mensch-Match. -Keiner der drei Zielwerte liegt in `SimDefinitions`: `FieldReserveAE` und die -Feldpositionen stehen in `MatchBootstrap`, `HarvestRateAE` in `EconomySystem`. -Der Definitions-Hash bewegt sich hier **nicht** — das tut nur 16.8. +Feldreserven und Feldpositionen liegen nicht in `SimDefinitions`, sondern in +der kanonischen Startaufstellung. D-102 trennt diese belegte +Knappheitskorrektur ausdrücklich von der noch unbelegten Ernteraten-Kalibrierung; +`HarvestRateAE` bleibt unverändert. Der Definitions-Hash bewegt sich hier +**nicht** — das tut erst 16.8. **Fünf synchrone Stellen** (siehe Regelwerk, „kanonische Startaufstellung"): @@ -329,8 +331,9 @@ dann **16.6**. Jeder Abwurf mit Begründung in den |---|---|---| | D-096 | Lager erhält eine **abgeleitete** AE-Obergrenze (kein Zustandsfeld); Radar schaltet die Minimap frei und leitet seine Abdeckung vom Gebäude ab | Inhaber (Richtung) / Agent (Ausformung) | | D-097 | „Stoppen" löscht den Angriffsbefehl; ein Halte-Feuer bleibt beim Einheitenstrang | Inhaber | +| D-102 | Fünf endliche, punktgespiegelte Aetheriumfelder; `HarvestRateAE` bleibt bis zur gespielten Kalibrierung bei 2 AE/Tick | Inhaber / Agent | -D-096 und D-097 sind im [DecisionLog](../DecisionLog.md) eingetragen. D-098 +D-096, D-097 und D-102 sind im [DecisionLog](../DecisionLog.md) eingetragen. D-098 (Entwurf) und D-099 stehen dort für [Sprint 17](17_Sprint_Zugangsprotokoll.md), D-100 bleibt für dessen Paket B vorgemerkt, D-098 gehört zu [Sprint 14](14_Sprint_Lobby.md). Keine dieser Nummern darf hier verbraucht @@ -354,4 +357,5 @@ Die Baseline-Neusetzung ist Zweck der Tests, kein Bruch. | Version | Datum | Änderung | Autor | |---|---|---|---| +| 1.1.0 | 2026-08-09 | D-102 ergänzt: fünf endliche symmetrische Felder sind Paket 16.7; die Ernterate bleibt mangels gespielter Zielkurve ausdrücklich bei 2 AE/Tick und wird getrennt kalibriert | Project Owner / Agent | | 1.0.0 | 2026-08-09 | Erstfassung: Strang C aus Sprint 12 und die acht Betatest-Befunde im selben Schreibbereich zu einem Sprint zusammengeführt, am Code geprüft und nach Kosten sortiert | Orchestrator | diff --git a/tools/Nova.SimRunner.Tests/CanonicalMatchSetupTests.cs b/tools/Nova.SimRunner.Tests/CanonicalMatchSetupTests.cs index 213e8a2..2d98c20 100644 --- a/tools/Nova.SimRunner.Tests/CanonicalMatchSetupTests.cs +++ b/tools/Nova.SimRunner.Tests/CanonicalMatchSetupTests.cs @@ -69,7 +69,21 @@ public sealed class CanonicalMatchSetupTests private const ushort MapWidth = 128; private const ushort MapHeight = 128; private const int EntityCapacity = 1024; - private const long FieldReserveAE = 2000000L; + private struct FieldLayout + { + public ushort Id; + public int X, Y; + public long ReserveAE; + } + + private static readonly FieldLayout[] FieldLayouts = + { + new FieldLayout { Id = 1, X = 7, Y = 7, ReserveAE = 9000L }, + new FieldLayout { Id = 2, X = 117, Y = 117, ReserveAE = 9000L }, + new FieldLayout { Id = 3, X = 24, Y = 40, ReserveAE = 9000L }, + new FieldLayout { Id = 4, X = 100, Y = 84, ReserveAE = 9000L }, + new FieldLayout { Id = 5, X = 62, Y = 62, ReserveAE = 15000L }, + }; // Faction-resolved opening placement ids (SimDefinitions id rule): // slot 0 Alliance (role value), slot 1 Legion (role value + 17). private const ushort DefHQAlliance = 3; @@ -142,46 +156,47 @@ private static ReferenceHost BuildReferenceHost(ulong seed) /// Fixed opening layout of one slot, in grid cells. private sealed class SlotLayout { - public ushort FieldId; - public int FieldX, FieldY; public int HqOriginX, HqOriginY; public int BuilderX, BuilderY; } private static readonly SlotLayout Slot0Layout = new SlotLayout { - FieldId = 1, FieldX = 7, FieldY = 7, HqOriginX = 4, HqOriginY = 4, BuilderX = 13, BuilderY = 7, }; private static readonly SlotLayout Slot1Layout = new SlotLayout { - FieldId = 2, FieldX = 119, FieldY = 119, HqOriginX = 120, HqOriginY = 120, - BuilderX = 113, BuilderY = 119, + BuilderX = 111, BuilderY = 117, }; /// /// Byte-exact mirror of Determinism10000Scenario.SetupMatch (D-077): - /// per slot one Aetherium field, a completed HQ and ONE Builder — - /// nothing else. Spawn ORDER is load-bearing: EntityManager hands - /// out ids from a deterministic free list, so any reordering shifts - /// every id and therefore every hash. Units spawn through SpawnUnit's - /// defaults (maxHealth 100 for all), exactly like the scenario — - /// NOT through SimDefinitions. + /// five finite Aetherium fields in canonical id order, then per slot a + /// completed HQ and ONE Builder — nothing else. Entity spawn order is + /// load-bearing: EntityManager hands out ids from a deterministic free + /// list, so any reordering shifts every id and therefore every hash. + /// Units spawn through SpawnUnit's defaults (maxHealth 100 for all), + /// exactly like the scenario — NOT through SimDefinitions. /// private static void ApplyOpeningPosition(ReferenceHost host) { // The slot factions are already bound: BuildReferenceHost mirrors // BuildHost, which assigns them before Kernel.Start() (the // SetSlotFaction guard requires it). + for (int i = 0; i < FieldLayouts.Length; i++) + { + FieldLayout field = FieldLayouts[i]; + Assert.That(host.Economy.TryAddField( + field.Id, new GridPos2D(field.X, field.Y), field.ReserveAE), + Is.True, "reference field registration"); + } + for (byte slot = 0; slot < 2; slot++) { SlotLayout c = slot == 0 ? Slot0Layout : Slot1Layout; - - Assert.That(host.Economy.TryAddField(c.FieldId, new GridPos2D(c.FieldX, c.FieldY), FieldReserveAE), - Is.True, "reference field registration"); Assert.That(host.Construction.PlaceCompletedBuilding(slot, slot == 0 ? DefHQAlliance : DefHQLegion, c.HqOriginX, c.HqOriginY).IsValid, Is.True, "reference HQ placement"); @@ -276,6 +291,23 @@ public void ReferenceOpeningPosition_MatchesDeterminism10000ScenarioSetupMatch() "drift here means the Unity host and the headless harness are no longer the same match."); } + [Test] + public void ReferenceOpeningPosition_RegistersFiveFiniteFields() + { + ReferenceHost host = BuildReferenceHost(CanonicalSeed); + ApplyOpeningPosition(host); + + Assert.That(host.Economy.FieldCount, Is.EqualTo(FieldLayouts.Length)); + for (int i = 0; i < FieldLayouts.Length; i++) + { + FieldLayout expected = FieldLayouts[i]; + Assert.That(host.Economy.TryGetField(expected.Id, out AetheriumField actual), Is.True); + Assert.That(actual.GridPos.X, Is.EqualTo(expected.X)); + Assert.That(actual.GridPos.Y, Is.EqualTo(expected.Y)); + Assert.That(actual.RemainingAE, Is.EqualTo(expected.ReserveAE)); + } + } + [Test] public void ReferenceOpeningPosition_IsSeedSensitive() { diff --git a/tools/Nova.SimRunner/Determinism10000Scenario.cs b/tools/Nova.SimRunner/Determinism10000Scenario.cs index 49db807..23b0e43 100644 --- a/tools/Nova.SimRunner/Determinism10000Scenario.cs +++ b/tools/Nova.SimRunner/Determinism10000Scenario.cs @@ -136,7 +136,8 @@ internal sealed class DeterminismComparison /// MS-1 manifest start state of quality/content/mvp-v1.json /// (startStatePerPlayer) per slot — a COMPLETED HQ, ONE Builder and /// 3.000 AE (EconomySystem.CanonicalMatchStartingCreditsAE, wired by - /// ) — plus one Aetherium field per slot. Nothing + /// ) — plus five finite canonical Aetherium fields. + /// Nothing /// else is spawned: the script then drives the opening exactly like a /// player, for BOTH slots — walk the Builder to the future site, place /// the Refinery once it is affordable and the committed grid covers its @@ -184,7 +185,30 @@ internal static class Determinism10000Scenario private const byte HumanSlot = 0; private const byte AiSlot = 1; private const int EntityCapacity = 1024; - private const long FieldReserveAE = 2000000L; + /// One Aetherium field of the canonical map (16.7, C1). + private struct FieldLayout + { + public ushort Id; + public int X, Y; + public long ReserveAE; + } + + /// + /// The five canonical fields (16.7, C1 — MVPContentManifest section 5): + /// two start fields and two natural expansions at 9.000 AE each, one + /// contested centre at 15.000. Every slot-1 coordinate is the point + /// mirror of slot 0 through the map centre ((x, y) -> + /// (124 - x, 124 - y)). Registration in ascending id order is part of + /// the canonical initial state. + /// + private static readonly FieldLayout[] FieldLayouts = + { + new FieldLayout { Id = 1, X = 7, Y = 7, ReserveAE = 9000L }, + new FieldLayout { Id = 2, X = 117, Y = 117, ReserveAE = 9000L }, + new FieldLayout { Id = 3, X = 24, Y = 40, ReserveAE = 9000L }, + new FieldLayout { Id = 4, X = 100, Y = 84, ReserveAE = 9000L }, + new FieldLayout { Id = 5, X = 62, Y = 62, ReserveAE = 15000L }, + }; /// /// Faction-resolved definition id of a role for the given slot @@ -601,25 +625,26 @@ private static void IssueSlotCommands( RefineryRallyX = 7, RefineryRallyY = 6, }; - /// Slot 1 base layout (top-right), the 180-degree mirror of slot 0. + /// Slot 1 base layout (top-right), the exact centre mirror of slot 0. private static readonly SlotLayout Slot1Layout = new SlotLayout { - FieldId = 2, FieldX = 119, FieldY = 119, + FieldId = 2, FieldX = 117, FieldY = 117, HqOriginX = 120, HqOriginY = 120, - BuilderSpawnX = 113, BuilderSpawnY = 119, - RefineryOriginX = 116, RefineryOriginY = 120, RefineryBuildX = 116, RefineryBuildY = 119, - RefineryRallyX = 119, RefineryRallyY = 120, + BuilderSpawnX = 111, BuilderSpawnY = 117, + RefineryOriginX = 114, RefineryOriginY = 118, RefineryBuildX = 114, RefineryBuildY = 117, + RefineryRallyX = 117, RefineryRallyY = 118, }; /// - /// Applies the deterministic match setup to a fresh host: per slot - /// the D-077 start state of quality/content/mvp-v1.json + /// Applies the deterministic match setup to a fresh host: the five + /// canonical fields in ascending id order, then per slot the D-077 + /// start state of quality/content/mvp-v1.json /// (startStatePerPlayer) — a COMPLETED HQ, ONE Builder and the 3.000 /// AE of - /// (wired by ) — plus one Aetherium field. No + /// (wired by ). No /// pre-placed Refinery, no Harvesters, no skirmish squad: the loop - /// start is scripted, not spawned. Deterministic spawn order (field, - /// HQ, Builder; slot 0 first) means identical entity ids on every + /// start is scripted, not spawned. Deterministic entity order (HQ, + /// Builder; slot 0 first) means identical entity ids on every /// host and platform. The slot factions are already bound — /// assigns them before Kernel.Start(), /// which the guard @@ -628,13 +653,17 @@ private static void IssueSlotCommands( private static SlotState[] SetupMatch(Host host) { var slots = new[] { new SlotState(), new SlotState() }; - for (byte slot = 0; slot < 2; slot++) + for (int f = 0; f < FieldLayouts.Length; f++) { - SlotLayout c = slot == HumanSlot ? Slot0Layout : Slot1Layout; - if (!host.Economy.TryAddField(c.FieldId, new GridPos2D(c.FieldX, c.FieldY), FieldReserveAE)) + FieldLayout field = FieldLayouts[f]; + if (!host.Economy.TryAddField(field.Id, new GridPos2D(field.X, field.Y), field.ReserveAE)) { - throw new InvalidOperationException($"field {c.FieldId} could not be registered"); + throw new InvalidOperationException($"field {field.Id} could not be registered"); } + } + for (byte slot = 0; slot < 2; slot++) + { + SlotLayout c = slot == HumanSlot ? Slot0Layout : Slot1Layout; if (!host.Construction.PlaceCompletedBuilding(slot, DefId(host, slot, UnitRole.HQ), c.HqOriginX, c.HqOriginY).IsValid) { throw new InvalidOperationException("HQ placement failed");