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
31 changes: 30 additions & 1 deletion Assets/Tests/EditMode/AI/SkirmishAiTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ private static AiHost BuildAiHost(ulong seed)
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 combat = new CombatSystem(entities, fogOfWar, economy);
var combat = new CombatSystem(entities, fogOfWar, economy, construction);
var victory = new VictorySystem(entities, construction);

var session = new MatchSession(HumanSlot, activeSlots: new byte[] { HumanSlot, AiSlot }, inputDelayTicks: 1);
Expand Down Expand Up @@ -290,6 +290,35 @@ public void SkirmishAi_PlacesRefineryThenBarracks_ThroughTheSealedCommandPath()
"AI orders must enter through the canonical session/ingress intent path, not direct system calls");
}

[Test]
public void SkirmishAi_DefinitionRoleSite_DoesNotCountAsCompletedOrAdvanceBuildOrder()
{
AiHost host = BuildMatch(Seed);

// The tick-20 decision submits the Refinery, tick 21 creates its
// site, and tick 40 is the first decision that must classify that
// definition-role entity through the site register. A bare role
// check queues a second (Barracks) site for tick 41.
host.Run(41);

Assert.That(host.Construction.SiteCount, Is.EqualTo(1),
"an unfinished Refinery is the active build, not a completed producer that unlocks Barracks");
Assert.That(host.Construction.HasFinishedBuilding(AiSlot, UnitRole.Refinery), Is.False);
Assert.That(host.Construction.HasFinishedBuilding(AiSlot, UnitRole.Barracks), Is.False);

UnitState[] units = host.Entities.RawUnits;
int definitionRoleSites = 0;
for (int i = 0; i < host.Entities.Capacity; i++)
{
ref readonly UnitState unit = ref units[i];
if (!unit.IsActive || unit.PlayerId != AiSlot || !host.Construction.IsActiveSite(unit.Id)) continue;
definitionRoleSites++;
Assert.That(unit.Role, Is.EqualTo(UnitRole.Refinery),
"the sole site carries the Refinery role without becoming a finished Refinery");
}
Assert.That(definitionRoleSites, Is.EqualTo(1));
}

// ----------------------------------------------------------------
// (b) Economy: harvesters work the field, credits recover
// ----------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ private static ReferenceHost BuildReferenceHost(ulong seed)
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 combat = new Nova.Simulation.Combat.CombatSystem(entities, fogOfWar, economy);
var combat = new Nova.Simulation.Combat.CombatSystem(entities, fogOfWar, economy, construction);
var victory = new Nova.Simulation.Victory.VictorySystem(entities, construction);

