CounterDroneBackend/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs
tian 317f47280b refactor: DefensePlanner 替代 DefenseAdvisor
- 新增 IDefensePlanner 接口 + DefaultDefensePlanner 实现
- FireUnit 重构为统一平台模型(Type/Position/CruiseSpeed/ReleaseAltitude/MuzzleVelocity)
- DroneGroup 加入威胁指数(类型系数×速度系数)和优先级排序
- 五步流水线:威胁排序→弹药匹配→候选生成→贪心分配→时序生成
- 弹药精确匹配,不降级;临界方案基于50%概率阈值
- 删除 IDefenseAdvisor/DefaultDefenseAdvisor/RecommendMultiGroup
- AlgorithmFactory 注册 IDefensePlanner
- 测试更新:DefensePlannerTests 替代旧测试,FullPipelineTests 适配新 API
- Unity Managers 更新调用
- 125 测试全通过
2026-06-12 18:20:25 +08:00

440 lines
20 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using CounterDrone.Core;
using CounterDrone.Core.Algorithms;
using CounterDrone.Core.Models;
using CounterDrone.Core.Repository;
using CounterDrone.Core.Services;
using CounterDrone.Core.Simulation;
using SQLite;
using Xunit;
namespace CounterDrone.Core.Tests
{
public class FullPipelineTests : IDisposable
{
private readonly string _testDir;
private readonly IPathProvider _paths;
private readonly IScenarioService _scenario;
private readonly FrameDataStore _frameStore;
private readonly SQLiteConnection _mainDb;
private List<AmmunitionSpec> _ammoCatalog;
private List<FireEvent> _lastFireSchedule;
private string _taskId = string.Empty;
public FullPipelineTests()
{
_testDir = Path.Combine(Path.GetTempPath(), $"cd_full_{Guid.NewGuid():N}");
_paths = new TestPathProvider(_testDir);
var dbm = new DatabaseManager(_paths);
_mainDb = dbm.OpenMainDb();
foreach (var a in DefaultAmmunition.GetAll()) _mainDb.Insert(a);
_ammoCatalog = new List<AmmunitionSpec>(DefaultAmmunition.GetAll());
_scenario = new ScenarioService(
new SimTaskRepository(_mainDb), new CombatSceneRepository(_mainDb),
new ControlZoneRepository(_mainDb), new TargetConfigRepository(_mainDb),
new EquipmentDeploymentRepository(_mainDb), new CloudDispersalRepository(_mainDb),
new RoutePlanRepository(_mainDb), new WaypointRepository(_mainDb));
_frameStore = new FrameDataStore(_paths);
}
public void Dispose()
{
_mainDb?.Close();
if (Directory.Exists(_testDir)) Directory.Delete(_testDir, true);
}
private SimulationEngine RunSimulation(int maxTicks, float tickDt = 1f / 20f)
{
var engine = new SimulationEngine(_scenario, _frameStore, new DamageModelRouter(), _paths);
// 如果有推荐方案,生成发射计划
var detail = _scenario.GetTaskDetail(_taskId)!;
if (detail.Equipment.Any(e => e.EquipmentRole == (int)EquipmentRole.LaunchPlatform))
{
var schedule = BuildFireSchedule(detail);
engine.SetFireSchedule(schedule);
}
engine.Initialize(_taskId);
engine.TimeScale = 8f; // 8倍速加速
for (int i = 0; i < maxTicks; i++)
{
var r = engine.Tick(tickDt);
if (r.State == SimulationState.Completed) break;
}
engine.Stop();
return engine;
}
/// <summary>从推荐方案的时机参数生成发射计划</summary>
private List<FireEvent> BuildFireSchedule(TaskFullConfig detail)
{
return _lastFireSchedule ?? new List<FireEvent>();
}
/// <summary>创建默认火力单元并调用 Planner 生成方案</summary>
private DefensePlan ApplyDefensePlan(PlatformType? preferredPlatform = null)
{
var detail = _scenario.GetTaskDetail(_taskId)!;
// 从已保存的 EquipmentDeployment 构建 FireUnit 列表
var fireUnits = new List<FireUnit>();
var platType = preferredPlatform ?? PlatformType.GroundBased;
var idx = 0;
foreach (var eq in detail.Equipment.Where(e => e.EquipmentRole == (int)EquipmentRole.LaunchPlatform))
{
fireUnits.Add(new FireUnit
{
Id = $"u{idx++}",
Type = platType,
Position = new Vector3((float)eq.PositionX, (float)eq.PositionY, (float)eq.PositionZ),
CruiseSpeed = (float)(eq.CruiseSpeed ?? 55f),
ReleaseAltitude = (float)(eq.ReleaseAltitude ?? 0f),
MuzzleVelocity = (float)(eq.MuzzleVelocity ?? 800f),
TotalMunitions = eq.MunitionCount ?? 1,
AmmoTypes = new List<AerosolType> { (AerosolType)(eq.AerosolType ?? 0) },
Cooldown = (float)eq.Cooldown,
});
}
// 如果没装备,生成默认的地面单元
if (fireUnits.Count == 0)
{
for (int i = 0; i < 5; i++)
fireUnits.Add(new FireUnit
{
Id = $"u{i}",
Type = platType,
Position = new Vector3(5000 + i * 50, 0, 50),
CruiseSpeed = 55f,
ReleaseAltitude = 1000f,
MuzzleVelocity = 800f,
TotalMunitions = 1,
AmmoTypes = new List<AerosolType> { AerosolType.InertGas, AerosolType.ActiveMaterial, AerosolType.ActiveFuel },
});
}
// 构建 DroneGroup 列表
var threats = new List<DroneGroup>();
foreach (var t in detail.Targets)
{
var route = detail.Routes.FirstOrDefault(r => r.GroupId == t.GroupId);
var wps = detail.WaypointGroups.GetValueOrDefault(t.GroupId, new List<Waypoint>());
threats.Add(new DroneGroup
{
GroupId = t.GroupId,
Target = t,
Route = route ?? new RoutePlan(),
Waypoints = wps,
});
}
var planner = new DefaultDefensePlanner(_ammoCatalog);
var result = planner.Plan(fireUnits, threats, detail.Scene);
_lastFireSchedule = result.Best.MergedSchedule;
// 写入云团参数(从规划方案派生的简化参数)
if (threats.Count > 0)
{
var t = threats[0];
var mid = new CounterDrone.Core.Algorithms.Vector3(
(float)(t.Waypoints[0].PosX + t.Waypoints[^1].PosX) / 2f,
(float)(t.Waypoints[0].PosY + t.Waypoints[^1].PosY) / 2f,
(float)(t.Waypoints[0].PosZ + t.Waypoints[^1].PosZ) / 2f);
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal
{
AerosolType = platType == PlatformType.AirBased ? (int)AerosolType.InertGas : (int)AerosolType.InertGas,
PositionX = mid.X, PositionY = mid.Y, PositionZ = mid.Z,
DisperseHeight = (float)t.Target.TypicalAltitude,
RecommendedTiming = _lastFireSchedule.Count > 0 ? _lastFireSchedule[0].FireTime : 0,
Source = "Algorithm",
PositionMode = (int)PositionMode.AlgorithmRecommended,
});
}
return result.Best;
}
private static readonly string ReportOutputDir = Path.Combine(
AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "..", "..", "reports");
private void VerifyAndExportReport(SimulationEngine eng, DroneEntity drone)
{
var detail = _scenario.GetTaskDetail(_taskId)!;
var report = new ReportGenerator().Generate(detail, eng.Events.ToList(), drone.Status, eng.SimulationTime);
Assert.Contains("对抗结果", report);
Assert.Contains(detail.Task.Name, report);
var svc = new ReportService(_mainDb, _paths);
var r = svc.Generate(_taskId, detail, eng.Events.ToList(), drone.Status.ToString(), eng.SimulationTime);
svc.ExportToFile(r.Id, ReportOutputDir); // 持久化目录
}
// ═══════════════════════════════════════════════
// 场景 1无防御 — 无人机到达目标
// ═══════════════════════════════════════════════
[Fact]
public void Scenario_NoDefense_DroneReachesTarget()
{
var task = _scenario.CreateTask("无防御测试", "");
_taskId = task.Id;
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
_scenario.SaveTarget(_taskId, new TargetConfig
{
GroupId = "default",
TargetType = (int)TargetType.FixedWing,
PowerType = (int)PowerType.Jet,
Quantity = 1,
TypicalSpeed = 600,
TypicalAltitude = 400,
});
_scenario.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },
new List<Waypoint>
{
new Waypoint { PosX = 0, PosY = 400, PosZ = 0, Speed = 600 },
new Waypoint { PosX = 3000, PosY = 400, PosZ = 0, Speed = 600 },
});
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal());
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>());
var eng = RunSimulation(400);
Assert.Equal(DroneStatus.ReachedTarget, eng.Drones[0].Status);
}
// ═══════════════════════════════════════════════
// 场景 2管控区域侵入
// ═══════════════════════════════════════════════
[Fact]
public void Scenario_ZoneIntrusion_DroneMarked()
{
var task = _scenario.CreateTask("管控区侵入测试", "");
_taskId = task.Id;
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
_scenario.SaveTarget(_taskId, new TargetConfig
{
GroupId = "default",
TargetType = (int)TargetType.Electric,
PowerType = (int)PowerType.Electric,
Quantity = 1,
TypicalSpeed = 300,
TypicalAltitude = 300,
});
_scenario.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },
new List<Waypoint>
{
new Waypoint { PosX = 0, PosY = 300, PosZ = 0, Speed = 300 },
new Waypoint { PosX = 3000, PosY = 300, PosZ = 0, Speed = 300 },
});
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal());
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>());
_scenario.SaveControlZones(_taskId, new List<Models.ControlZone>
{
new Models.ControlZone
{
Name = "禁飞区",
VerticesJson = "[{\"X\":1200,\"Y\":0,\"Z\":-200},{\"X\":1800,\"Y\":0,\"Z\":-200},{\"X\":1800,\"Y\":0,\"Z\":200},{\"X\":1200,\"Y\":0,\"Z\":200}]",
MinAltitude = 0, MaxAltitude = 1000,
},
});
var eng = RunSimulation(400);
Assert.Equal(DroneStatus.ZoneIntruded, eng.Drones[0].Status);
}
// ═══════════════════════════════════════════════
// 场景 3活塞发动机 → 算法推荐惰性气体 → 拦截成功
// ═══════════════════════════════════════════════
[Fact]
public void Scenario_Piston_InertGasIntercept()
{
var task = _scenario.CreateTask("拦截活塞式无人机", "");
_taskId = task.Id;
_scenario.SaveScene(_taskId, new CombatScene
{
WindSpeed = 3,
WindDirection = (int)WindDirection.W,
});
_scenario.SaveTarget(_taskId, new TargetConfig
{
GroupId = "default",
TargetType = (int)TargetType.Piston,
PowerType = (int)PowerType.Piston,
Quantity = 1,
TypicalSpeed = 200,
TypicalAltitude = 500,
});
_scenario.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },
new List<Waypoint>
{
new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 },
new Waypoint { PosX = 20000, PosY = 500, PosZ = 0, Speed = 200 },
});
// 部署地基单元
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
{
new EquipmentDeployment
{
EquipmentRole = (int)EquipmentRole.LaunchPlatform,
PlatformType = (int)Models.PlatformType.GroundBased,
Quantity = 8, PositionX = 5000, PositionY = 0, PositionZ = 50,
AerosolType = (int)AerosolType.InertGas, MunitionCount = 3,
MuzzleVelocity = 800, Cooldown = 5,
},
});
var plan = ApplyDefensePlan();
Assert.True(plan.ThreatsEngaged > 0);
Assert.NotEmpty(plan.MergedSchedule);
var eng = RunSimulation(8000);
var launched = eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched);
var cloudsGenerated = eng.Events.Count(e => e.Type == SimEventType.CloudGenerated);
var drone = eng.Drones[0];
var msg = $"Piston: launched={launched}, clouds={cloudsGenerated}, " +
$"droneStatus={drone.Status}, hp={drone.Hp:F3}, posX={drone.PosX:F0}, " +
$"cloudCount={eng.Clouds.Count}";
Assert.True(launched > 0, msg);
Assert.True(cloudsGenerated > 0, msg);
VerifyAndExportReport(eng, drone);
}
// ═══════════════════════════════════════════════
// 场景 4喷气发动机 → 算法推荐活性材料
// ═══════════════════════════════════════════════
[Fact]
public void Scenario_Jet_ActiveMaterialIntercept()
{
var task = _scenario.CreateTask("拦截喷气式无人机", "");
_taskId = task.Id;
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
_scenario.SaveTarget(_taskId, new TargetConfig
{
GroupId = "default",
TargetType = (int)TargetType.HighSpeed,
PowerType = (int)PowerType.Jet,
Quantity = 1,
TypicalSpeed = 500,
TypicalAltitude = 800,
});
_scenario.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },
new List<Waypoint>
{
new Waypoint { PosX = 0, PosY = 800, PosZ = 0, Speed = 500 },
new Waypoint { PosX = 30000, PosY = 800, PosZ = 0, Speed = 500 },
});
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
{
new EquipmentDeployment
{
EquipmentRole = (int)EquipmentRole.LaunchPlatform,
PlatformType = (int)Models.PlatformType.GroundBased,
Quantity = 8, PositionX = 8000, PositionY = 0, PositionZ = 50,
AerosolType = (int)AerosolType.ActiveMaterial, MunitionCount = 3,
MuzzleVelocity = 800, Cooldown = 5,
},
});
var plan = ApplyDefensePlan();
Assert.True(plan.ThreatsEngaged > 0);
Assert.NotEmpty(plan.MergedSchedule);
var eng = RunSimulation(8000);
var launched = eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched);
var cloudsGenerated = eng.Events.Count(e => e.Type == SimEventType.CloudGenerated);
var drone = eng.Drones[0];
var msg = $"Jet: launched={launched}, clouds={cloudsGenerated}, " +
$"droneStatus={drone.Status}, hp={drone.Hp:F3}, posX={drone.PosX:F0}, " +
$"cloudCount={eng.Clouds.Count}";
Assert.True(launched > 0, msg);
Assert.True(cloudsGenerated > 0, msg);
VerifyAndExportReport(eng, drone);
}
// ═══════════════════════════════════════════════
// 场景 5空基平台 — 基于推荐方案,平台改为空基
// ═══════════════════════════════════════════════
[Fact]
public void Scenario_AirBased_PlatformFliesAndDrops()
{
var task = _scenario.CreateTask("空基平台拦截测试", "");
_taskId = task.Id;
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
_scenario.SaveTarget(_taskId, new TargetConfig
{
GroupId = "default",
TargetType = (int)TargetType.Piston,
PowerType = (int)PowerType.Piston,
Quantity = 1,
TypicalSpeed = 300,
TypicalAltitude = 500,
});
_scenario.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },
new List<Waypoint>
{
new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 300 },
new Waypoint { PosX = 15000, PosY = 500, PosZ = 0, Speed = 300 },
});
// 部署空基平台单元
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
{
new EquipmentDeployment
{
EquipmentRole = (int)EquipmentRole.LaunchPlatform,
PlatformType = (int)Models.PlatformType.AirBased,
Quantity = 8, PositionX = 5000, PositionY = 2500, PositionZ = -2000,
AerosolType = (int)AerosolType.InertGas, MunitionCount = 3,
CruiseSpeed = 55, ReleaseAltitude = 1500, Cooldown = 5,
},
});
var plan = ApplyDefensePlan(PlatformType.AirBased);
Assert.True(plan.ThreatsEngaged > 0);
Assert.NotEmpty(plan.MergedSchedule);
var eng = RunSimulation(8000);
var launched = eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched);
var cloudsGenerated = eng.Events.Count(e => e.Type == SimEventType.CloudGenerated);
var drone = eng.Drones[0];
var msg = $"AirBased: launched={launched}, clouds={cloudsGenerated}, " +
$"droneStatus={drone.Status}, hp={drone.Hp:F3}, simTime={eng.SimulationTime:F1}s";
Assert.True(launched > 0, msg);
Assert.True(cloudsGenerated > 0, msg);
// 验证平台确实飞了(位置偏离了巡逻点)
var platSnapshots = eng.Events
.Where(e => e.Type == SimEventType.MunitionLaunched && e.Description.Contains("空基投放"))
.ToList();
Assert.True(platSnapshots.Count > 0, "应有空基投放事件");
VerifyAndExportReport(eng, drone);
}
}
}