Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Assets/Tests/EditMode/AI/SkirmishAiTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ private static AiHost BuildAiHost(ulong seed)
var economy = new EconomySystem(entities, EconomySystem.CanonicalMatchStartingCreditsAE);
var construction = new ConstructionSystem(entities, economy, pathfinding.CostField);
var production = new ProductionSystem(entities, economy, construction);
var fogOfWar = new FogOfWarSystem(entities, construction, teamCount: 2, MapWidth, MapHeight);
var fogOfWar = new FogOfWarSystem(entities, construction, economy, teamCount: 2, MapWidth, MapHeight);
var combat = new CombatSystem(entities, fogOfWar, economy, construction);
var victory = new VictorySystem(entities, construction);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ private static ReferenceHost BuildReferenceHost(ulong seed)
var economy = new EconomySystem(entities, EconomySystem.CanonicalMatchStartingCreditsAE);
var construction = new ConstructionSystem(entities, economy, pathfinding.CostField);
var production = new ProductionSystem(entities, economy, construction);
var fogOfWar = new FogOfWarSystem(entities, construction, teamCount: 2, MapWidth, MapHeight);
var fogOfWar = new FogOfWarSystem(entities, construction, economy, teamCount: 2, MapWidth, MapHeight);
var combat = new Nova.Simulation.Combat.CombatSystem(entities, fogOfWar, economy, construction);
var victory = new Nova.Simulation.Victory.VictorySystem(entities, construction);

Expand Down
4 changes: 2 additions & 2 deletions Assets/Tests/EditMode/Simulation/CombatSystemTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public static TestHost Create(ulong seed, int capacity = 64, ushort width = 64,
// FoW radar read also requires the placement register.
var factions = new EconomySystem(entities);
var construction = new Nova.Simulation.Construction.ConstructionSystem(entities, factions);
var fog = new FogOfWarSystem(entities, construction, teamCount: 2, width, height);
var fog = new FogOfWarSystem(entities, construction, factions, teamCount: 2, width, height);
var combat = new CombatSystem(entities, fog, factions, construction);

var kernel = new SimulationKernel(new SimRandom(seed));
Expand Down Expand Up @@ -644,7 +644,7 @@ public static ReplayHost Create(ulong seed)
var economy = new EconomySystem(entities);
// 16.5: the FoW radar read requires the placement register.
var construction = new Nova.Simulation.Construction.ConstructionSystem(entities, economy);
var fog = new FogOfWarSystem(entities, construction, teamCount: 2, 64, 64);
var fog = new FogOfWarSystem(entities, construction, economy, teamCount: 2, 64, 64);
var combat = new CombatSystem(entities, fog, economy, construction);

var kernel = new SimulationKernel(new SimRandom(seed));
Expand Down
23 changes: 23 additions & 0 deletions Assets/Tests/EditMode/Simulation/ConstructionSystemTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,8 @@ public void SellStorage_CapsRefundThenLoweredCapacityDrivesExcessDecay()
public void Repair_BuilderRestoresHp_InReachOnly_AndResolvesAtFull()
{
var f = new Fixture();
Assert.That(f.Construction.PlaceCompletedBuilding(0, 5, 40, 40).IsValid, Is.True,
"a completed power plant keeps the normal repair rate active");
EntityId barracks = f.Construction.PlaceCompletedBuilding(0, 7, 20, 20);
uint raw = UnitCommandStateView.ToRawEntityId(barracks);
f.Entities.GetUnitRef(barracks).CurrentHealth = 100;
Expand All @@ -803,6 +805,27 @@ public void Repair_BuilderRestoresHp_InReachOnly_AndResolvesAtFull()
"repair caps at MaxHealth and the order resolves");
}

[Test]
public void Repair_LowPower_ExactlyHalvesTheRate()
{
// 16.6 (C4, Economy.md repair rule): under LOW POWER the repair
// rate halves exactly — 5 HP per tick, no rounding.
var f = new Fixture();
Assert.That(f.Construction.PlaceCompletedBuilding(0, 4, 40, 40).IsValid, Is.True,
"a completed Refinery (20 required, nothing provided) forces low power");
EntityId barracks = f.Construction.PlaceCompletedBuilding(0, 7, 20, 20);
f.Entities.GetUnitRef(barracks).CurrentHealth = 100;

EntityId builder = f.SpawnBuilder(0, 19, 20);
f.Step(1); // commit the balance: refinery + barracks draw 35, nothing provided
Assert.That(f.Economy.GetPlayerEconomy(0).IsLowPower, Is.True);

f.Construction.AssignRepairOrder(UnitCommandStateView.ToRawEntityId(builder), UnitCommandStateView.ToRawEntityId(barracks));
f.Step(10);
Assert.That(f.Entities.GetUnitRef(barracks).CurrentHealth, Is.EqualTo(150),
"5 HP per tick under low power — exactly half the provisional rate");
}

