diff --git a/Assets/Tests/EditMode/Gameplay/CanonicalMatchSetupTests.cs b/Assets/Tests/EditMode/Gameplay/CanonicalMatchSetupTests.cs index a1c9c23..65a4e56 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/Data/Maps/MAP_Glutrinne.asset b/Assets/_Project/Data/Maps/MAP_Glutrinne.asset index dc5fe34..0c117ad 100644 --- a/Assets/_Project/Data/Maps/MAP_Glutrinne.asset +++ b/Assets/_Project/Data/Maps/MAP_Glutrinne.asset @@ -21,4 +21,7 @@ MonoBehaviour: - {x: 121, y: 121} _resourceNodes: - {x: 7, y: 7} - - {x: 119, y: 119} + - {x: 117, y: 117} + - {x: 24, y: 40} + - {x: 100, y: 84} + - {x: 62, y: 62} diff --git a/Assets/_Project/Editor/BootstrapSceneGenerator.cs b/Assets/_Project/Editor/BootstrapSceneGenerator.cs index 935ab1f..dd442e6 100644 --- a/Assets/_Project/Editor/BootstrapSceneGenerator.cs +++ b/Assets/_Project/Editor/BootstrapSceneGenerator.cs @@ -269,8 +269,8 @@ private static void CreateMapObject(MatchRunner runner, GameObject ground) /// /// The data-layer map asset. It records exactly the graybox-accurate /// subset of the Glutrinne manifest layout — the two spawn points and - /// the two aetherium fields the canonical match actually registers. - /// The full five-field layout with both primary routes is G4 scope + /// the five aetherium fields the canonical match registers since + /// Sprint 16.7. Primary-route dressing remains G4 scope /// (docs/production/ScopeLedger.md) and is deliberately not invented here. /// private static void EnsureGlutrinneMapAsset() @@ -292,8 +292,15 @@ private static void EnsureGlutrinneMapAsset() 128, // HQ footprint centres of the canonical opening (4,4)+(3x3) and its 180° mirror. new[] { new Vector2(5f, 5f), new Vector2(121f, 121f) }, - // The two fields MatchBootstrap registers: local (7,7), enemy (119,119). - new[] { new Vector2(7f, 7f), new Vector2(119f, 119f) }); + // The five fields MatchBootstrap registers, in canonical id order. + new[] + { + new Vector2(7f, 7f), + new Vector2(117f, 117f), + new Vector2(24f, 40f), + new Vector2(100f, 84f), + new Vector2(62f, 62f), + }); EditorUtility.SetDirty(map); } diff --git a/Assets/_Project/Scripts/Gameplay/Match/MatchBootstrap.cs b/Assets/_Project/Scripts/Gameplay/Match/MatchBootstrap.cs index 66ba566..0a2d431 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 e38d655..3391cb3 100644 --- a/Assets/_Project/Scripts/Simulation/Economy/EconomySystem.cs +++ b/Assets/_Project/Scripts/Simulation/Economy/EconomySystem.cs @@ -129,7 +129,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 e883c69..d377a05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,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 - **Low Power ist eine Waffe (C4, Sprint 16.6)** — bei Energiedefizit fällt Radar zuerst: `FogOfWarSystem.GetRadarSignatures` liefert nichts mehr diff --git a/README.md b/README.md index 44fa2a5..00948c7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Project Nova -**Dokumentversion:** 0.20.0 | **Status:** unveröffentlichter Entwicklungsstand, spielbar | **Verantwortungsbereich:** Executive Producer / Technical Writer | **Stand:** 2026-08-10 +**Dokumentversion:** 0.21.0 | **Status:** unveröffentlichter Entwicklungsstand, spielbar | **Verantwortungsbereich:** Executive Producer / Technical Writer | **Stand:** 2026-08-10 > Ein Echtzeitstrategiespiel in der Tradition von **Command & Conquer** — Basisbau, > Ernte, Armee, Karte kontrollieren. Gebaut mit Unity und C#, offen entwickelt. @@ -156,9 +156,10 @@ funktionierender Loop: - **Gefechtsdichte ist noch nicht manuell abgenommen.** VFX und SFX sind implementiert; die geplante Sicht- und Gegenhörprüfung mit 60 Einheiten bleibt offen. -- **Strang C ist offen.** Endliche Aetheriumfelder, wirksames Lager und Radar, - Low-Power-Wirkung sowie die übrigen Wirtschafts- und Platzierungsregeln sind - noch nicht umgesetzt. +- **Strang C läuft weiter.** Fünf endliche Aetheriumfelder, Lagergrenze, Radar + und Low-Power-Wirkung sind umgesetzt; Mehrfachvoraussetzungen, + Platzierungsregeln, Reparaturkosten und ehrliches Blocker-Feedback folgen + noch in den nachgelagerten Paketen. - **Kein Speichern.** Die Simulation kann ihren Zustand vollständig serialisieren und hash-identisch fortsetzen — es fehlt nur das Schreiben auf die Platte. @@ -284,10 +285,9 @@ Runde*. - **Netzwerk und Gefechtsdichte sind noch nicht vollständig manuell abgenommen.** A8 Stufen 2–4 sowie die 60-Einheiten-Sicht-/Gegenhörprüfung bleiben offen. -- **Strang C ist offen.** Wirtschaftsdruck, Low Power und die ausstehenden - Gebäude- und Platzierungsregeln fehlen noch. -- **Lager und Radar kosten Geld und tun nichts.** Zwei von neun Gebäuden warten - noch auf ihre Wirkung. +- **Strang C läuft weiter.** Wirtschaftsdruck, Lager, Radar und Low Power sind + da; Mehrfachvoraussetzungen, Platzierungsregeln, Reparaturkosten und das + vollständige Entscheidungsfeedback fehlen noch. Die vollständigen Grenzen stehen im aktuellen [Sprintbericht](docs/production/hashkrieg/12_Sprint_Zu_Zweit.md). @@ -434,7 +434,9 @@ nicht rückwirkend übertragen. ## Offene Punkte -- **Die Wirtschaftsfrage aus §2** ist die wichtigste offene Entscheidung. +- **Die verbleibenden Strang-C-Pakete aus §2** — Voraussetzungen, + Platzierung/Reparaturkosten und Entscheidungsfeedback — sind die wichtigste + offene Umsetzungsfolge. - Der Umbenennungsbeschluss auf *Hashkrieg* ist im Bestand dieses Repositories noch nicht vollzogen — Repo, Code und Wiki laufen weiter unter *Project Nova*. - Q-018 (Preis) und Q-019 (Telemetrie) bleiben offen und blockieren MS-1 nicht. @@ -447,12 +449,12 @@ Zur Bewertung, in dieser Reihenfolge: 2–4; Linux/systemd und der Live-Workflow müssen ebenfalls real laufen. 2. **Gefechtsfeedback manuell abnehmen** — ungefähr 60 feuernde Einheiten, SFX-Regler, Klangbalance und Kamera-Listener gegenhören und ansehen. -3. **Wirtschaftsdruck** — endliche Aetheriumfelder geben der Runde einen Bogen und - einen Grund, um Gebiet zu kämpfen. +3. **Strang C abschließen** — Mehrfachvoraussetzungen, + Platzierungs-/Reparaturregeln und ehrliches Blocker-Feedback integrieren. 4. **Attack-Move** — Truppen sollen unterwegs Gegner bekämpfen, ohne jeden Kontakt einzeln befohlen zu bekommen. -5. **Gebäude mit Wirkung und KI-Ausbau** — Lager und Radar warten auf ihre - Funktion; der Gegner spielt, aber schlicht. +5. **KI-Ausbau** — der Gegner spielt, aber weiterhin schlicht; die neuen + Wirtschaftsregeln müssen später auch in sein strategisches Verhalten einfließen. Die Gate-Kette G0–G5 ruht unter Tier 2 und wird erst wieder aufgenommen, wenn das Projekt ein Publikum hat. @@ -465,6 +467,7 @@ Projekt ein Publikum hat. | 0.19.0 | 2026-08-08 | D-091: Source-available Lizenz, CLA und Tier-2-Beitragsmodell ergänzt; die zwei Maintainer mit Merge-Recht und die Asset-/Markenabgrenzung klar benannt | Technical Writer | | 0.19.1 | 2026-08-08 | CLA-Wirkung auf Beiträge mit dokumentierter Zustimmung begrenzt; keine rückwirkende Rechteübertragung unterstellt | Technical Writer | | 0.20.0 | 2026-08-10 | D-105: alleinige Projektleitung und Merge-Autorität von `@cubetribe`, externe aktuelle Inhaberfreigabe sowie die ehrliche Zurückstellung manueller Spielabnahmen dokumentiert | Technical Writer | +| 0.21.0 | 2026-08-10 | Strang-C-Status auf #73/#77/#78/#80 nachgezogen: Lagergrenze, Radar, Low Power und fünf endliche Aetheriumfelder sind umgesetzt; die tatsächlich verbleibenden Pakete bleiben offen | Codex / Dennis Westermann | | 0.7.1 | 2026-07-24 | Recovery-Baseline nach Implementierungs-Audit | Executive Producer / Lead Technical Director | | 0.8.0 | 2026-07-24 | Closed-Core MS-1, exakten Engine-Pin, G0-offenen Status und Quality-Verträge D-056–D-061 aufgenommen | Executive Producer / Technical Writer | | 0.8.1 | 2026-07-24 | Evidence-Semantikvalidator ergänzt und Dokumentstruktur korrigiert | Technical Writer / Lead QA Engineer | diff --git a/docs/README.md b/docs/README.md index f09bc65..64e1334 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ # Project Nova – Entwicklungs-Wiki -**Version:** 0.19.0 | **Status:** unveröffentlichter Arbeitsstand – Lobby (Sprint 14) client- und relay-seitig implementiert und getestet; Supabase-Anlage, Relay-Redeploy und gespielte Abnahme offen | **Verantwortungsbereich:** Executive Producer / Technical Writer | **Sprint:** 16 +**Version:** 0.20.0 | **Status:** unveröffentlichter Arbeitsstand – Lobby (Sprint 14) client- und relay-seitig implementiert und getestet; Supabase-Anlage, Relay-Redeploy und gespielte Abnahme offen | **Verantwortungsbereich:** Executive Producer / Technical Writer | **Sprint:** 16 ## Zweck @@ -183,7 +183,7 @@ gelisteten Kernverträge. - [RiskAnalysis](production/RiskAnalysis.md) - [GrayboxLog](production/GrayboxLog.md) – Sitzungsprotokoll der Graybox-Spur (D-067, Entwurf) - [ScopeLedger](production/ScopeLedger.md) – Zurückstellungen der Graybox-Spur, verweist auf Manifest-Schlüsselpfade -- [DemoRunbook](production/DemoRunbook.md) (0.1.0, Entwurf) – erste Demo-Runde: Ablauf, Steuerung, bekannte Grenzen, Asset-Ablage +- [DemoRunbook](production/DemoRunbook.md) (0.6.0, Entwurf) – erste Demo-Runde: Ablauf, Steuerung, fünf endliche Aetheriumfelder, bekannte Grenzen und Asset-Ablage - [StatusSnapshot 2026-08-05](production/StatusSnapshot_2026-08-05.md) (0.1.0) – datierter Projektstand vor dem Eintreffen der ersten 3D-Assets - [Hashkrieg-Planungsmappe](production/hashkrieg/README.md) und [Sprint 12 „Zu zweit"](production/hashkrieg/12_Sprint_Zu_Zweit.md) – @@ -273,3 +273,4 @@ kann keine Datei einen Gate-Pass erzeugen. | 0.17.0 | 2026-08-08 | D-091 und Sprint 13.0 indexiert: Tier-2-Beitragsmodell, Lizenz- und Merge-Schutz vorbereitet | Technical Writer | | 0.18.0 | 2026-08-09 | Sprint-14-Lobby indexiert: LobbySupabase.md (Vertrag, Schema, Edge-Function-Referenzen, Betriebspfad) und RelayServer.md 1.1.0 (kurzlebige Lobby-Tokens) aufgenommen, D-092 bis D-094; Supabase-Anlage, Relay-Redeploy und gespielte Abnahme ausdrücklich offen | Agent (Umsetzung) | | 0.19.0 | 2026-08-10 | D-105 indexiert: Dennis Westermann ist alleiniger Projektinhaber, Tier-Entscheider und Mergeberechtigter; Tier 2 und die externen CLA-/Review-Regeln bleiben aktiv | Technical Writer | +| 0.20.0 | 2026-08-10 | DemoRunbook 0.6.0 indexiert: D-102/Sprint 16.7 ersetzt die alte Zwei-Feld-Demo durch fünf endliche, sichtbare Aetheriumfelder | Codex / Dennis Westermann | diff --git a/docs/production/DecisionLog.md b/docs/production/DecisionLog.md index 4d9d73d..0ba7eb5 100644 --- a/docs/production/DecisionLog.md +++ b/docs/production/DecisionLog.md @@ -1,6 +1,6 @@ # Decision Log -**Version:** 1.35.0 | **Status:** aktiv (laufend) | **Verantwortungsbereich:** Game Director / Lead Technical Director / Project Owner | **Sprint:** 16 +**Version:** 1.36.0 | **Status:** aktiv (laufend) | **Verantwortungsbereich:** Game Director / Lead Technical Director / Project Owner | **Sprint:** 16 ## Zweck @@ -3005,6 +3005,52 @@ 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. + +--- + ### D-105 | verbindlich | Sprint 16 (alleinige Projektleitung und Merge-Autorität) **Status:** unmittelbar wirksame Inhaberentscheidung vom 2026-08-10 (Dennis @@ -3264,6 +3310,7 @@ Konvergenz, `long.MaxValue` und die Ablehnung des alten Rules-Stubs ab. | Version | Datum | Änderung | Autor | |---|---|---|---| +| 1.36.0 | 2026-08-10 | 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.35.0 | 2026-08-10 | D-106 aufgenommen: einmalige 2.000-AE-HQ-Basis, +2.000 je fertigem Lager, sofort gedeckelte Einzahlungen, zustandsloser 25-%-Abbau des aktuellen Überhangs je Sekunde und kanonischer Rules-Hash; D-024/D-096 in Verlustausformung, HQ-Stapelung und Replay-Kompatibilität teilweise ersetzt; Fehlverweise der Lobbyfamilie D-095–D-097 auf D-092–D-094 berichtigt | Agent (unter Delegation) / Dennis Westermann | | 1.34.0 | 2026-08-10 | D-105 aufgenommen: Dennis Westermann ist alleiniger Projektinhaber, Tier-Entscheider und Mergeberechtigter; Tier 2 bleibt mit CLA und aktueller Inhaberfreigabe aktiv, Inhaber-PRs dürfen nach grüner Pflicht-CI und unabhängigem Review selbst gemergt werden, und manuelle Spielabnahme darf ehrlich zurückgestellt, aber nicht als gelaufen behauptet werden | Project Owner / Orchestrator | | 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 | diff --git a/docs/production/DemoRunbook.md b/docs/production/DemoRunbook.md index 6f48149..52e3562 100644 --- a/docs/production/DemoRunbook.md +++ b/docs/production/DemoRunbook.md @@ -1,6 +1,6 @@ # Demo-Runbook – erste spielbare Runde (Glutrinne-Graybox) -**Version:** 0.5.0 | **Status:** Entwurf – Graybox-Spur, kein Gate-Nachweis | **Verantwortungsbereich:** Producer / Technical Writer | **Sprint:** 7 +**Version:** 0.6.0 | **Status:** Entwurf – Graybox-Spur, kein Gate-Nachweis | **Verantwortungsbereich:** Producer / Technical Writer | **Sprint:** 16 ## Zweck @@ -42,7 +42,7 @@ Gate-Status steht ausschließlich in [MVPRecoveryPlan.md](MVPRecoveryPlan.md). Musiklautstärke in den Einstellungen auf 0 oder Musik ist ausgeschaltet — siehe §7. -## 2. Was die Demo zeigt (Spielstand GB-005, D-077 + Hauptmenü, D-083) +## 2. Was die Demo zeigt (Spielstand Sprint 16.7, D-077/D-102 + Hauptmenü, D-083) - **Hauptmenü mit Musik (D-083):** Key Art im Vollbild, Titel „HASHKRIEG", vier Einträge — **Neues Spiel / Laden / Einstellungen / Beenden**. „Laden" ist @@ -51,8 +51,10 @@ Gate-Status steht ausschließlich in [MVPRecoveryPlan.md](MVPRecoveryPlan.md). Die Einstellungen überleben den Neustart (§7). Es ist das erste UI-Toolkit-UI des Projekts; das übrige HUD bleibt bis auf Weiteres OnGUI-Wegwerfcode. - **Karte „Glutrinne" (Blockout):** Wüstengetönte 128×128-Ebene, dunkler - Kartenrand-Rahmen, Aetherium-Kristallmarker (cyan) auf den beiden Feldern, - die das kanonische Match registriert (lokal (7,7), gegnerisch (119,119)). + Kartenrand-Rahmen und Aetherium-Kristallmarker (cyan) auf allen fünf + kanonischen Feldern: Start `(7,7)` / `(117,117)` und Expansion + `(24,40)` / `(100,84)` mit je 9.000 AE sowie Zentrum `(62,62)` mit + 15.000 AE (D-102). - **Startaufstellung je Slot:** HQ + 1 Builder + 3.000 AE. Slot 0 (Mensch) = Allianz, Slot 1 = Legion. Mehr gibt es nicht — der Kernloop wird gespielt, nicht geschenkt. @@ -179,8 +181,8 @@ steht davor und dauert so lange, wie man ihn zeigen will. Fraktionswahl hängt an `InitialStateHash` und wäre eine Determinismus-, keine Menü-Änderung (D-083). - Aetherium-Felder sind endlich, aber statisch (kein Nachwachsen, keine - Warnung); das Manifest-Layout mit 5 Feldern und 2 Angriffswegen ist G4-Scope - – der Blockout zeigt bewusst nur die zwei real registrierten Felder. + Warnung). Das Fünf-Feld-Layout ist seit D-102 registriert und sichtbar; + die zwei ausgearbeiteten Angriffswege bleiben G4-Scope. - Erledigt seit GB-005 (hier nur als Historie): das Vollbild-Debug-Overlay ist standardmäßig aus (F3); die 3D-Modelle überlagern sich nicht mehr (Laufzeit-Normierung auf den Sim-Footprint); der KI-Slot spielt; der @@ -272,3 +274,4 @@ der Editor in der Konsole, wenn das Schreiben fehlschlägt. | 0.3.0 | 2026-08-06 | Stand GB-005 (D-077): Start HQ + Builder + 3.000 AE, Kernloop-Ablauf neu (Raffinerie → Harvester → Kaserne), KI-Gegner aktiv, Sieg bei HQ-Zerstörung, Statusleiste + F3-Panel, Skalierungsreparatur vermerkt | Agent | | 0.4.0 | 2026-08-06 | Hauptmenü (D-083): §1 korrigiert – Play zeigt das Menü, das Match startet über „Neues Spiel" (`AutoStart = false`), nicht mehr von selbst; §2 um Menü, Key Art und Menümusik ergänzt; §4 um einen Menüschritt vorangestellt und durchnummeriert (Zeitmarken zählen ab Matchstart); §5 um „Laden" ausgegraut, wirkungslosen SFX-Regler, gemeinsames URP-Asset über alle sechs Render-Detail-Stufen und fehlenden Rückweg ins Menü erweitert; neues §7 zu `settings.json` in `Application.persistentDataPath` (Inhalt, Zurücksetzen, Verhalten bei kaputter Datei) | Agent | | 0.5.0 | 2026-08-06 | Bedienbares HUD (D-084): §2 um Bauleiste/Ghost-Platzierung, Command Card, Minimap, sichtbaren Nebel und Selektionsmarker ergänzt; §3 um MMB-Drag-Rotation, Space-Reset und die tastaturfreien Wege über Bauleiste/Command Card erweitert | Agent | +| 0.6.0 | 2026-08-10 | D-102/Sprint 16.7 nachgezogen: fünf endliche Aetheriumfelder samt Markerpositionen und Reserven ersetzen die alte Zwei-Feld-Beschreibung; Angriffswege bleiben G4-Scope | Codex / Dennis Westermann | diff --git a/docs/production/ScopeLedger.md b/docs/production/ScopeLedger.md index 23267bd..34a6db5 100644 --- a/docs/production/ScopeLedger.md +++ b/docs/production/ScopeLedger.md @@ -1,6 +1,6 @@ # Scope-Ledger der Graybox-Spur -**Version:** 0.6.0 | **Status:** laufendes Register – Graybox-Entwurf D-067 plus verbindliche 12B-Abweichungen D-090 | **Verantwortungsbereich:** Orchestrator / Technical Writer | **Sprint:** 12 +**Version:** 0.7.0 | **Status:** laufendes Register – Graybox-Entwurf D-067 plus verbindliche 12B-Abweichungen D-090 und D-102-Fünf-Feld-Stand | **Verantwortungsbereich:** Orchestrator / Technical Writer | **Sprint:** 16 ## Zweck @@ -41,7 +41,7 @@ funktionalen Anteil, das genannte Gate den vollständigen Inhalt. |---|---|---|---| | `startStatePerPlayer.unitRoles` | Startaufstellung des Determinismus-Szenarios portiert; zusätzlich vier Infanterieeinheiten je Slot, damit überhaupt etwas zu sehen ist | G4 | D-067 K1, K2 | | `map.id`, `map.biome` | die Karte heißt seit GB-003 Glutrinne und zeigt einen Wüsten-Blockout (Sandtönung, Kartenrand-Rahmen, Kristallmarker – reine Präsentation über `GlutrinneBlockoutView` plus Datenasset `MAP_Glutrinne.asset`); weiterhin kein Terrain-/Biom-System und keine Hindernisse, nur die Kantenlänge stimmt | G4 (G2: technisch korrektes Testlayout) | D-067 K1, K2 | -| `map.aetheriumFields` | zwei Felder an festen Zellen nahe den Startbasen statt der im Manifest festgelegten Feldliste; seit GB-003 als Kristallmarker sichtbar, Reserven und Verhalten unverändert | G4 (G2) | D-067 K1, K2 | +| `map.aetheriumFields` | seit D-102 sind fünf endliche Felder in Manifestanzahl und -reserven an festen, punktgespiegelten Zellen registriert und als Kristallmarker sichtbar; Startaufstellung, Headless-Szenario, Datenasset und Tests sind synchron, aber eine gespielte Abnahme beziehungsweise ein G4-Nachweis fehlt weiterhin | G4 (G2) | D-067 K1, K2; D-102 | | `map.primaryRouteCount` | keine Routenführung; die Ebene ist überall passierbar | G4 (G2) | D-067 K1, K2 | | `factions[1]` | seit dieser Sitzung Simulationswirklichkeit: 34 fraktionsaufgelöste Definitionen (`SimDefinitions`, Id-Regel und Provenienz per D-075), Slot-Fraktion im Economy-Snapshotblock v2 mit `SetSlotFaction`-Guard, fraktionsaufgelöste Kosten/Bauzeiten/Energie/Waffenwerte, Graybox-Farben aus den D-072-Paletten im `UnitViewManager` und Slot-Fraktionen im Debug-HUD. Verbleibend: das `weaponProfile` der Identität (eigene Zeile) und der untätige KI-Slot (eigene Zeile). Kein Gate-Nachweis — die Zeile bleibt bis zur auflösenden Evidence | G4 | D-067 K1, K2 | | `factions[1].identity.harvesterCargoAE` | seit dieser Sitzung im Code erfüllt: die Kapazität lebt in der Harvester-Definitionszeile (`SimUnitDefinition.CargoCapacityAE`), das `EconomySystem` klammert die Ernte fraktionsaufgelöst; die Entity-Store-Snapshotvalidierung deckelt auf das fraktionsübergreifende Maximum und bietet die pro-Entity-Fraktionsgrenze als Überladung. Kein Gate-Nachweis — die Zeile bleibt bis zur auflösenden Evidence | G4 | D-067 K1, K2 | @@ -152,3 +152,4 @@ implementierten Gefechtsfeedback fest. D-090 ist die führende Entscheidung. | 0.4.0 | 2026-08-05 | Sitzung GB-003: Zeilen `map.id`/`map.biome` und `map.aetheriumFields` auf den Stand des Glutrinne-Blockouts fortgeschrieben (Wüsten-Präsentation und Kristallmarker, weiterhin kein Terrain-System und nur zwei registrierte Felder; nicht entfernt – es gibt keinen auflösenden Gate-Nachweis) | Technical Writer | | 0.5.0 | 2026-08-05 | Sitzung GB-004: Zeile `persistence.pauseRequired` fortgeschrieben (Pause an P gebunden; kein Pausenmenü – Zeile bleibt bis G2) | Technical Writer | | 0.6.0 | 2026-08-08 | Separaten D-090-Abschnitt mit sämtlichen bekannten Abweichungen des ausgeführten Sprint-12-Strangs B ergänzt; Graybox-Hauptregister und Manifestverweise unverändert erhalten | Technical Writer / Agent (Umsetzung) | +| 0.7.0 | 2026-08-10 | `map.aetheriumFields` auf D-102/Sprint 16.7 fortgeschrieben: fünf endliche Felder sind registriert, sichtbar und automatisiert gespiegelt; mangels gespielter Abnahme bleibt die Zeile bis zum Gate-Nachweis bestehen | Codex / Dennis Westermann | diff --git a/docs/production/hashkrieg/16_Sprint_Wirtschaft.md b/docs/production/hashkrieg/16_Sprint_Wirtschaft.md index 27b1e3f..ed11f2c 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.3.0 | **Status:** in Umsetzung | **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.4.0 | **Status:** in Umsetzung | **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 @@ -236,14 +236,16 @@ aktuellen Host aber vor Tick 1 mit `RulesHash64`-Mismatch abgelehnt. |---|---|---| | 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"): @@ -368,9 +370,10 @@ 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-106 | AE-Kontobasis gilt einmalig je Slot; vorhandener Überhang zerfällt zustandslos pro Sekunde und die Regelrevision wird im Match-Fingerprint gebunden | Agent unter Inhaberdelegation | -D-096, D-097 und D-106 sind im [DecisionLog](../DecisionLog.md) eingetragen. D-098 +D-096, D-097, D-102 und D-106 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 @@ -396,6 +399,7 @@ kein Bruch. | Version | Datum | Änderung | Autor | |---|---|---|---| +| 1.4.0 | 2026-08-10 | 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.3.0 | 2026-08-10 | C4-Kompatibilitätsgrenze dokumentiert: Low-Power-Reparatur bindet Rules-Revision 2 und 10/5 HP pro Tick, ohne Zustands- oder Schema-Bump | Codex / Dennis Westermann | | 1.2.0 | 2026-08-10 | D-106 für 16.4 festgeschrieben: einmalige HQ-Kontobasis, periodischer 25-%-Abbau des aktuellen Überhangs und Rules-Hash-Kompatibilitätsgrenze | Codex / Dennis Westermann | | 1.1.0 | 2026-08-10 | D-105-Integrationsausnahme für 16.3 dokumentiert: aktive Sites sind keine Kampfteilnehmer oder fertigen KI-Produzenten | Codex / Dennis Westermann | diff --git a/tools/Nova.SimRunner.Tests/CanonicalMatchSetupTests.cs b/tools/Nova.SimRunner.Tests/CanonicalMatchSetupTests.cs index 337b67e..2bf06a8 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 cbaf834..a8fbdf7 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");