CounterDroneBackend/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs

404 lines
18 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 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);
}
}
}