[Test]
public void Repair_Validation_RejectsNonBuilder_AndUndamagedTarget()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public static EcoHost Create(ulong seed, int capacity = 256, ushort width = 64,
var economy = new EconomySystem(entities);
// 16.5: the FoW radar read requires the placement register.
var construction = new Nova.Simulation.Construction.ConstructionSystem(entities, economy);
var fogOfWar = new FogOfWarSystem(entities, construction, teamCount: 2, width, height);
var fogOfWar = new FogOfWarSystem(entities, construction, economy, teamCount: 2, width, height);
var combat = new CombatSystem(entities, fogOfWar, economy, construction);

var kernel = new SimulationKernel(new SimRandom(seed));
Expand Down
39 changes: 33 additions & 6 deletions Assets/Tests/EditMode/Simulation/FogOfWarSystemTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,15 @@ private sealed class TestHost
{
public SimulationKernel Kernel { get; }
public EntityManager Entities { get; }
public EconomySystem Economy { get; }
public ConstructionSystem Construction { get; }
public FogOfWarSystem Fog { get; }

private TestHost(SimulationKernel kernel, EntityManager entities, ConstructionSystem construction, FogOfWarSystem fog)
private TestHost(SimulationKernel kernel, EntityManager entities, EconomySystem economy, ConstructionSystem construction, FogOfWarSystem fog)
{
Kernel = kernel;
Entities = entities;
Economy = economy;
Construction = construction;
Fog = fog;
}
Expand All @@ -51,19 +53,19 @@ public static TestHost Create(ulong seed, int capacity = 64, ushort width = 64,
var entities = new EntityManager(capacity);
var pathfinding = new PathfindingSystem(width, height);
var movement = new MovementSystem(entities, pathfinding);
// 16.5: the FoW radar read requires the placement register
// an unregistered economy/construction pair answers placement
// queries without ever ticking.
// 16.5/16.6: the FoW radar read requires the placement register
// and the power balance — an unregistered economy/construction
// pair answers both queries without ever ticking.
var economy = new EconomySystem(entities);
var construction = new ConstructionSystem(entities, economy);
var fog = new FogOfWarSystem(entities, construction, teamCount: 2, width, height);
var fog = new FogOfWarSystem(entities, construction, economy, teamCount: 2, width, height);

var kernel = new SimulationKernel(new SimRandom(seed));
kernel.RegisterSystem(pathfinding);
kernel.RegisterSystem(movement);
kernel.RegisterSystem(fog);
kernel.Start();
return new TestHost(kernel, entities, construction, fog);
return new TestHost(kernel, entities, economy, construction, fog);
}

public void Step() => Kernel.StepTick();
Expand Down Expand Up @@ -286,6 +288,31 @@ public void RadarSignature_WithoutCompletedRadar_NoCoverage()
Assert.That(pings.Count, Is.EqualTo(0), "no finished Radar building, no coverage at all");
}

[Test]
public void RadarSignatures_StopAtPowerDeficit_AndResumeWhenBalanceRecovers()
{
// 16.6 (C4, Economy.md Low-Power rule): at a power deficit the radar is the FIRST
// system to fall — no coverage, no pings. The deficit here is set
// directly on the balance (the rig's economy never recomputes).
var host = TestHost.Create(Seed);
Assert.That(host.Construction.PlaceCompletedBuilding(0, 10, 9, 14).IsValid, Is.True);
SpawnAt(host, 1, 24, 10, sightRadius: 5); // radar-covered, hidden
host.Step(2);

var pings = new List<RadarSignature>();
host.Fog.GetRadarSignatures(0, pings);
Assert.That(pings.Count, Is.EqualTo(1), "coverage with a finished Radar and a balanced grid");

host.Economy.GetPlayerEconomy(0).PowerRequired = 1; // deficit: 1 required > 0 provided
pings.Clear();
host.Fog.GetRadarSignatures(0, pings);
Assert.That(pings.Count, Is.EqualTo(0), "LOW POWER takes the radar offline");

host.Economy.GetPlayerEconomy(0).PowerRequired = 0;
host.Fog.GetRadarSignatures(0, pings);
Assert.That(pings.Count, Is.EqualTo(1), "the radar comes back with the balance");
}

[Test]
public void HiddenWorldMetamorphic_HiddenEnemyVariation_LeavesTeamZeroViewIdentical()
{
Expand Down
12 changes: 12 additions & 0 deletions Assets/Tests/EditMode/Simulation/MatchFingerprintV1Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,18 @@ public void ComputeHash_IsStableAcrossInstances_AndStubHashesAreDistinct()
"D-106 rules must not match the legacy empty rules stub");
}

