- ApplyDefensePlan 展开 EquipmentDeployment.Quantity → 多个 FireUnit - Planner 分配后用 fireUnits.IndexOf 修正 PlatformIndex - 新增 FullPipeline_Piston_VerifiesRoundAllocation 验证 ≥8 发 - Piston 集成测试加入计划大小断言
446 lines
20 KiB
C#
446 lines
20 KiB
C#
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))
|
||
{
|
||
int qty = Math.Max(1, eq.Quantity);
|
||
for (int i = 0; i < qty; i++)
|
||
{
|
||
fireUnits.Add(new FireUnit
|
||
{
|
||
Id = $"u{idx++}",
|
||
Type = platType,
|
||
Position = new Vector3((float)eq.PositionX + i * 50, (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);
|
||
Assert.True(plan.MergedSchedule.Count >= 8,
|
||
$"Piston Planner 只生成了 {plan.MergedSchedule.Count} 发,预期 >= 8");
|
||
|
||
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);
|
||
}
|
||
}
|
||
}
|