kernel.RegisterSystem(economy);
Expand Down
77 changes: 73 additions & 4 deletions Assets/Tests/EditMode/Simulation/CombatSystemTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Nova.Simulation;
using Nova.Simulation.Combat;
using Nova.Simulation.CommandsV1;
using Nova.Simulation.Definitions;
using Nova.Simulation.Economy;
using Nova.Simulation.Movement;
using Nova.Simulation.Pathfinding;
Expand Down Expand Up @@ -35,14 +36,17 @@ private sealed class TestHost
{
public SimulationKernel Kernel { get; }
public EntityManager Entities { get; }
public EconomySystem Economy { get; }
public FogOfWarSystem Fog { get; }
public CombatSystem Combat { get; }

private TestHost(SimulationKernel kernel, EntityManager entities,
Nova.Simulation.Construction.ConstructionSystem construction, FogOfWarSystem fog, CombatSystem combat)
EconomySystem economy, Nova.Simulation.Construction.ConstructionSystem construction,
FogOfWarSystem fog, CombatSystem combat)
{
Kernel = kernel;
Entities = entities;
Economy = economy;
Construction = construction;
Fog = fog;
Combat = combat;
Expand All @@ -61,15 +65,15 @@ public static TestHost Create(ulong seed, int capacity = 64, ushort width = 64,
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 combat = new CombatSystem(entities, fog, factions);
var combat = new CombatSystem(entities, fog, factions, construction);

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

public void Step() => Kernel.StepTick();
Expand Down Expand Up @@ -107,6 +111,31 @@ private static int HealthOf(TestHost host, EntityId id)
return u.CurrentHealth;
}

private static EntityId PlaceActiveDefensePlatformSite(TestHost host, byte team, int originX, int originY)
{
FactionId faction = host.Economy.GetSlotFaction(team);
ushort powerDefId = SimDefinitions.ToDefinitionId(faction, UnitRole.Power);
ushort platformDefId = SimDefinitions.ToDefinitionId(faction, UnitRole.DefensePlatform);
Assert.That(host.Construction.PlaceCompletedBuilding(team, powerDefId, 50, 50).IsValid, Is.True,
"a completed Power plant unlocks the DefensePlatform site");
host.Economy.ExecuteTick(Tick.Zero);
Assert.That(host.Construction.TryPlaceBuilding(team, platformDefId, originX, originY), Is.True);

UnitState[] units = host.Entities.RawUnits;
for (int i = 0; i < host.Entities.Capacity; i++)
{
ref readonly UnitState unit = ref units[i];
if (unit.IsActive && unit.PlayerId == team && unit.Role == UnitRole.DefensePlatform
&& host.Construction.IsActiveSite(unit.Id))
{
return unit.Id;
}
}

Assert.Fail("the active DefensePlatform site was not found in the entity store");
return EntityId.Invalid;
}

[Test]
public void Fires_WhenTargetAliveInRangeAndVisible_OnlyAfterFirstCommit()
{
Expand Down Expand Up @@ -212,6 +241,46 @@ public void AutoAcquire_DefensePlatform_FiresOnItsOwn()
Assert.That(HealthOf(host, attacker), Is.LessThan(200), "the platform actually fires");
}

[Test]
public void ActiveDefensePlatformSite_NeitherAcquiresNorExecutesExplicitAttack()
{
var host = TestHost.Create(Seed);
EntityId site = PlaceActiveDefensePlatformSite(host, 0, 10, 10);
EntityId hostile = SpawnAt(host, 1, 14, 11, maxHealth: 200);

host.Step(2);
Assert.That(host.Construction.IsActiveSite(site), Is.True);
Assert.That(host.Entities.GetUnitRef(site).AttackTarget.IsValid, Is.False,
"an armed building role must stay inert while its entity is a site");
Assert.That(HealthOf(host, hostile), Is.EqualTo(200));

host.Entities.GetUnitRef(site).AttackTarget = hostile;
host.Step();
Assert.That(host.Entities.GetUnitRef(site).AttackTarget.IsValid, Is.False,
"an explicit or stale site order is cleared instead of becoming a completion-time free shot");
Assert.That(HealthOf(host, hostile), Is.EqualTo(200));
}

[Test]
public void ActiveConstructionSite_CannotBeAutoAcquiredOrExplicitlyEngaged()
{
var host = TestHost.Create(Seed);
EntityId site = PlaceActiveDefensePlatformSite(host, 1, 10, 10);
EntityId attacker = SpawnAt(host, 0, 14, 11, maxHealth: 200);

host.Step(2);
Assert.That(host.Entities.GetUnitRef(attacker).AttackTarget.IsValid, Is.False,
"auto-acquisition must exclude unfinished sites");
Assert.That(host.Entities.GetUnitRef(site).CurrentHealth, Is.EqualTo(1));

host.Entities.GetUnitRef(attacker).AttackTarget = site;
host.Step();
Assert.That(host.Entities.GetUnitRef(attacker).AttackTarget.IsValid, Is.False,
"an explicit order on an illegal site target is cleared");
Assert.That(host.Construction.IsActiveSite(site), Is.True);
Assert.That(host.Entities.GetUnitRef(site).CurrentHealth, Is.EqualTo(1));
}

[Test]
public void AutoAcquire_UnarmedRoles_NeverAcquire()
{
Expand Down Expand Up @@ -576,7 +645,7 @@ public static ReplayHost Create(ulong seed)
// 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 combat = new CombatSystem(entities, fog, economy);
var combat = new CombatSystem(entities, fog, economy, construction);

var kernel = new SimulationKernel(new SimRandom(seed));
kernel.RegisterSystem(economy);
Expand Down
79 changes: 71 additions & 8 deletions Assets/Tests/EditMode/Simulation/ConstructionSystemTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -264,12 +264,13 @@ public void PlaceBuilding_ChargesExactCost_AndCreatesSiteEntity()
"Barracks costs exactly 500 AE (provisional)");
Assert.That(f.Construction.SiteCount, Is.EqualTo(1));

// The site entity sits at the footprint center with role Unit and 1 HP.
// The site entity sits at the footprint center carrying its
// DEFINITION role (16.3, #44) with 1 HP.
bool found = false;
UnitState[] units = f.Entities.RawUnits;
for (int i = 0; i < f.Entities.Capacity; i++)
{
if (!units[i].IsActive || units[i].Role != UnitRole.Unit) continue;
if (!units[i].IsActive || units[i].Role != UnitRole.Barracks) continue;
found = true;
Assert.That(units[i].Transform.PositionX, Is.EqualTo(SimFixed.FromInt(21)));
Assert.That(units[i].Transform.PositionY, Is.EqualTo(SimFixed.FromInt(21)));
Expand Down Expand Up @@ -407,7 +408,10 @@ public void SiteProgress_LowPower_ExactlyHalvesProgress()
f.Step(279); // 289 ticks total: still short of 150 effective
Assert.That(f.Construction.TryGetSite(siteRaw, out _, out progressRaw, out _), Is.True);
Assert.That(progressRaw, Is.EqualTo(289 * (SimFixed.OneRaw / 2)));
Assert.That(f.Entities.GetUnitRef(UnitCommandStateView.ToEntityId(siteRaw)).Role, Is.EqualTo(UnitRole.Unit));
// 16.3 (#44): the role no longer tells "unfinished" — the site
// register and the 1 HP do.
Assert.That(f.Entities.GetUnitRef(UnitCommandStateView.ToEntityId(siteRaw)).CurrentHealth, Is.EqualTo(1),
"still unfinished: site HP stays 1 until completion");

f.Step(11); // 300 ticks = exactly 150 effective ticks
Assert.That(f.Entities.GetUnitRef(UnitCommandStateView.ToEntityId(siteRaw)).Role, Is.EqualTo(UnitRole.Power),
Expand All @@ -417,17 +421,22 @@ public void SiteProgress_LowPower_ExactlyHalvesProgress()
}

[Test]
public void Completion_BecomesRoleEntity_PowerAppliesFromNextTick()
public void Completion_NormalizesLegacySiteRole_AndPowerAppliesFromNextTick()
{
var f = new Fixture();
f.SpawnBuilder(0, 19, 20);
Assert.That(f.Construction.TryPlaceBuilding(0, 5, 20, 20), Is.True);
uint siteRaw = UnitCommandStateView.ToRawEntityId(SiteEntity(f));

f.Step(149);
Assert.That(f.Entities.GetUnitRef(UnitCommandStateView.ToEntityId(siteRaw)).Role, Is.EqualTo(UnitRole.Unit));
Assert.That(f.Construction.TryGetSite(siteRaw, out _, out _, out _), Is.True, "still a site one tick short");
// Emulate a pre-16.3 mid-construction snapshot: its site entity
// restores with the legacy generic role while the site table still
// names the Power definition.
f.Entities.GetUnitRef(UnitCommandStateView.ToEntityId(siteRaw)).Role = UnitRole.Unit;
f.Step(1); // tick 150: completion in phase 4
Assert.That(f.Entities.GetUnitRef(UnitCommandStateView.ToEntityId(siteRaw)).Role, Is.EqualTo(UnitRole.Power));
Assert.That(f.Entities.GetUnitRef(UnitCommandStateView.ToEntityId(siteRaw)).Role, Is.EqualTo(UnitRole.Power),
"completion normalizes legacy snapshot entities to their definition role");
Assert.That(f.Entities.GetUnitRef(UnitCommandStateView.ToEntityId(siteRaw)).CurrentHealth, Is.EqualTo(400),
"completion restores full HP");
Assert.That(f.Economy.GetPlayerEconomy(0).PowerProvided, Is.EqualTo(0),
Expand Down Expand Up @@ -630,6 +639,59 @@ public void PlaceCompletedBuilding_Refinery_GrantsNothing_MatchStartIsUnchanged(
"an instantly placed Refinery grants nothing");
}

[Test]
public void Site_CarriesDefinitionRole_ButDrawsAndProvidesNoPower_UntilCompletion()
{
// 16.3 (#44): the site carries its definition role so the armed
// generic-slot fallback dies — and the power recompute must not
// read that role. A Refinery site drains nothing, a Power site
// feeds nothing, until the site register flips at completion.
var f = new Fixture();
Assert.That(f.Construction.PlaceCompletedBuilding(0, 3, 40, 40).IsValid, Is.True,
"the completed HQ supplies the 30 power needed to permit the Refinery");
f.SpawnBuilder(0, 19, 20);
f.Step(1); // commit: HQ provides 30, nothing required

Assert.That(f.Construction.TryPlaceBuilding(0, 4, 20, 20), Is.True, "Refinery def 4 (draws 20 completed)");
uint siteRaw = UnitCommandStateView.ToRawEntityId(SiteEntity(f));
EntityId siteId = UnitCommandStateView.ToEntityId(siteRaw);
f.Step(1);
Assert.That(f.Entities.GetUnitRef(siteId).Role, Is.EqualTo(UnitRole.Refinery),
"the site carries its definition role");
Assert.That(f.Construction.IsActiveSite(siteId), Is.True);
Assert.That(f.Construction.IsCompletedPlacement(siteRaw), Is.False);
Assert.That(f.Construction.HasFinishedBuilding(0, UnitRole.Refinery), Is.False,
"definition role is not completion; producer scans must use the placement register");
Assert.That(f.Economy.GetPlayerEconomy(0).PowerRequired, Is.EqualTo(0),
"the unfinished site draws nothing");
Assert.That(f.Economy.GetPlayerEconomy(0).PowerProvided, Is.EqualTo(30),
"the unfinished site neither adds nor removes power from the completed-HQ baseline");

f.Step(200); // completion (200 full-power ticks)
Assert.That(f.Construction.TryGetSite(siteRaw, out _, out _, out _), Is.False, "completed: no longer a site");
f.Step(1); // next economy recompute
Assert.That(f.Economy.GetPlayerEconomy(0).PowerRequired, Is.EqualTo(20),
"the completed Refinery draws its 20");
}

[Test]
public void PowerSite_ProvidesNothing_UntilCompletion()
{
var f = new Fixture();
f.SpawnBuilder(0, 19, 20);
f.Step(1);

Assert.That(f.Construction.TryPlaceBuilding(0, 5, 20, 20), Is.True, "Power plant def 5 (feeds 100 completed)");
f.Step(1);
Assert.That(f.Economy.GetPlayerEconomy(0).PowerProvided, Is.EqualTo(0),
"a Power site must not power itself up mid-build");

f.Step(150); // completion (150 full-power ticks)
f.Step(1); // next economy recompute
Assert.That(f.Economy.GetPlayerEconomy(0).PowerProvided, Is.EqualTo(100),
"the completed plant feeds its 100");
}

[Test]
public void CancelConstruction_Refunds75Percent_AndFreesFootprint()
{
Expand Down Expand Up @@ -840,13 +902,14 @@ public void ProgressSites_ReassignsNonBuilderAssignment_DefenseInDepth()
"the site pauses — the non-builder never progressed it");
}

/// <summary>Returns the single active site entity of the fixture.</summary>
/// <summary>Returns the single active site entity of the fixture (16.3: via the site register — the role is the definition's now).</summary>
private static EntityId SiteEntity(Fixture f)
{
UnitState[] units = f.Entities.RawUnits;
for (int i = 0; i < f.Entities.Capacity; i++)
{
if (units[i].IsActive && units[i].Role == UnitRole.Unit)
if (units[i].IsActive
&& f.Construction.TryGetSite(UnitCommandStateView.ToRawEntityId(units[i].Id), out _, out _, out _))
{
return units[i].Id;
}
Expand Down
Loading
Loading