[Test]
public void CurrentRulesHash_MovesPastRevisionOne_ForLowPowerRepair()
{
ulong revisionOne = MatchFingerprint.ComputeRulesHash64(MatchFingerprint.RulesRevisionV1);
ulong current = MatchFingerprint.ComputeCurrentRulesHash64();

Assert.That(MatchFingerprint.CurrentRulesRevision, Is.EqualTo(MatchFingerprint.RulesRevisionV2));
Assert.That(current, Is.EqualTo(MatchFingerprint.ComputeRulesHash64(MatchFingerprint.RulesRevisionV2)));
Assert.That(current, Is.Not.EqualTo(revisionOne),
"Sprint-16.6 C4 repair behavior must not share D-106's revision-1 rules identity");
}

[Test]
public void ComputeHash_AndEquality_AreSensitiveToEveryField()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public static ProdHost Create(ulong seed, int capacity = 256, long startingCredi
var economy = new EconomySystem(entities, startingCredits);
var construction = new ConstructionSystem(entities, economy, pathfinding.CostField);
var production = new ProductionSystem(entities, economy, construction);
var fogOfWar = new FogOfWarSystem(entities, construction, teamCount: 2, 128, 128);
var fogOfWar = new FogOfWarSystem(entities, construction, economy, teamCount: 2, 128, 128);
var combat = new CombatSystem(entities, fogOfWar, economy, construction);

var kernel = new SimulationKernel(new SimRandom(seed));
Expand Down
23 changes: 23 additions & 0 deletions Assets/Tests/EditMode/Simulation/ReplayV1Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,29 @@ public void FingerprintMismatch_LegacyEmptyRules_RefusesPlaybackBeforeTickOne()
"an old/new rules mismatch must be refused before execution");
}

[Test]
public void FingerprintMismatch_RevisionOneRules_RefusesPlaybackBeforeTickOne()
{
ReplayV1TestUtil.LiveMatch live = ReplayV1TestUtil.RunLiveMatch();
MatchFingerprint revisionOne = MatchFingerprint.CreateCurrent(
MatchFingerprint.ComputeRulesHash64(MatchFingerprint.RulesRevisionV1),
live.Fingerprint.DefinitionsHash64, live.Fingerprint.MapHash64,
live.Fingerprint.GetSlotOccupancyCopy(), live.Fingerprint.GetSlotFactionCopy(),
live.Fingerprint.StartSeed, live.Fingerprint.InitialStateHash,
live.Fingerprint.InputDelayTicks);

ReplayV1TestUtil.TestHost playback = ReplayV1TestUtil.CreatePlaybackHost();
Assert.That(
ReplayPlayer.TryPlay(
live.ReplayBytes, revisionOne, playback.Kernel, playback.Ingress,
out ReplayPlaybackError error, out string detail),
Is.False);
Assert.That(error, Is.EqualTo(ReplayPlaybackError.FingerprintMismatch));
StringAssert.Contains("RulesHash64", detail);
Assert.That(playback.Kernel.CurrentTick.Value, Is.EqualTo(0u),
"revision-1 rules must be refused before execution");
}

