using System.Collections.Generic; using System.Linq; using CounterDrone.Core.Algorithms; using CounterDrone.Core.Models; using Xunit; namespace CounterDrone.Core.Tests { public class DefensePlannerTests { private static readonly List TestAmmo = DefaultAmmunition.GetAll(); private static List MakeGroundUnits(int count = 2) { var units = new List(); for (int i = 0; i < count; i++) units.Add(new FireUnit { Id = $"u{i}", Type = PlatformType.GroundBased, Position = new Vector3(5000 + i * 100, 0, 50), GunCount = 4, ChannelsPerGun = 4, ChannelInterval = 1f, MuzzleVelocity = 800f, TotalMunitions = 16, Cooldown = 5f, AmmoTypes = new List { AerosolType.InertGas, AerosolType.ActiveMaterial, AerosolType.ActiveFuel }, }); return units; } private DroneGroup MakeThreat(PowerType powerType = PowerType.Piston, float speed = 120f) { return new DroneGroup { GroupId = "default", Target = new TargetConfig { TargetType = (int)TargetType.Piston, PowerType = (int)powerType, Quantity = 1, TypicalSpeed = speed, TypicalAltitude = 500, }, Waypoints = new List { new Waypoint { PosX = 0, PosY = 500, PosZ = 100, Altitude = 500, Speed = speed }, new Waypoint { PosX = 10000, PosY = 500, PosZ = 100, Altitude = 500, Speed = speed }, }, }; } // ═══════════════════════════════════════ // 威胁排序 // ═══════════════════════════════════════ [Fact] public void ThreatPriority_SpeedMatters() { var slow = MakeThreat(PowerType.Piston, 60); slow.Target.TargetType = (int)TargetType.Piston; slow.ArrivalTime = 100; slow.ThreatIndex = CalcIndex(slow.Target); var fast = MakeThreat(PowerType.Piston, 500); fast.Target.TargetType = (int)TargetType.Piston; fast.ArrivalTime = 100; fast.ThreatIndex = CalcIndex(fast.Target); Assert.True(fast.Priority > slow.Priority, $"fast={fast.Priority} slow={slow.Priority}"); } [Fact] public void ThreatPriority_TypeMatters() { var rotor = MakeThreat(PowerType.Electric, 200); rotor.Target.TargetType = (int)TargetType.Rotor; rotor.ArrivalTime = 50; rotor.ThreatIndex = CalcIndex(rotor.Target); var jet = MakeThreat(PowerType.Piston, 200); jet.Target.TargetType = (int)TargetType.HighSpeed; jet.ArrivalTime = 50; jet.ThreatIndex = CalcIndex(jet.Target); Assert.True(jet.ThreatIndex > rotor.ThreatIndex, $"jet={jet.ThreatIndex} rotor={rotor.ThreatIndex}"); } private static float CalcIndex(TargetConfig t) { var coef = new Dictionary { { TargetType.HighSpeed, 4f }, { TargetType.FixedWing, 2f }, { TargetType.Piston, 2f }, { TargetType.Rotor, 1f }, { TargetType.Electric, 1f }, }.GetValueOrDefault((TargetType)t.TargetType, 1f); return coef * (float)t.TypicalSpeed / 60f; } // ═══════════════════════════════════════ // 弹药匹配 // ═══════════════════════════════════════ [Fact] public void Piston_InertGas() { Assert.Equal(AerosolType.InertGas, Plan(PowerType.Piston).Best.Assignments[0].AmmoType); } [Fact] public void Jet_ActiveMaterial() { Assert.Equal(AerosolType.ActiveMaterial, Plan(PowerType.Jet, 500).Best.Assignments[0].AmmoType); } [Fact] public void Electric_InertGas() { Assert.Equal(AerosolType.InertGas, Plan(PowerType.Electric).Best.Assignments[0].AmmoType); } // ═══════════════════════════════════════ // 通道分配 // ═══════════════════════════════════════ [Fact] public void SingleUnit_AllChannelsUsed_IfNeeded() { var result = Plan(PowerType.Piston, 60); var events = result.Best.MergedSchedule; Assert.True(events.Count >= 3, $"count={events.Count}"); } [Fact] public void NoThreats_Empty() { Assert.Equal(0, new DefaultDefensePlanner(TestAmmo).Plan(new(), new(), new CombatScene()).Best.ThreatsEngaged); } [Fact] public void ChannelInterval_StaggersWithinSalvo() { var result = Plan(PowerType.Piston, 60); var events = result.Best.MergedSchedule.OrderBy(e => e.FireTime).ToList(); // 同一单元通道有 ChannelInterval=1s 错开 Assert.True(events.Count >= 2); } [Fact] public void MultiUnit_PoolsChannels() { // 2 个单元 = 32 通道 var result = new DefaultDefensePlanner(TestAmmo).Plan( MakeGroundUnits(2), new() { MakeThreat(PowerType.Piston, 200) }, new CombatScene()); Assert.True(result.Best.ThreatsEngaged > 0); Assert.True(result.Best.MergedSchedule.Count > 1); } [Fact] public void OneSalvo_NoCooldownNeeded() { // 16 通道足够覆盖慢速目标 var result = Plan(PowerType.Piston, 60); var events = result.Best.MergedSchedule; float first = events.Min(e => e.FireTime); float last = events.Max(e => e.FireTime); // 最后一个事件 - 第一个事件 < Cooldown → 同一轮 Assert.True(last - first < 5f, $"spread={last - first:F1}s, should be < 5s cooldown"); } [Fact] public void TotalMunitions_CapsFirepower() { var units = new List { new FireUnit { Id = "u0", Type = PlatformType.GroundBased, Position = new Vector3(5000, 0, 50), GunCount = 4, ChannelsPerGun = 4, MuzzleVelocity = 800, TotalMunitions = 2, // 只够打 2 发 AmmoTypes = new() { AerosolType.InertGas } }, }; var result = new DefaultDefensePlanner(TestAmmo).Plan( units, new() { MakeThreat(PowerType.Piston, 200) }, new CombatScene()); Assert.True(result.Best.ThreatsEngaged > 0); Assert.True(result.Best.Assignments.Sum(a => a.RoundsFired) <= 2); } // ═══════════════════════════════════════ // 过滤与边界 // ═══════════════════════════════════════ [Fact] public void IncompatibleAmmo_NotEngaged() { var units = new List { new FireUnit { Id = "u0", Type = PlatformType.GroundBased, Position = new Vector3(5000, 0, 50), GunCount = 4, ChannelsPerGun = 4, MuzzleVelocity = 800, TotalMunitions = 16, AmmoTypes = new() { AerosolType.ActiveMaterial } }, }; var result = new DefaultDefensePlanner(TestAmmo).Plan( units, new() { MakeThreat(PowerType.Piston) }, new CombatScene()); Assert.Equal(0, result.Best.ThreatsEngaged); } [Fact] public void NoUnits_Unengaged() { Assert.Equal(1, Plan(unitCount: 0).Best.ThreatsUnengaged); } [Fact] public void FireTime_Positive() { var result = Plan(PowerType.Piston, 60); Assert.NotEmpty(result.Best.MergedSchedule); Assert.All(result.Best.MergedSchedule, fe => Assert.True(fe.FireTime > 0)); } [Fact] public void FireTime_BeforeArrival() { var threat = MakeThreat(PowerType.Piston, 120); var result = new DefaultDefensePlanner(TestAmmo).Plan( MakeGroundUnits(1), new() { threat }, new CombatScene()); float arrival = threat.GetArrivalTime(); Assert.All(result.Best.MergedSchedule, fe => Assert.True(fe.FireTime < arrival, $"FireTime={fe.FireTime:F0} >= {arrival:F0}")); } [Fact] public void MultiThreat_BothEngaged() { var threats = new List { MakeThreat(PowerType.Piston, 120), MakeThreat(PowerType.Jet, 300) }; threats[0].GroupId = "g0"; threats[1].GroupId = "g1"; var result = new DefaultDefensePlanner(TestAmmo).Plan(MakeGroundUnits(2), threats, new CombatScene()); Assert.Equal(2, result.Best.ThreatsEngaged); } [Fact] public void Critical_HasAssignments() { var result = Plan(PowerType.Piston, 120); Assert.True(result.Critical.Assignments.Count > 0); Assert.True(result.Critical.OverallProbability >= 0.4f); } [Fact] public void Best_ProbabilityHigherThanCritical() { var result = Plan(PowerType.Piston, 120); Assert.True(result.Best.OverallProbability >= result.Critical.OverallProbability, $"Best={result.Best.OverallProbability:P0} Critical={result.Critical.OverallProbability:P0}"); } // ═══════════════════════════════════════ // 空基 // ═══════════════════════════════════════ [Fact] public void AirBased_Engaged() { var units = new List { new FireUnit { Id = "u0", Type = PlatformType.AirBased, Position = new Vector3(3000, 2500, -2000), GunCount = 4, ChannelsPerGun = 4, CruiseSpeed = 55, ReleaseAltitude = 1500, TotalMunitions = 16, AmmoTypes = new() { AerosolType.InertGas } }, }; var result = new DefaultDefensePlanner(TestAmmo).Plan(units, new() { MakeThreat() }, new CombatScene()); Assert.True(result.Best.ThreatsEngaged > 0); } // ═══════════════════════════════════════ // 集成验证 // ═══════════════════════════════════════ [Fact] public void FullPipeline_Piston_VerifiesRoundAllocation() { var units = MakeGroundUnits(1); // 1 单元 16 通道 var threat = MakeThreat(PowerType.Piston, 200); threat.Waypoints.Clear(); threat.Waypoints.Add(new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 }); threat.Waypoints.Add(new Waypoint { PosX = 20000, PosY = 500, PosZ = 0, Speed = 200 }); var result = new DefaultDefensePlanner(TestAmmo).Plan(units, new() { threat }, new CombatScene { WindSpeed = 3 }); Assert.True(result.Best.MergedSchedule.Count >= 8, $"只有 {result.Best.MergedSchedule.Count} 发,预期 >= 8"); Assert.All(result.Best.MergedSchedule, fe => Assert.True(fe.PlatformIndex >= 0 && fe.PlatformIndex < units.Count)); } // ═══════════════════════════════════════ // helpers // ═══════════════════════════════════════ private PlannerResult Plan(PowerType power = PowerType.Piston, float speed = 120f, int unitCount = 1) { return new DefaultDefensePlanner(TestAmmo).Plan( unitCount > 0 ? MakeGroundUnits(unitCount) : new List(), new() { MakeThreat(power, speed) }, new CombatScene()); } } }