CounterDroneBackend/test/unit/CounterDrone.Core.Tests/DefensePlannerTests.cs
tian c610573e34 fix: 物理间隔错开发射 — 云团直径/无人机速度,非硬件间隔
- 跨 FireUnit 按 physicsInterval = max(ChannelInterval, cloudDiameter/droneSpeed) 错开发射时间
- Piston 200km/h 测试通过(被摧毁=1)
- Jet 500km/h 测试通过(被摧毁=1)
2026-06-13 10:55:58 +08:00

286 lines
13 KiB
C#

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<AmmunitionSpec> TestAmmo = DefaultAmmunition.GetAll();
private static List<FireUnit> MakeGroundUnits(int count = 2)
{
var units = new List<FireUnit>();
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> { 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<Waypoint>
{
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, float>
{
{ 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<FireUnit>
{
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<FireUnit>
{
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<DroneGroup> { 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<FireUnit>
{
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<FireUnit>(),
new() { MakeThreat(power, speed) },
new CombatScene());
}
}
}