409 lines
18 KiB
C#
409 lines
18 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 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();
|
||
|
||
_mainDb.Insert(new AmmunitionSpec
|
||
{
|
||
Id = "ammo-inert", AerosolType = (int)AerosolType.InertGas,
|
||
Name = "惰性气体弹", InitialRadius = 50, CoreDensity = 1.0,
|
||
DispersionRateBase = 5, MaxRadius = 500, MaxDuration = 120,
|
||
EffectiveConcentration = 0.05,
|
||
});
|
||
_mainDb.Insert(new AmmunitionSpec
|
||
{
|
||
Id = "ammo-active", AerosolType = (int)AerosolType.ActiveMaterial,
|
||
Name = "活性材料弹", InitialRadius = 30, CoreDensity = 2.0,
|
||
DispersionRateBase = 3, MaxRadius = 400, MaxDuration = 90,
|
||
EffectiveConcentration = 0.1,
|
||
});
|
||
_mainDb.Insert(new AmmunitionSpec
|
||
{
|
||
Id = "ammo-fuel", AerosolType = (int)AerosolType.ActiveFuel,
|
||
Name = "活性燃料弹", InitialRadius = 40, CoreDensity = 1.5,
|
||
DispersionRateBase = 4, MaxRadius = 450, MaxDuration = 100,
|
||
EffectiveConcentration = 0.03,
|
||
});
|
||
|
||
_ammoCatalog = _mainDb.Table<AmmunitionSpec>().ToList();
|
||
|
||
_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 = 4f; // 4倍速加速
|
||
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)
|
||
{
|
||
var schedule = new List<FireEvent>();
|
||
var cloud = detail.Cloud;
|
||
|
||
if (!cloud.RecommendedTiming.HasValue || cloud.RecommendedTiming.Value <= 0)
|
||
return schedule;
|
||
|
||
var platforms = detail.Equipment
|
||
.Where(e => e.EquipmentRole == (int)EquipmentRole.LaunchPlatform)
|
||
.ToList();
|
||
if (platforms.Count == 0) return schedule;
|
||
|
||
// 从弹药规格库计算需要几发、间隔多少
|
||
var aerosolType = (AerosolType)cloud.AerosolType;
|
||
var ammo = _ammoCatalog.FirstOrDefault(a => a.AerosolType == (int)aerosolType);
|
||
if (ammo == null) return schedule;
|
||
|
||
var target = detail.Targets.FirstOrDefault();
|
||
var avgSpeed = target != null ? (float)target.TypicalSpeed / 3.6f : 50f;
|
||
var neededExposure = aerosolType == AerosolType.ActiveMaterial ? 2f : 6f;
|
||
var spacing = (float)ammo.InitialRadius * 1.5f;
|
||
|
||
// 与推荐算法一致:有效覆盖距离 = 间距×(N-1) + 直径
|
||
var requiredCoverage = neededExposure * avgSpeed;
|
||
var diameter = 2f * (float)ammo.InitialRadius;
|
||
var roundsNeeded = Math.Max(1,
|
||
(int)Math.Ceiling((requiredCoverage - diameter) / spacing) + 1);
|
||
|
||
var p = platforms[0];
|
||
var muzzleV = (float)(p.MuzzleVelocity ?? 800);
|
||
var releaseAlt = (float)cloud.DisperseHeight;
|
||
if (releaseAlt < 10) releaseAlt = 300f;
|
||
|
||
// 多枚同时发射,线性排列扩大有效覆盖
|
||
for (int r = 0; r < roundsNeeded; r++)
|
||
{
|
||
var offset = (r - (roundsNeeded - 1) / 2f) * spacing;
|
||
var tx = (float)cloud.PositionX + offset;
|
||
var ty = (float)cloud.PositionY;
|
||
var tz = (float)cloud.PositionZ;
|
||
|
||
var dx = tx - (float)p.PositionX;
|
||
var dz = tz - (float)p.PositionZ;
|
||
var dist = (float)Math.Sqrt(dx * dx + dz * dz);
|
||
|
||
// 用真实弹道算飞行时间
|
||
var launchAngle = Kinematics.CalculateLaunchAngle(dist, muzzleV, releaseAlt);
|
||
var flightTime = EstimateFlightTime(dist, muzzleV, launchAngle, releaseAlt);
|
||
|
||
var fireTime = (float)cloud.RecommendedTiming!.Value - flightTime;
|
||
if (fireTime < 0) fireTime = 0.1f;
|
||
|
||
schedule.Add(new FireEvent
|
||
{
|
||
FireTime = fireTime,
|
||
PlatformIndex = r % platforms.Count,
|
||
TargetX = tx, TargetY = ty, TargetZ = tz,
|
||
MuzzleVelocity = muzzleV,
|
||
});
|
||
}
|
||
|
||
return schedule;
|
||
}
|
||
|
||
/// <summary>估算炮弹从发射到下降至释放高度的总飞行时间</summary>
|
||
private static float EstimateFlightTime(float range, float v0, float launchAngle, float releaseAlt)
|
||
{
|
||
var sinA = (float)Math.Sin(launchAngle);
|
||
// 上升至顶点时间:t_up = v0*sinθ/g
|
||
var tUp = v0 * sinA / 9.81f;
|
||
// 从顶点下降到释放高度时间:0.5*g*t² = v0²*sin²θ/(2g) - releaseAlt
|
||
var peakH = v0 * v0 * sinA * sinA / (2f * 9.81f);
|
||
var dropH = peakH - releaseAlt;
|
||
if (dropH < 0) dropH = 0;
|
||
var tDown = (float)Math.Sqrt(2f * dropH / 9.81f);
|
||
return tUp + tDown;
|
||
}
|
||
|
||
/// <summary>将 DefenseAdvisor 推荐方案写入想定</summary>
|
||
private DefenseRecommendation GetAndApplyRecommendation()
|
||
{
|
||
var detail = _scenario.GetTaskDetail(_taskId)!;
|
||
var threat = new ThreatProfile
|
||
{
|
||
Environment = detail.Scene,
|
||
Targets = detail.Targets,
|
||
Route = detail.Route,
|
||
Waypoints = detail.Waypoints,
|
||
};
|
||
var rec = new DefaultDefenseAdvisor(_ammoCatalog).Recommend(threat);
|
||
|
||
_scenario.SaveCloudDispersal(_taskId, rec.Best.RecommendedCloud);
|
||
|
||
var equips = new List<EquipmentDeployment>();
|
||
foreach (var d in rec.Best.Detections)
|
||
equips.Add(new EquipmentDeployment
|
||
{
|
||
EquipmentRole = (int)EquipmentRole.Detection,
|
||
Quantity = d.Quantity,
|
||
PositionX = d.Position.X, PositionY = d.Position.Y, PositionZ = d.Position.Z,
|
||
DetectionRadius = d.DetectionRadius,
|
||
});
|
||
foreach (var p in rec.Best.Platforms)
|
||
equips.Add(new EquipmentDeployment
|
||
{
|
||
EquipmentRole = (int)EquipmentRole.LaunchPlatform,
|
||
PlatformType = (int)p.Type,
|
||
Quantity = p.Quantity,
|
||
PositionX = p.Position.X, PositionY = p.Position.Y, PositionZ = p.Position.Z,
|
||
AerosolType = (int)rec.Best.RecommendedAerosolType,
|
||
MunitionCount = p.MunitionCount,
|
||
Cooldown = p.Cooldown,
|
||
MuzzleVelocity = p.MuzzleVelocity,
|
||
});
|
||
_scenario.SaveDeployment(_taskId, equips);
|
||
|
||
return rec;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// 场景 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
|
||
{
|
||
TargetType = (int)TargetType.FixedWing,
|
||
PowerType = (int)PowerType.Jet,
|
||
Quantity = 1, TypicalSpeed = 600, TypicalAltitude = 400,
|
||
});
|
||
_scenario.SaveRoute(_taskId, 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
|
||
{
|
||
TargetType = (int)TargetType.Electric,
|
||
PowerType = (int)PowerType.Electric,
|
||
Quantity = 1, TypicalSpeed = 300, TypicalAltitude = 300,
|
||
});
|
||
_scenario.SaveRoute(_taskId, 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
|
||
{
|
||
TargetType = (int)TargetType.Piston,
|
||
PowerType = (int)PowerType.Piston,
|
||
Quantity = 1, TypicalSpeed = 200, TypicalAltitude = 500,
|
||
});
|
||
_scenario.SaveRoute(_taskId, 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 },
|
||
});
|
||
|
||
var rec = GetAndApplyRecommendation();
|
||
|
||
// 验证推荐参数和装备被正确持久化
|
||
var saved = _scenario.GetTaskDetail(_taskId)!;
|
||
Assert.True(saved.Cloud.RecommendedTiming.HasValue,
|
||
$"Piston: RecommendedTiming={saved.Cloud.RecommendedTiming}");
|
||
Assert.True(saved.Cloud.RecommendedTiming > 0);
|
||
var platforms = saved.Equipment.Where(e => e.EquipmentRole == (int)EquipmentRole.LaunchPlatform).ToList();
|
||
Assert.True(platforms.Count > 0, "应有发射平台");
|
||
|
||
// 直接验证 BuildFireSchedule 逻辑
|
||
var c = saved.Cloud;
|
||
var p = platforms[0];
|
||
var mv = (float)(p.MuzzleVelocity ?? 800);
|
||
var dx = (float)c.PositionX - (float)p.PositionX;
|
||
var dz = (float)c.PositionZ - (float)p.PositionZ;
|
||
var dist = (float)System.Math.Sqrt(dx * dx + dz * dz);
|
||
var ft = dist / mv;
|
||
var fireTime = (float)c.RecommendedTiming!.Value - ft;
|
||
Assert.True(fireTime > 0, $"发射时间={fireTime}, 距离={dist}, 飞行时间={ft}, 推荐时机={c.RecommendedTiming}");
|
||
|
||
Assert.Equal(AerosolType.InertGas, rec.Best.RecommendedAerosolType);
|
||
Assert.True(rec.Best.InterceptProbability > 0);
|
||
Assert.True(rec.Best.RecommendedCloud.RecommendedTiming > 0);
|
||
|
||
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: guns={platforms.Count}, 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);
|
||
Assert.True(drone.Status == DroneStatus.Destroyed, msg);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// 场景 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
|
||
{
|
||
TargetType = (int)TargetType.HighSpeed,
|
||
PowerType = (int)PowerType.Jet,
|
||
Quantity = 1, TypicalSpeed = 500, TypicalAltitude = 800,
|
||
});
|
||
_scenario.SaveRoute(_taskId, 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 },
|
||
});
|
||
|
||
var rec = GetAndApplyRecommendation();
|
||
|
||
var saved = _scenario.GetTaskDetail(_taskId)!;
|
||
Assert.True(saved.Cloud.RecommendedTiming.HasValue,
|
||
$"Jet: RecommendedTiming={saved.Cloud.RecommendedTiming}");
|
||
|
||
Assert.Equal(AerosolType.ActiveMaterial, rec.Best.RecommendedAerosolType);
|
||
Assert.True(rec.Best.InterceptProbability > 0);
|
||
Assert.True(rec.Best.RecommendedCloud.RecommendedTiming > 0);
|
||
|
||
var jetPlatforms = saved.Equipment.Where(e => e.EquipmentRole == (int)EquipmentRole.LaunchPlatform).ToList();
|
||
|
||
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: guns={jetPlatforms.Count}, 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);
|
||
Assert.True(drone.Status == DroneStatus.Destroyed, msg);
|
||
|
||
var detailJ = _scenario.GetTaskDetail(_taskId)!;
|
||
var report = new ReportGenerator().Generate(detailJ, eng.Events.ToList(), drone.Status, eng.SimulationTime);
|
||
Assert.Contains("成功拦截", report);
|
||
Assert.Contains(detailJ.Task.Name, report);
|
||
}
|
||
}
|
||
}
|