- SimulationEngineTests: AirBased_LaunchesWithCorrectDescription (空基描述="空基投放") - SimulationEngineTests: GroundBased_LaunchesWithCorrectDescription (地基描述="计划发射") - DefensePlannerTests: 23个全覆盖(威胁排序/弹药匹配/地基弹道/空基弹道/分配/边界/多威胁/临界) - FullPipeline AirBased: 加入事件描述诊断断言
277 lines
13 KiB
C#
277 lines
13 KiB
C#
using System;
|
||
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 FireUnit MakeGroundUnit(string id, float posX, int munitions = 16)
|
||
=> new()
|
||
{
|
||
Id = id, Type = PlatformType.GroundBased,
|
||
Position = new Vector3(posX, 0, 50),
|
||
GunCount = 1, ChannelsPerGun = 16,
|
||
MuzzleVelocity = 800, TotalMunitions = munitions, Cooldown = 5f,
|
||
AmmoTypes = new() { AerosolType.InertGas, AerosolType.ActiveMaterial, AerosolType.ActiveFuel },
|
||
};
|
||
|
||
private static FireUnit MakeAirUnit(string id, float posX)
|
||
=> new()
|
||
{
|
||
Id = id, Type = PlatformType.AirBased,
|
||
Position = new Vector3(posX, 2500, -2000),
|
||
GunCount = 1, ChannelsPerGun = 16,
|
||
CruiseSpeed = 55, ReleaseAltitude = 1500,
|
||
TotalMunitions = 16, Cooldown = 5f,
|
||
AmmoTypes = new() { AerosolType.InertGas },
|
||
};
|
||
|
||
private static DroneGroup MakeThreat(PowerType power = PowerType.Piston, float speed = 120f,
|
||
float startX = 0, float endX = 10000, float alt = 500)
|
||
{
|
||
var t = new DroneGroup
|
||
{
|
||
GroupId = "default",
|
||
Target = new TargetConfig
|
||
{
|
||
TargetType = (int)TargetType.Piston, PowerType = (int)power,
|
||
Quantity = 1, TypicalSpeed = speed, TypicalAltitude = alt,
|
||
},
|
||
Waypoints = new List<Waypoint>
|
||
{
|
||
new() { PosX = startX, PosY = alt, PosZ = 100, Speed = speed },
|
||
new() { PosX = endX, PosY = alt, PosZ = 100, Speed = speed },
|
||
},
|
||
};
|
||
return t;
|
||
}
|
||
|
||
private PlannerResult Plan(List<FireUnit> units, DroneGroup threat,
|
||
CombatScene? env = null)
|
||
=> new DefaultDefensePlanner(TestAmmo).Plan(units, new() { threat }, env ?? new CombatScene());
|
||
|
||
// ═══════════════════════════════════════
|
||
// 威胁排序
|
||
// ═══════════════════════════════════════
|
||
|
||
[Fact] public void Priority_FasterThreat_RanksHigher()
|
||
{
|
||
var a = MakeThreat(speed: 60); a.ArrivalTime = 50; a.ThreatIndex = 2f;
|
||
var b = MakeThreat(speed: 500); b.ArrivalTime = 50; b.ThreatIndex = 16f;
|
||
Assert.True(b.Priority > a.Priority);
|
||
}
|
||
|
||
[Fact] public void Priority_EarlierThreat_RanksHigher()
|
||
{
|
||
var a = MakeThreat(); a.ArrivalTime = 20; a.ThreatIndex = 2f;
|
||
var b = MakeThreat(); b.ArrivalTime = 100; b.ThreatIndex = 2f;
|
||
Assert.True(a.Priority > b.Priority);
|
||
}
|
||
|
||
// ═══════════════════════════════════════
|
||
// 弹药匹配
|
||
// ═══════════════════════════════════════
|
||
|
||
[Fact] public void Ammo_Piston_MatchesInertGas()
|
||
=> Assert.Equal(AerosolType.InertGas,
|
||
Plan(new() { MakeGroundUnit("u0", 5000) }, MakeThreat(PowerType.Piston)).Best.Assignments[0].AmmoType);
|
||
|
||
[Fact] public void Ammo_Jet_MatchesActiveMaterial()
|
||
=> Assert.Equal(AerosolType.ActiveMaterial,
|
||
Plan(new() { MakeGroundUnit("u0", 5000) }, MakeThreat(PowerType.Jet, 500)).Best.Assignments[0].AmmoType);
|
||
|
||
[Fact] public void Ammo_Electric_MatchesInertGas()
|
||
=> Assert.Equal(AerosolType.InertGas,
|
||
Plan(new() { MakeGroundUnit("u0", 5000) }, MakeThreat(PowerType.Electric)).Best.Assignments[0].AmmoType);
|
||
|
||
// ═══════════════════════════════════════
|
||
// 地基弹道:每发 shellTime 因目标位置不同而不同
|
||
// ═══════════════════════════════════════
|
||
|
||
[Fact]
|
||
public void Ground_FireTime_VariesPerRound()
|
||
{
|
||
var result = Plan(new() { MakeGroundUnit("u0", 5000) }, MakeThreat(speed: 120));
|
||
var events = result.Best.MergedSchedule.OrderBy(e => e.FireTime).ToList();
|
||
Assert.True(events.Count >= 3);
|
||
// 地基弹道:目标更近的弹飞行时间更短,FireTime 更晚(更接近 recommendedTiming)
|
||
float first = events[0].FireTime;
|
||
float last = events[^1].FireTime;
|
||
Assert.True(last > first, "远目标 FireTime 应 > 近目标");
|
||
}
|
||
|
||
[Fact]
|
||
public void Ground_AllFireTimesPositive() => Assert.All(
|
||
Plan(new() { MakeGroundUnit("u0", 5000) }, MakeThreat()).Best.MergedSchedule,
|
||
fe => Assert.True(fe.FireTime > 0));
|
||
|
||
[Fact]
|
||
public void Ground_AllBeforeArrival()
|
||
{
|
||
var threat = MakeThreat();
|
||
float arrival = threat.GetArrivalTime();
|
||
Assert.All(Plan(new() { MakeGroundUnit("u0", 5000) }, threat).Best.MergedSchedule,
|
||
fe => Assert.True(fe.FireTime < arrival, $"FireTime={fe.FireTime:F1} >= arrival={arrival:F1}"));
|
||
}
|
||
|
||
[Fact]
|
||
public void Ground_MuzzleVelocity_InFireEvent()
|
||
=> Assert.All(Plan(new() { MakeGroundUnit("u0", 5000) }, MakeThreat()).Best.MergedSchedule,
|
||
fe => Assert.Equal(800f, fe.MuzzleVelocity));
|
||
|
||
[Fact]
|
||
public void Ground_TargetX_OnRoute()
|
||
{
|
||
var threat = MakeThreat(startX: 0, endX: 10000);
|
||
Assert.All(Plan(new() { MakeGroundUnit("u0", 5000) }, threat).Best.MergedSchedule,
|
||
fe => Assert.True(fe.TargetX >= 0 && fe.TargetX <= 10000, $"TargetX={fe.TargetX:F0}"));
|
||
}
|
||
|
||
// ═══════════════════════════════════════
|
||
// 空基弹道:平台飞一次,所有弹药同飞行时间
|
||
// ═══════════════════════════════════════
|
||
|
||
[Fact]
|
||
public void AirBased_PlatformFliesOnce_SameFlightTime()
|
||
{
|
||
// 同一平台多通道:所有弹药飞行时间相同(平台停在投放点)
|
||
var unit = MakeAirUnit("u0", 3000);
|
||
var result = Plan(new() { unit }, MakeThreat(speed: 120));
|
||
var events = result.Best.MergedSchedule.OrderBy(e => e.FireTime).ToList();
|
||
Assert.True(events.Count >= 3);
|
||
|
||
// 间隔应稳定 ≈ physicsInterval(1.2s),不应因目标位置变化而波动
|
||
// physicsInterval = max(1, 40.38/33.33) = 1.21s
|
||
for (int i = 1; i < events.Count; i++)
|
||
{
|
||
float gap = events[i].FireTime - events[i - 1].FireTime;
|
||
Assert.True(Math.Abs(gap - 1.21f) < 0.2f,
|
||
$"gap[{i}]={gap:F3}, expected 1.21±0.2");
|
||
}
|
||
}
|
||
|
||
[Fact]
|
||
public void AirBased_FireTime_BeforeArrival()
|
||
{
|
||
var threat = MakeThreat(speed: 120);
|
||
float arrival = threat.GetArrivalTime();
|
||
Assert.All(Plan(new() { MakeAirUnit("u0", 3000) }, threat).Best.MergedSchedule,
|
||
fe => Assert.True(fe.FireTime < arrival - 40f,
|
||
$"空基FireTime={fe.FireTime:F1} 应远早于 arrival={arrival:F1}"));
|
||
}
|
||
|
||
[Fact]
|
||
public void AirBased_MuzzleVelocity_Zero()
|
||
=> Assert.All(Plan(new() { MakeAirUnit("u0", 3000) }, MakeThreat()).Best.MergedSchedule,
|
||
fe => Assert.Equal(0f, fe.MuzzleVelocity));
|
||
|
||
[Fact]
|
||
public void AirBased_TargetX_OnRoute()
|
||
{
|
||
var threat = MakeThreat(startX: 0, endX: 10000);
|
||
Assert.All(Plan(new() { MakeAirUnit("u0", 3000) }, threat).Best.MergedSchedule,
|
||
fe => Assert.True(fe.TargetX >= 0 && fe.TargetX <= 10000, $"TargetX={fe.TargetX:F0}"));
|
||
}
|
||
|
||
// ═══════════════════════════════════════
|
||
// 分配逻辑
|
||
// ═══════════════════════════════════════
|
||
|
||
[Fact]
|
||
public void Allocation_RespectsTotalMunitions()
|
||
{
|
||
var unit = MakeGroundUnit("u0", 5000, munitions: 2); // 只能打 2 发
|
||
var result = Plan(new() { unit }, MakeThreat(speed: 200));
|
||
int totalFired = result.Best.Assignments.Sum(a => a.RoundsFired);
|
||
Assert.True(totalFired <= 2, $"fired={totalFired} > 2 munitions");
|
||
}
|
||
|
||
[Fact]
|
||
public void Allocation_MultiUnit_PoolsChannels()
|
||
{
|
||
var result = new DefaultDefensePlanner(TestAmmo).Plan(
|
||
new() { MakeGroundUnit("u0", 5000), MakeGroundUnit("u1", 5100) },
|
||
new() { MakeThreat(speed: 200) }, new CombatScene());
|
||
Assert.True(result.Best.ThreatsEngaged == 1);
|
||
Assert.True(result.Best.MergedSchedule.Count > 1);
|
||
}
|
||
|
||
[Fact]
|
||
public void Allocation_IncompatibleAmmo_NotEngaged()
|
||
{
|
||
var unit = new FireUnit
|
||
{
|
||
Id = "u0", Type = PlatformType.GroundBased,
|
||
Position = new Vector3(5000, 0, 50),
|
||
GunCount = 1, ChannelsPerGun = 16, MuzzleVelocity = 800,
|
||
TotalMunitions = 16,
|
||
AmmoTypes = new() { AerosolType.ActiveMaterial }, // 无 InertGas
|
||
};
|
||
var result = Plan(new() { unit }, MakeThreat(PowerType.Piston));
|
||
Assert.Equal(0, result.Best.ThreatsEngaged);
|
||
}
|
||
|
||
// ═══════════════════════════════════════
|
||
// 边界
|
||
// ═══════════════════════════════════════
|
||
|
||
[Fact] public void Edge_NoUnits_Unengaged() => Assert.Equal(1, new DefaultDefensePlanner(TestAmmo).Plan(new(), new() { MakeThreat() }, new CombatScene()).Best.ThreatsUnengaged);
|
||
[Fact] public void Edge_NoThreats_Empty() => Assert.Equal(0, new DefaultDefensePlanner(TestAmmo).Plan(new() { MakeGroundUnit("u0", 5000) }, new(), new CombatScene()).Best.ThreatsEngaged);
|
||
|
||
[Fact]
|
||
public void Edge_OutOfRange_Ground()
|
||
{
|
||
var unit = MakeGroundUnit("u0", 100000); // 极远
|
||
var result = Plan(new() { unit }, MakeThreat());
|
||
Assert.Equal(0, result.Best.ThreatsEngaged);
|
||
}
|
||
|
||
[Fact]
|
||
public void Edge_OutOfRange_Air()
|
||
{
|
||
var unit = new FireUnit
|
||
{
|
||
Id = "u0", Type = PlatformType.AirBased,
|
||
Position = new Vector3(100000, 2500, 100000), // 极远
|
||
GunCount = 1, ChannelsPerGun = 4, CruiseSpeed = 10, ReleaseAltitude = 1500,
|
||
TotalMunitions = 4, AmmoTypes = new() { AerosolType.InertGas },
|
||
};
|
||
var result = Plan(new() { unit }, MakeThreat());
|
||
Assert.Equal(0, result.Best.ThreatsEngaged);
|
||
}
|
||
|
||
// ═══════════════════════════════════════
|
||
// 多威胁
|
||
// ═══════════════════════════════════════
|
||
|
||
[Fact]
|
||
public void MultiThreat_BothEngaged()
|
||
{
|
||
var a = MakeThreat(PowerType.Piston, 120); a.GroupId = "g0";
|
||
var b = MakeThreat(PowerType.Jet, 300); b.GroupId = "g1";
|
||
var result = new DefaultDefensePlanner(TestAmmo).Plan(
|
||
new() { MakeGroundUnit("u0", 5000), MakeGroundUnit("u1", 5100) },
|
||
new() { a, b }, new CombatScene());
|
||
Assert.Equal(2, result.Best.ThreatsEngaged);
|
||
}
|
||
|
||
// ═══════════════════════════════════════
|
||
// 临界方案
|
||
// ═══════════════════════════════════════
|
||
|
||
[Fact]
|
||
public void Critical_HasAssignments()
|
||
{
|
||
var result = Plan(new() { MakeGroundUnit("u0", 5000) }, MakeThreat());
|
||
Assert.True(result.Critical.Assignments.Count > 0);
|
||
Assert.True(result.Critical.OverallProbability >= 0.4f);
|
||
}
|
||
}
|
||
}
|