[Test]
public void FingerprintMismatch_DifferentSlotOccupancy_RefusesPlayback()
{
Expand Down
2 changes: 1 addition & 1 deletion Assets/Tests/EditMode/Simulation/WeaponValuesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ public static TestHost Create()
// FoW radar read also requires the placement register.
var factions = new EconomySystem(entities);
var construction = new Nova.Simulation.Construction.ConstructionSystem(entities, factions);
var fog = new FogOfWarSystem(entities, construction, teamCount: 2, 64, 64);
var fog = new FogOfWarSystem(entities, construction, factions, teamCount: 2, 64, 64);
var combat = new CombatSystem(entities, fog, factions, construction);

var kernel = new SimulationKernel(new SimRandom(Seed));
Expand Down
2 changes: 1 addition & 1 deletion Assets/_Project/Scripts/Gameplay/Match/MatchRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ public void InitializeMatch(MatchConfig config)
Economy = new EconomySystem(Entities, _config.StartingCredits);
Construction = new Simulation.Construction.ConstructionSystem(Entities, Economy, Pathfinding.CostField);
Production = new Simulation.Production.ProductionSystem(Entities, Economy, Construction);
FogOfWar = new FogOfWarSystem(Entities, Construction, teamCount: 2, _mapWidth, _mapHeight);
FogOfWar = new FogOfWarSystem(Entities, Construction, Economy, teamCount: 2, _mapWidth, _mapHeight);
Combat = new CombatSystem(Entities, FogOfWar, Economy, Construction);
Victory = new Simulation.Victory.VictorySystem(Entities, Construction);

Expand Down
13 changes: 10 additions & 3 deletions Assets/_Project/Scripts/Presentation/UI/MinimapHud.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Nova.Gameplay.Match;
using Nova.Simulation.CommandsV1;
using Nova.Simulation.Definitions;
using Nova.Simulation.Economy;
using Nova.Simulation.State;
using Nova.Simulation.Vision;
using EntityId = Nova.Core.EntityId;
Expand Down Expand Up @@ -176,13 +177,19 @@ private void OnGUI()

/// <summary>
/// 16.5 (#54, C3): the minimap unlocks with the local slot's first
/// COMPLETED Radar building and goes dark when it is lost. One read,
/// shared by the draw and the hit test, so they can never disagree.
/// COMPLETED Radar building and goes dark when it is lost. 16.6 (C4,
/// Economy.md Low-Power rule): at a power deficit the radar is the FIRST system to fall —
/// the map goes dark too, exactly like the sim-side pings stop. One
/// read, shared by the draw and the hit test, so they can never
/// disagree.
/// </summary>
private bool LocalRadarOnline()
{
if (_runner == null || _runner.Construction == null || _runner.Session == null) return false;
return _runner.Construction.HasFinishedBuilding(_runner.Session.LocalSlot, UnitRole.Radar);
if (!_runner.Construction.HasFinishedBuilding(_runner.Session.LocalSlot, UnitRole.Radar)) return false;
EconomySystem economy = _runner.Economy;
if (economy == null) return false;
return !economy.GetPlayerEconomy(_runner.Session.LocalSlot).IsLowPower;
}

/// <summary>The local viewer team — the same convention as FogOfWarOverlayView/UnitViewManager.</summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@ namespace Nova.Simulation.Construction
/// damaged building: in reach (same Chebyshev rule) the target gains
/// <see cref="RepairRateHpPerTick"/> HP per tick up to its MaxHealth,
/// where the order resolves; out of reach the order is HELD, never
/// dropped; Stop clears it. Repair is unaffected by low power.
/// dropped; Stop clears it. Under LOW POWER the rate halves exactly
/// (16.6, C4 — Economy.md repair rule; 10/2 is exact, no rounding).
/// </para>
/// <para>
/// State (snapshot block <see cref="SnapshotBlockIds.Construction"/>,
Expand Down Expand Up @@ -165,6 +166,9 @@ public sealed class ConstructionSystem : IStatefulSimSystem
/// <summary>Provisional repair rate in HP per tick per repairing Builder (Q-040 candidate).</summary>
public const int RepairRateHpPerTick = 10;

/// <summary>Repair rate in HP per tick while the owner's grid is in LOW POWER (C4, Sprint 16.6).</summary>
public const int LowPowerRepairRateHpPerTick = 5;

private struct SiteState
{
public bool IsActive;
Expand Down Expand Up @@ -841,7 +845,17 @@ private void ProcessRepairOrders()
continue; // held, not dropped
}

int repaired = target.CurrentHealth + RepairRateHpPerTick;
// 16.6 (C4, Economy.md repair rule): LOW POWER halves the
// repair rate — 10/2 is exact, no rounding. The last stage of
// the shutdown order still repairs; it just repairs slower.
int rate = RepairRateHpPerTick;
ref readonly PlayerEconomyState repairEco = ref _economy.GetPlayerEconomy(target.PlayerId);
if (repairEco.IsLowPower)
{
rate = LowPowerRepairRateHpPerTick;
}

int repaired = target.CurrentHealth + rate;
target.CurrentHealth = repaired > target.MaxHealth ? target.MaxHealth : repaired;
}
}
Expand Down
Loading
Loading