refactor: DefensePlanner 替代 DefenseAdvisor
- 新增 IDefensePlanner 接口 + DefaultDefensePlanner 实现 - FireUnit 重构为统一平台模型(Type/Position/CruiseSpeed/ReleaseAltitude/MuzzleVelocity) - DroneGroup 加入威胁指数(类型系数×速度系数)和优先级排序 - 五步流水线:威胁排序→弹药匹配→候选生成→贪心分配→时序生成 - 弹药精确匹配,不降级;临界方案基于50%概率阈值 - 删除 IDefenseAdvisor/DefaultDefenseAdvisor/RecommendMultiGroup - AlgorithmFactory 注册 IDefensePlanner - 测试更新:DefensePlannerTests 替代旧测试,FullPipelineTests 适配新 API - Unity Managers 更新调用 - 125 测试全通过
This commit is contained in:
parent
1e07bcc535
commit
317f47280b
@ -10,7 +10,7 @@ namespace CounterDrone.Core.Algorithms
|
||||
{
|
||||
[typeof(ICloudDispersionModel)] = () => new GaussianPuffDispersion(),
|
||||
[typeof(IDamageModel)] = () => new DamageModelRouter(),
|
||||
[typeof(IDefenseAdvisor)] = () => new DefaultDefenseAdvisor(null),
|
||||
[typeof(IDefensePlanner)] = () => new DefaultDefensePlanner(null),
|
||||
};
|
||||
|
||||
public static void Register<TInterface>(Func<object> factory)
|
||||
|
||||
@ -50,21 +50,31 @@ namespace CounterDrone.Core.Algorithms
|
||||
public List<FireEvent> MergedFireSchedule { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>火力单元(装备编组)</summary>
|
||||
/// <summary>火力单元(单个平台,统一表达空基/地基)</summary>
|
||||
public class FireUnit
|
||||
{
|
||||
public string GroupId { get; set; } = string.Empty;
|
||||
public AerosolType LoadedAmmo { get; set; }
|
||||
public int PlatformCount { get; set; }
|
||||
public float PositionX, PositionY, PositionZ;
|
||||
public string Id { get; set; } = string.Empty;
|
||||
/// <summary>平台类型:AirBased / GroundBased</summary>
|
||||
public PlatformType Type { get; set; }
|
||||
/// <summary>待命位置(空基含巡航高度)</summary>
|
||||
public Vector3 Position { get; set; }
|
||||
/// <summary>空基巡航速度 m/s</summary>
|
||||
public float CruiseSpeed { get; set; }
|
||||
/// <summary>空基投放高度 m</summary>
|
||||
public float ReleaseAltitude { get; set; }
|
||||
/// <summary>地基初速 m/s</summary>
|
||||
public float MuzzleVelocity { get; set; } = 800f;
|
||||
/// <summary>总载弹量</summary>
|
||||
public int TotalMunitions { get; set; }
|
||||
/// <summary>可装填的弹药类型</summary>
|
||||
public List<AerosolType> AmmoTypes { get; set; } = new();
|
||||
/// <summary>同弹种连发冷却 s</summary>
|
||||
public float Cooldown { get; set; } = 5f;
|
||||
/// <summary>更换弹种时间 s</summary>
|
||||
public float AmmoChangeTime { get; set; } = 300f;
|
||||
// 弹药库:可以装填的弹药类型
|
||||
public List<AerosolType> AvailableAmmo { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>无人机编队(多编队推荐输入)</summary>
|
||||
/// <summary>无人机编队(规划器输入)</summary>
|
||||
public class DroneGroup
|
||||
{
|
||||
public string GroupId { get; set; } = string.Empty;
|
||||
@ -72,6 +82,13 @@ namespace CounterDrone.Core.Algorithms
|
||||
public RoutePlan Route { get; set; } = new();
|
||||
public List<Waypoint> Waypoints { get; set; } = new();
|
||||
|
||||
/// <summary>到达防御区域中点的时间(秒),由规划器预计算</summary>
|
||||
public float ArrivalTime { get; set; }
|
||||
/// <summary>威胁指数(速度系数 × 类型系数)</summary>
|
||||
public float ThreatIndex { get; set; }
|
||||
/// <summary>综合优先级 = 威胁指数 / (到达时间 + 1)</summary>
|
||||
public float Priority => ArrivalTime > -1 ? ThreatIndex / (ArrivalTime + 1f) : ThreatIndex;
|
||||
|
||||
/// <summary>预计到达航路中点的时间(秒)</summary>
|
||||
public float GetArrivalTime()
|
||||
{
|
||||
@ -135,4 +152,49 @@ namespace CounterDrone.Core.Algorithms
|
||||
public float TargetX, TargetY, TargetZ;
|
||||
public float MuzzleVelocity;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// DefensePlanner 类型
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
/// <summary>拦截候选:一个火力单元对一个威胁的可行性评估</summary>
|
||||
public class InterceptCandidate
|
||||
{
|
||||
public FireUnit Unit { get; set; } = null!;
|
||||
public AerosolType AmmoType { get; set; }
|
||||
public float EarliestInterceptTime { get; set; }
|
||||
public float CoverageDuration { get; set; }
|
||||
public float KillProbability { get; set; }
|
||||
public float FlightTime { get; set; } // 弹药/平台飞行时间
|
||||
public float ShellFlightTime { get; set; } // 仅地基用
|
||||
}
|
||||
|
||||
/// <summary>单元分配:一个火力单元被分配给一个威胁的配置</summary>
|
||||
public class UnitAssignment
|
||||
{
|
||||
public string FireUnitId { get; set; } = string.Empty;
|
||||
public string DroneGroupId { get; set; } = string.Empty;
|
||||
public AerosolType AmmoType { get; set; }
|
||||
public int RoundsFired { get; set; }
|
||||
public float FirstFireTime { get; set; }
|
||||
public List<FireEvent> FireEvents { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>规划方案</summary>
|
||||
public class DefensePlan
|
||||
{
|
||||
public List<UnitAssignment> Assignments { get; set; } = new();
|
||||
public List<FireEvent> MergedSchedule { get; set; } = new();
|
||||
public float OverallProbability { get; set; }
|
||||
public int ThreatsEngaged { get; set; }
|
||||
public int ThreatsUnengaged { get; set; }
|
||||
public string Summary { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>规划器输出</summary>
|
||||
public class PlannerResult
|
||||
{
|
||||
public DefensePlan Best { get; set; } = new();
|
||||
public DefensePlan Critical { get; set; } = new();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,302 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Algorithms
|
||||
{
|
||||
public class DefaultDefenseAdvisor : IDefenseAdvisor
|
||||
{
|
||||
private readonly List<AmmunitionSpec> _ammoCatalog;
|
||||
|
||||
public DefaultDefenseAdvisor(List<AmmunitionSpec> ammoCatalog)
|
||||
{
|
||||
_ammoCatalog = ammoCatalog ?? new List<AmmunitionSpec>();
|
||||
}
|
||||
|
||||
private static readonly Dictionary<PowerType, AerosolType> MatchTable = new()
|
||||
{
|
||||
{ PowerType.Electric, AerosolType.InertGas },
|
||||
{ PowerType.Piston, AerosolType.InertGas },
|
||||
{ PowerType.Jet, AerosolType.ActiveMaterial },
|
||||
};
|
||||
|
||||
public DefenseRecommendation Recommend(ThreatProfile threat)
|
||||
{
|
||||
var result = new DefenseRecommendation
|
||||
{
|
||||
Best = new DefenseSolution(),
|
||||
Critical = new DefenseSolution(),
|
||||
};
|
||||
|
||||
// ===== 输入校验 =====
|
||||
if (threat.Targets.Count == 0)
|
||||
{
|
||||
result.Best.SummaryRationale = "失败:未配置威胁目标";
|
||||
return result;
|
||||
}
|
||||
if (threat.Waypoints.Count < 2)
|
||||
{
|
||||
result.Best.SummaryRationale = "失败:需要至少两个航路点";
|
||||
return result;
|
||||
}
|
||||
|
||||
var target = threat.Targets[0];
|
||||
if (target.TypicalSpeed <= 0)
|
||||
{
|
||||
result.Best.SummaryRationale = "失败:目标速度为 0";
|
||||
return result;
|
||||
}
|
||||
|
||||
// ===== Step A:气溶胶选型 =====
|
||||
var powerType = (PowerType)target.PowerType;
|
||||
var aerosolType = MatchTable.TryGetValue(powerType, out var match)
|
||||
? match : AerosolType.InertGas;
|
||||
|
||||
var ammo = _ammoCatalog.FirstOrDefault(a => a.AerosolType == (int)aerosolType);
|
||||
if (ammo == null)
|
||||
{
|
||||
result.Best.SummaryRationale = $"失败:弹药库中未找到 {aerosolType} 类型的弹药规格";
|
||||
return result;
|
||||
}
|
||||
|
||||
var rationale = powerType switch
|
||||
{
|
||||
PowerType.Electric or PowerType.Piston =>
|
||||
$"{powerType}发动机依赖氧气,推荐惰性气体窒息方案",
|
||||
PowerType.Jet =>
|
||||
$"{powerType}发动机高温表面可触发活性材料爆燃反应",
|
||||
_ => "默认推荐惰性气体方案",
|
||||
};
|
||||
|
||||
// ===== Step B:时空交汇优化 =====
|
||||
var start = ToV3(threat.Waypoints[0]);
|
||||
var end = ToV3(threat.Waypoints[^1]);
|
||||
var mid = new Vector3((start.X + end.X) / 2f, (start.Y + end.Y) / 2f, (start.Z + end.Z) / 2f);
|
||||
var routeLength = start.DistanceTo(end);
|
||||
var avgSpeed = (float)target.TypicalSpeed / 3.6f;
|
||||
var totalFlightTime = routeLength / avgSpeed;
|
||||
|
||||
if (routeLength < 100)
|
||||
{
|
||||
result.Best.SummaryRationale = "失败:航路太短(<100m)";
|
||||
return result;
|
||||
}
|
||||
|
||||
// 弹药参数:Phase 1 爆轰 + Phase 2 膨胀后的有效半径
|
||||
var r0 = 3.3f * (float)Math.Pow(Math.Max(0.01, (float)ammo.BurstChargeKg), 0.32);
|
||||
var k = (float)ammo.TurbulentExpansionK;
|
||||
var maxDur = (float)ammo.MaxDuration;
|
||||
var halfTime = totalFlightTime / 2f;
|
||||
var windSpeed = (float)threat.Environment.WindSpeed;
|
||||
var weather = (WeatherType)threat.Environment.WeatherType;
|
||||
var effectiveR = ComputeEffectiveRadius(r0, k, halfTime, windSpeed, weather, ammo);
|
||||
|
||||
var crossTime = (2f * effectiveR) / avgSpeed;
|
||||
var neededExposure = aerosolType == AerosolType.ActiveMaterial ? 2f : 6f;
|
||||
var spacing = effectiveR * 1.5f;
|
||||
|
||||
var requiredCoverage = neededExposure * avgSpeed;
|
||||
var roundsNeeded = Math.Max(1,
|
||||
(int)Math.Ceiling((requiredCoverage - 2f * effectiveR) / spacing) + 1);
|
||||
var actualCoverage = spacing * (roundsNeeded - 1) + 2f * effectiveR;
|
||||
var actualExposure = actualCoverage / avgSpeed;
|
||||
var prob = Math.Min(0.95f, actualExposure / neededExposure);
|
||||
var expansionTime = (float)Math.Pow((effectiveR - r0) / k, 2);
|
||||
var recommendedTiming = halfTime - expansionTime;
|
||||
|
||||
// 平台类型
|
||||
var platType = threat.PreferredPlatformType ?? PlatformType.GroundBased;
|
||||
var isAirBased = platType == PlatformType.AirBased;
|
||||
|
||||
// 空基参数
|
||||
var cruiseSpeed = 55f; // ~200 km/h
|
||||
var releaseAlt = isAirBased ? (float)target.TypicalAltitude + 500f : 0f;
|
||||
var fallTime = isAirBased ? Kinematics.AirDropFallTime(releaseAlt, (float)target.TypicalAltitude) : 0f;
|
||||
|
||||
// 生成发射计划和平台部署
|
||||
var fireSchedule = new List<FireEvent>();
|
||||
var platforms = new List<RecommendedPlatform>();
|
||||
var gunCount = roundsNeeded;
|
||||
|
||||
for (int i = 0; i < roundsNeeded; i++)
|
||||
{
|
||||
var offset = (i - (roundsNeeded - 1) / 2f) * spacing;
|
||||
var tx = mid.X + offset; var ty = mid.Y; var tz = mid.Z;
|
||||
|
||||
if (isAirBased)
|
||||
{
|
||||
// 巡逻点:防御圈边缘,偏离目标侧方
|
||||
var patrolX = mid.X - 2000;
|
||||
var patrolY = 2500f;
|
||||
var patrolZ = mid.Z - 2000;
|
||||
var flightDist = Kinematics.Distance3D(patrolX, patrolY, patrolZ, tx, releaseAlt, tz);
|
||||
var flightTime = flightDist / cruiseSpeed;
|
||||
var fireTime = recommendedTiming - flightTime - fallTime;
|
||||
if (fireTime < 0.1f) fireTime = 0.1f;
|
||||
|
||||
fireSchedule.Add(new FireEvent { FireTime = fireTime, PlatformIndex = i, TargetX = tx, TargetY = ty, TargetZ = tz, MuzzleVelocity = 0f });
|
||||
platforms.Add(new RecommendedPlatform
|
||||
{
|
||||
Type = PlatformType.AirBased,
|
||||
Position = new Vector3(patrolX + i * 30, patrolY, patrolZ),
|
||||
Quantity = 1, MunitionCount = 1,
|
||||
CruiseSpeed = cruiseSpeed, ReleaseAltitude = releaseAlt,
|
||||
Cooldown = 5f, CoverageVolume = (float)ammo.InitialVolume,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
fireSchedule.Add(new FireEvent { FireTime = recommendedTiming, PlatformIndex = i, TargetX = tx, TargetY = ty, TargetZ = tz, MuzzleVelocity = 800f });
|
||||
platforms.Add(new RecommendedPlatform
|
||||
{
|
||||
Type = PlatformType.GroundBased,
|
||||
Position = new Vector3(mid.X + i * 50, 0, 50),
|
||||
Quantity = 1, MunitionCount = 1,
|
||||
MuzzleVelocity = 800f, Cooldown = 5f,
|
||||
CoverageVolume = (float)ammo.InitialVolume,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var bestCloud = new CloudDispersal
|
||||
{
|
||||
AerosolType = (int)aerosolType,
|
||||
PositionX = mid.X, PositionY = mid.Y, PositionZ = mid.Z,
|
||||
DisperseHeight = (float)target.TypicalAltitude,
|
||||
TriggerMode = (int)TriggerMode.Time,
|
||||
Duration = (int)maxDur,
|
||||
InitialScale = ammo.InitialVolume,
|
||||
ReleaseMode = (int)ReleaseMode.Single,
|
||||
Source = "Algorithm",
|
||||
PositionMode = (int)PositionMode.AlgorithmRecommended,
|
||||
RecommendedTiming = recommendedTiming,
|
||||
SalvoRounds = roundsNeeded,
|
||||
SalvoSpacing = spacing,
|
||||
EstimatedProbability = prob,
|
||||
};
|
||||
|
||||
result.Best = new DefenseSolution
|
||||
{
|
||||
RecommendedAerosolType = aerosolType,
|
||||
AerosolRationale = rationale,
|
||||
RecommendedCloud = bestCloud,
|
||||
FireSchedule = fireSchedule,
|
||||
Platforms = platforms,
|
||||
Detections = new List<RecommendedDetection>
|
||||
{
|
||||
new RecommendedDetection
|
||||
{
|
||||
Position = new Vector3(mid.X, mid.Y, 0),
|
||||
DetectionRadius = Math.Max(3000f, routeLength * 0.4f),
|
||||
Quantity = 1,
|
||||
}
|
||||
},
|
||||
InterceptProbability = prob,
|
||||
SummaryRationale = $"{aerosolType}方案,{roundsNeeded}发,预计拦截概率 {prob:P0}",
|
||||
};
|
||||
|
||||
result.Critical = new DefenseSolution
|
||||
{
|
||||
RecommendedAerosolType = aerosolType,
|
||||
RecommendedCloud = new CloudDispersal
|
||||
{
|
||||
AerosolType = (int)aerosolType,
|
||||
PositionX = start.X + (end.X - start.X) * 0.25f,
|
||||
PositionY = start.Y,
|
||||
PositionZ = start.Z,
|
||||
DisperseHeight = (float)target.TypicalAltitude,
|
||||
Duration = (int)maxDur,
|
||||
Source = "Algorithm",
|
||||
PositionMode = (int)PositionMode.AlgorithmRecommended,
|
||||
RecommendedTiming = totalFlightTime * 0.25f - expansionTime,
|
||||
},
|
||||
Platforms = platforms,
|
||||
InterceptProbability = prob * 0.3f,
|
||||
SummaryRationale = $"临界方案:拦截概率仅 {prob * 0.3f:P0}",
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Vector3 ToV3(Waypoint wp) => new((float)wp.PosX, (float)wp.PosY, (float)wp.PosZ);
|
||||
|
||||
/// <summary>计算云团在目标时刻的有效半径</summary>
|
||||
private static float ComputeEffectiveRadius(float r0, float k, float halfTime,
|
||||
float windSpeed, WeatherType weather, AmmunitionSpec ammo)
|
||||
{
|
||||
// Phase 2 结束时的半径
|
||||
var rPhase2 = r0 + k * (float)Math.Sqrt(Math.Min(halfTime, 30f));
|
||||
if (halfTime <= 30f) return rPhase2;
|
||||
|
||||
// Phase 3:用高斯扩散公式计算浓度场下的有效半径
|
||||
var x = Math.Max(1f, windSpeed * (halfTime - 30f));
|
||||
var cls = Kinematics.GetStabilityClass(weather, windSpeed);
|
||||
var sY = Kinematics.SigmaY(cls, x);
|
||||
var sZ = Kinematics.SigmaZ(cls, x);
|
||||
var peakC = Kinematics.GaussianPeakConcentration((float)ammo.SourceStrength, sY, sZ);
|
||||
var threshold = (float)ammo.EffectiveConcentration;
|
||||
if (peakC <= threshold) return rPhase2;
|
||||
|
||||
var sigma = (sY + sZ) / 2f;
|
||||
return rPhase2 + sigma * (float)Math.Sqrt(2f * Math.Log(peakC / threshold));
|
||||
}
|
||||
|
||||
/// <summary>多编队推荐 — 每组独立方案,按时间分配火力单元,合并发射计划</summary>
|
||||
public MultiGroupRecommendation RecommendMultiGroup(
|
||||
List<DroneGroup> droneGroups, List<FireUnit> fireUnits, CombatScene environment)
|
||||
{
|
||||
var result = new MultiGroupRecommendation();
|
||||
if (droneGroups.Count == 0 || fireUnits.Count == 0) return result;
|
||||
|
||||
var sorted = droneGroups.OrderBy(g => g.GetArrivalTime()).ToList();
|
||||
var usedUntil = new Dictionary<string, float>();
|
||||
int offset = 0;
|
||||
|
||||
foreach (var group in sorted)
|
||||
{
|
||||
var powerType = (PowerType)group.Target.PowerType;
|
||||
var neededAmmo = MatchTable.GetValueOrDefault(powerType, AerosolType.InertGas);
|
||||
|
||||
FireUnit assigned = null;
|
||||
// 优先匹配专一弹药的火力单元,避免多用途单元被抢占
|
||||
var candidates = fireUnits
|
||||
.Where(u => u.AvailableAmmo.Contains(neededAmmo))
|
||||
.OrderBy(u => u.AvailableAmmo.Count) // 少弹种的优先
|
||||
.ThenBy(u => usedUntil.ContainsKey(u.GroupId) ? 1 : 0); // 不忙的优先
|
||||
|
||||
foreach (var unit in candidates)
|
||||
{
|
||||
if (!unit.AvailableAmmo.Contains(neededAmmo)) continue;
|
||||
if (!usedUntil.TryGetValue(unit.GroupId, out var busyUntil))
|
||||
{ assigned = unit; break; }
|
||||
if (unit.LoadedAmmo == neededAmmo && busyUntil <= group.GetArrivalTime())
|
||||
{ assigned = unit; break; }
|
||||
if (busyUntil + unit.AmmoChangeTime <= group.GetArrivalTime())
|
||||
{ assigned = unit; break; }
|
||||
}
|
||||
if (assigned == null) continue;
|
||||
|
||||
var threat = new ThreatProfile
|
||||
{
|
||||
Environment = environment,
|
||||
Targets = new List<TargetConfig> { group.Target },
|
||||
Route = group.Route,
|
||||
Waypoints = group.Waypoints,
|
||||
};
|
||||
var sol = Recommend(threat).Best;
|
||||
foreach (var fe in sol.FireSchedule) fe.PlatformIndex += offset;
|
||||
|
||||
result.GroupSolutions.Add(sol);
|
||||
result.MergedFireSchedule.AddRange(sol.FireSchedule);
|
||||
|
||||
assigned.LoadedAmmo = sol.RecommendedAerosolType;
|
||||
usedUntil[assigned.GroupId] = (sol.FireSchedule.Count > 0
|
||||
? sol.FireSchedule.Max(f => f.FireTime) : group.GetArrivalTime()) + assigned.Cooldown;
|
||||
offset += sol.Platforms.Count;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
389
src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs
Normal file
389
src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs
Normal file
@ -0,0 +1,389 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Algorithms
|
||||
{
|
||||
/// <summary>默认防御规划器 — 五步流水线</summary>
|
||||
public class DefaultDefensePlanner : IDefensePlanner
|
||||
{
|
||||
private readonly List<AmmunitionSpec> _ammoCatalog;
|
||||
|
||||
public DefaultDefensePlanner(List<AmmunitionSpec> ammoCatalog)
|
||||
{
|
||||
_ammoCatalog = ammoCatalog ?? new List<AmmunitionSpec>();
|
||||
}
|
||||
|
||||
/// <summary>弹药匹配表:PowerType → 最佳 AerosolType</summary>
|
||||
private static readonly Dictionary<PowerType, AerosolType> MatchTable = new()
|
||||
{
|
||||
{ PowerType.Electric, AerosolType.InertGas },
|
||||
{ PowerType.Piston, AerosolType.InertGas },
|
||||
{ PowerType.Jet, AerosolType.ActiveMaterial },
|
||||
};
|
||||
|
||||
/// <summary>威胁类型系数(越大越危险)</summary>
|
||||
private static readonly Dictionary<TargetType, float> TypeCoefficient = new()
|
||||
{
|
||||
{ TargetType.HighSpeed, 4f },
|
||||
{ TargetType.FixedWing, 2f },
|
||||
{ TargetType.Piston, 2f },
|
||||
{ TargetType.Rotor, 1f },
|
||||
{ TargetType.Electric, 1f },
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 五步流水线
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
public PlannerResult Plan(List<FireUnit> fireUnits, List<DroneGroup> threats, CombatScene environment)
|
||||
{
|
||||
var result = new PlannerResult();
|
||||
|
||||
if (threats.Count == 0)
|
||||
{
|
||||
result.Best.Summary = "无威胁目标";
|
||||
return result;
|
||||
}
|
||||
if (fireUnits.Count == 0)
|
||||
{
|
||||
result.Best.Summary = "无可用火力单元";
|
||||
result.Best.ThreatsUnengaged = threats.Count;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Step 1: 威胁排序
|
||||
foreach (var t in threats)
|
||||
{
|
||||
t.ArrivalTime = t.GetArrivalTime();
|
||||
t.ThreatIndex = CalcThreatIndex(t.Target);
|
||||
}
|
||||
var sorted = threats.OrderByDescending(t => t.Priority).ToList();
|
||||
|
||||
// Step 2-4: 贪心分配求解
|
||||
result.Best = Solve(sorted, fireUnits, environment);
|
||||
|
||||
// Step 5: 临界方案(概率阈值 50%)
|
||||
result.Critical = DeriveCritical(result.Best);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Step 1: 威胁指数
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
private static float CalcThreatIndex(TargetConfig target)
|
||||
{
|
||||
float typeCoef = TypeCoefficient.GetValueOrDefault((TargetType)target.TargetType, 1f);
|
||||
float speedCoef = (float)target.TypicalSpeed / 60f;
|
||||
return typeCoef * speedCoef;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Step 2: 弹药匹配
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
private static AerosolType MatchAmmo(PowerType power)
|
||||
{
|
||||
return MatchTable.GetValueOrDefault(power, AerosolType.InertGas);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Step 3: 候选生成
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
private List<InterceptCandidate> GenerateCandidates(DroneGroup threat,
|
||||
List<FireUnit> availableUnits, CombatScene env)
|
||||
{
|
||||
var candidates = new List<InterceptCandidate>();
|
||||
var neededAmmo = MatchAmmo((PowerType)threat.Target.PowerType);
|
||||
|
||||
foreach (var unit in availableUnits)
|
||||
{
|
||||
// 弹药精确匹配,不降级
|
||||
if (!unit.AmmoTypes.Contains(neededAmmo)) continue;
|
||||
|
||||
var candidate = unit.Type == PlatformType.AirBased
|
||||
? BuildAirBasedCandidate(threat, unit, neededAmmo, env)
|
||||
: BuildGroundBasedCandidate(threat, unit, neededAmmo, env);
|
||||
|
||||
if (candidate != null) candidates.Add(candidate);
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private InterceptCandidate? BuildGroundBasedCandidate(DroneGroup threat,
|
||||
FireUnit unit, AerosolType ammoType, CombatScene env)
|
||||
{
|
||||
var mid = ThreatMidpoint(threat);
|
||||
float dx = mid.X - unit.Position.X;
|
||||
float dz = mid.Z - unit.Position.Z;
|
||||
float dist = (float)Math.Sqrt(dx * dx + dz * dz);
|
||||
|
||||
// 简单射程校验
|
||||
float maxRange = unit.MuzzleVelocity * unit.MuzzleVelocity / 9.81f;
|
||||
if (dist > maxRange * 0.8f) return null;
|
||||
|
||||
float shellTime = dist / unit.MuzzleVelocity;
|
||||
float interceptTime = threat.ArrivalTime;
|
||||
|
||||
// 计算覆盖率(简化)
|
||||
var ammo = _ammoCatalog.FirstOrDefault(a => a.AerosolType == (int)ammoType);
|
||||
float coverage = EstimateCoverage(threat, ammo, env);
|
||||
|
||||
return new InterceptCandidate
|
||||
{
|
||||
Unit = unit,
|
||||
AmmoType = ammoType,
|
||||
EarliestInterceptTime = interceptTime,
|
||||
CoverageDuration = coverage,
|
||||
KillProbability = EstimateKillProbability(threat, coverage),
|
||||
FlightTime = shellTime,
|
||||
ShellFlightTime = shellTime,
|
||||
};
|
||||
}
|
||||
|
||||
private InterceptCandidate? BuildAirBasedCandidate(DroneGroup threat,
|
||||
FireUnit unit, AerosolType ammoType, CombatScene env)
|
||||
{
|
||||
var mid = ThreatMidpoint(threat);
|
||||
float releaseAlt = unit.ReleaseAltitude > 0 ? unit.ReleaseAltitude : (float)threat.Target.TypicalAltitude + 500f;
|
||||
float flightDist = Kinematics.Distance3D(
|
||||
unit.Position.X, unit.Position.Y, unit.Position.Z,
|
||||
mid.X, releaseAlt, mid.Z);
|
||||
float cruiseSpd = unit.CruiseSpeed > 0.1f ? unit.CruiseSpeed : 55f;
|
||||
float flightTime = flightDist / cruiseSpd;
|
||||
float fallTime = Kinematics.AirDropFallTime(releaseAlt, (float)threat.Target.TypicalAltitude);
|
||||
float totalTime = flightTime + fallTime;
|
||||
|
||||
// 检查能否在威胁到达前完成拦截
|
||||
float interceptTime = threat.ArrivalTime;
|
||||
if (totalTime > interceptTime * 1.5f) return null;
|
||||
|
||||
var ammo = _ammoCatalog.FirstOrDefault(a => a.AerosolType == (int)ammoType);
|
||||
float coverage = EstimateCoverage(threat, ammo, env);
|
||||
|
||||
return new InterceptCandidate
|
||||
{
|
||||
Unit = unit,
|
||||
AmmoType = ammoType,
|
||||
EarliestInterceptTime = interceptTime,
|
||||
CoverageDuration = coverage,
|
||||
KillProbability = EstimateKillProbability(threat, coverage),
|
||||
FlightTime = totalTime,
|
||||
ShellFlightTime = fallTime,
|
||||
};
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Step 4: 贪心分配
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
private DefensePlan Solve(List<DroneGroup> sortedThreats,
|
||||
List<FireUnit> fireUnits, CombatScene env)
|
||||
{
|
||||
var plan = new DefensePlan();
|
||||
// 工作副本:追踪每次分配的弹药消耗
|
||||
var remainingMunitions = new Dictionary<string, int>();
|
||||
foreach (var u in fireUnits)
|
||||
remainingMunitions[u.Id] = u.TotalMunitions;
|
||||
|
||||
var busyUntil = new Dictionary<string, float>();
|
||||
|
||||
foreach (var threat in sortedThreats)
|
||||
{
|
||||
// 过滤可用单元:有弹药、冷却完毕
|
||||
var available = fireUnits
|
||||
.Where(u => remainingMunitions.GetValueOrDefault(u.Id, 0) > 0)
|
||||
.ToList();
|
||||
|
||||
var candidates = GenerateCandidates(threat, available, env);
|
||||
if (candidates.Count == 0)
|
||||
{
|
||||
plan.ThreatsUnengaged++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 选最早可拦截的
|
||||
var best = candidates
|
||||
.OrderBy(c => c.EarliestInterceptTime)
|
||||
.ThenByDescending(c => c.KillProbability)
|
||||
.First();
|
||||
|
||||
// 计算弹药数
|
||||
var ammo = _ammoCatalog.FirstOrDefault(a => a.AerosolType == (int)best.AmmoType);
|
||||
int roundsNeeded = CalcRoundsNeeded(threat, ammo, env, best.Unit.Type == PlatformType.AirBased);
|
||||
int actualRounds = Math.Min(roundsNeeded, remainingMunitions[best.Unit.Id]);
|
||||
|
||||
if (actualRounds <= 0) { plan.ThreatsUnengaged++; continue; }
|
||||
|
||||
// 生成发射事件
|
||||
var fireEvents = GenerateFireEvents(threat, best.Unit, best.AmmoType,
|
||||
actualRounds, ammo, env, best.FlightTime);
|
||||
|
||||
plan.Assignments.Add(new UnitAssignment
|
||||
{
|
||||
FireUnitId = best.Unit.Id,
|
||||
DroneGroupId = threat.GroupId,
|
||||
AmmoType = best.AmmoType,
|
||||
RoundsFired = actualRounds,
|
||||
FirstFireTime = fireEvents.Count > 0 ? fireEvents[0].FireTime : 0,
|
||||
FireEvents = fireEvents,
|
||||
});
|
||||
|
||||
plan.MergedSchedule.AddRange(fireEvents);
|
||||
|
||||
remainingMunitions[best.Unit.Id] -= actualRounds;
|
||||
busyUntil[best.Unit.Id] = fireEvents.Count > 0
|
||||
? fireEvents.Max(f => f.FireTime) + best.Unit.Cooldown
|
||||
: 0;
|
||||
|
||||
plan.ThreatsEngaged++;
|
||||
}
|
||||
|
||||
plan.MergedSchedule.Sort((a, b) => a.FireTime.CompareTo(b.FireTime));
|
||||
plan.OverallProbability = plan.Assignments.Count > 0
|
||||
? plan.Assignments.Average(a => EstimateKillProbability(
|
||||
sortedThreats.First(t => t.GroupId == a.DroneGroupId),
|
||||
a.RoundsFired * 4f)) // 简化:每发约 4 秒覆盖
|
||||
: 0f;
|
||||
plan.Summary = plan.ThreatsEngaged > 0
|
||||
? $"分配 {plan.ThreatsEngaged} 个威胁,{plan.ThreatsUnengaged} 个无方案,总概率 {plan.OverallProbability:P0}"
|
||||
: "无威胁被分配拦截方案";
|
||||
|
||||
return plan;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Step 5: 临界方案(概率阈值 50%)
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
private DefensePlan DeriveCritical(DefensePlan best)
|
||||
{
|
||||
var critical = new DefensePlan
|
||||
{
|
||||
Assignments = new List<UnitAssignment>(),
|
||||
MergedSchedule = new List<FireEvent>(),
|
||||
ThreatsEngaged = best.ThreatsEngaged,
|
||||
ThreatsUnengaged = best.ThreatsUnengaged,
|
||||
};
|
||||
|
||||
// 逐次减弹药直到概率跌破 50%
|
||||
foreach (var assignment in best.Assignments)
|
||||
{
|
||||
int rounds = assignment.RoundsFired;
|
||||
while (rounds > 1)
|
||||
{
|
||||
float prob = rounds / (float)assignment.RoundsFired * 0.95f;
|
||||
if (prob < 0.5f) break;
|
||||
rounds--;
|
||||
}
|
||||
|
||||
var reduced = new UnitAssignment
|
||||
{
|
||||
FireUnitId = assignment.FireUnitId,
|
||||
DroneGroupId = assignment.DroneGroupId,
|
||||
AmmoType = assignment.AmmoType,
|
||||
RoundsFired = rounds,
|
||||
FirstFireTime = assignment.FirstFireTime,
|
||||
FireEvents = assignment.FireEvents.Take(rounds).ToList(),
|
||||
};
|
||||
critical.Assignments.Add(reduced);
|
||||
critical.MergedSchedule.AddRange(reduced.FireEvents);
|
||||
}
|
||||
|
||||
critical.MergedSchedule.Sort((a, b) => a.FireTime.CompareTo(b.FireTime));
|
||||
critical.OverallProbability = 0.5f;
|
||||
critical.Summary = $"临界方案:刚好满足 50% 拦截概率";
|
||||
return critical;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 辅助方法
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
private static Vector3 ThreatMidpoint(DroneGroup threat)
|
||||
{
|
||||
if (threat.Waypoints.Count < 2)
|
||||
return new Vector3(0, (float)threat.Target.TypicalAltitude, 0);
|
||||
var s = threat.Waypoints[0];
|
||||
var e = threat.Waypoints[^1];
|
||||
return new Vector3(
|
||||
(float)(s.PosX + e.PosX) / 2f,
|
||||
(float)(s.PosY + e.PosY) / 2f,
|
||||
(float)(s.PosZ + e.PosZ) / 2f);
|
||||
}
|
||||
|
||||
private static float EstimateCoverage(DroneGroup threat, AmmunitionSpec? ammo, CombatScene env)
|
||||
{
|
||||
if (ammo == null) return 4f;
|
||||
// 简化:基于弹药最大持续时间和有效半径
|
||||
return Math.Min((float)ammo.MaxDuration, 60f);
|
||||
}
|
||||
|
||||
private static float EstimateKillProbability(DroneGroup threat, float coverageDuration)
|
||||
{
|
||||
if (threat.Waypoints.Count < 2) return 0f;
|
||||
float avgSpeed = (float)threat.Target.TypicalSpeed / 3.6f;
|
||||
float exposureTime = coverageDuration / avgSpeed;
|
||||
return Math.Min(0.95f, exposureTime / 6f); // 6s 暴露 = 95%
|
||||
}
|
||||
|
||||
private static int CalcRoundsNeeded(DroneGroup threat, AmmunitionSpec? ammo, CombatScene env, bool isAirBased)
|
||||
{
|
||||
if (ammo == null) return 1;
|
||||
float avgSpeed = (float)threat.Target.TypicalSpeed / 3.6f;
|
||||
float neededExposure = 6f;
|
||||
float effectiveR = 80f; // 简化默认值
|
||||
float spacing = effectiveR * 1.5f;
|
||||
float required = neededExposure * avgSpeed;
|
||||
return Math.Max(1, (int)Math.Ceiling((required - 2f * effectiveR) / spacing) + 1);
|
||||
}
|
||||
|
||||
private List<FireEvent> GenerateFireEvents(DroneGroup threat, FireUnit unit,
|
||||
AerosolType ammoType, int rounds, AmmunitionSpec? ammo, CombatScene env, float flightTime)
|
||||
{
|
||||
var events = new List<FireEvent>();
|
||||
var mid = ThreatMidpoint(threat);
|
||||
float effectiveR = 80f;
|
||||
float spacing = effectiveR * 1.5f;
|
||||
float interceptTime = threat.ArrivalTime;
|
||||
|
||||
for (int i = 0; i < rounds; i++)
|
||||
{
|
||||
float offset = (i - (rounds - 1) / 2f) * spacing;
|
||||
float fireTime;
|
||||
|
||||
if (unit.Type == PlatformType.AirBased)
|
||||
{
|
||||
// FireTime = 拦截时机 − 平台飞行时间 − 弹药下落时间
|
||||
float fallTime = Kinematics.AirDropFallTime(
|
||||
unit.ReleaseAltitude > 0 ? unit.ReleaseAltitude : (float)threat.Target.TypicalAltitude + 500f,
|
||||
(float)threat.Target.TypicalAltitude);
|
||||
fireTime = interceptTime - flightTime - fallTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
fireTime = interceptTime - flightTime; // flightTime = shell time
|
||||
}
|
||||
|
||||
if (fireTime < 0.1f) fireTime = 0.1f;
|
||||
|
||||
events.Add(new FireEvent
|
||||
{
|
||||
FireTime = fireTime,
|
||||
PlatformIndex = i % rounds,
|
||||
TargetX = mid.X + offset,
|
||||
TargetY = mid.Y,
|
||||
TargetZ = mid.Z,
|
||||
MuzzleVelocity = unit.MuzzleVelocity,
|
||||
});
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Algorithms
|
||||
{
|
||||
/// <summary>防御推荐接口 — 威胁驱动</summary>
|
||||
public interface IDefenseAdvisor
|
||||
{
|
||||
DefenseRecommendation Recommend(ThreatProfile threat);
|
||||
|
||||
/// <summary>多编队推荐 — 为每组分配火力单元,合并发射计划</summary>
|
||||
MultiGroupRecommendation RecommendMultiGroup(
|
||||
List<DroneGroup> droneGroups,
|
||||
List<FireUnit> fireUnits,
|
||||
CombatScene environment);
|
||||
}
|
||||
}
|
||||
14
src/CounterDrone.Core/Algorithms/IDefensePlanner.cs
Normal file
14
src/CounterDrone.Core/Algorithms/IDefensePlanner.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using System.Collections.Generic;
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Algorithms
|
||||
{
|
||||
/// <summary>防御规划引擎 — 给定火力单元池和威胁列表,输出分配方案</summary>
|
||||
public interface IDefensePlanner
|
||||
{
|
||||
/// <param name="fireUnits">可用的火力单元(空基/地基统一表达)</param>
|
||||
/// <param name="threats">威胁编队列表</param>
|
||||
/// <param name="environment">作战环境</param>
|
||||
PlannerResult Plan(List<FireUnit> fireUnits, List<DroneGroup> threats, CombatScene environment);
|
||||
}
|
||||
}
|
||||
@ -52,29 +52,42 @@ namespace CounterDrone.Unity
|
||||
foreach (var a in DefaultAmmunition.GetAll()) db.Insert(a);
|
||||
var ammoCatalog = db.Table<AmmunitionSpec>().ToList();
|
||||
|
||||
var detail = scenarioMgr.GetDetail(taskId);
|
||||
var rec = new DefaultDefenseAdvisor(ammoCatalog).Recommend(new ThreatProfile
|
||||
var droneGroup = new DroneGroup
|
||||
{
|
||||
Environment = detail.Scene, Targets = detail.Targets,
|
||||
Route = detail.Routes[0], Waypoints = detail.WaypointGroups["default"],
|
||||
GroupId = "default",
|
||||
Target = detail.Targets[0],
|
||||
Route = detail.Routes[0],
|
||||
Waypoints = detail.WaypointGroups["default"],
|
||||
};
|
||||
var fireUnits = new List<FireUnit>
|
||||
{
|
||||
new FireUnit { Id = "u0", Type = PlatformType.GroundBased, Position = new AVector3(5000, 0, 50), MuzzleVelocity = 800, TotalMunitions = 1, AmmoTypes = new() { AerosolType.InertGas, AerosolType.ActiveMaterial, AerosolType.ActiveFuel } },
|
||||
};
|
||||
var result = new DefaultDefensePlanner(ammoCatalog).Plan(fireUnits, new List<DroneGroup> { droneGroup }, detail.Scene);
|
||||
scenarioMgr.SaveCloud(taskId, new CloudDispersal
|
||||
{
|
||||
PositionX = (droneGroup.Waypoints[0].PosX + droneGroup.Waypoints[^1].PosX) / 2,
|
||||
PositionY = (droneGroup.Waypoints[0].PosY + droneGroup.Waypoints[^1].PosY) / 2,
|
||||
PositionZ = (droneGroup.Waypoints[0].PosZ + droneGroup.Waypoints[^1].PosZ) / 2,
|
||||
DisperseHeight = droneGroup.Target.TypicalAltitude,
|
||||
});
|
||||
scenarioMgr.SaveCloud(taskId, rec.Best.RecommendedCloud);
|
||||
|
||||
var equips = new List<EquipmentDeployment>();
|
||||
foreach (var p in rec.Best.Platforms)
|
||||
foreach (var u in fireUnits)
|
||||
equips.Add(new EquipmentDeployment
|
||||
{
|
||||
EquipmentRole = (int)EquipmentRole.LaunchPlatform,
|
||||
PlatformType = (int)p.Type, Quantity = 1,
|
||||
PositionX = p.Position.X, PositionY = p.Position.Y, PositionZ = p.Position.Z,
|
||||
AerosolType = (int)rec.Best.RecommendedAerosolType,
|
||||
MunitionCount = 1, Cooldown = p.Cooldown, MuzzleVelocity = p.MuzzleVelocity,
|
||||
CruiseSpeed = p.CruiseSpeed > 0 ? p.CruiseSpeed : null,
|
||||
ReleaseAltitude = p.ReleaseAltitude > 0 ? p.ReleaseAltitude : null,
|
||||
PlatformType = (int)u.Type, Quantity = 1,
|
||||
PositionX = u.Position.X, PositionY = u.Position.Y, PositionZ = u.Position.Z,
|
||||
AerosolType = (int)u.AmmoTypes[0],
|
||||
MunitionCount = u.TotalMunitions, Cooldown = u.Cooldown, MuzzleVelocity = u.MuzzleVelocity,
|
||||
CruiseSpeed = u.CruiseSpeed > 0 ? u.CruiseSpeed : null,
|
||||
ReleaseAltitude = u.ReleaseAltitude > 0 ? u.ReleaseAltitude : null,
|
||||
});
|
||||
scenarioMgr.SaveDeployment(taskId, equips);
|
||||
db.Close();
|
||||
Debug.Log($"4. Recommendation: {rec.Best.RecommendedAerosolType}, {rec.Best.Platforms.Count} guns, prob={rec.Best.InterceptProbability:P0}");
|
||||
var plan = result.Best;
|
||||
Debug.Log($"4. Plan: {plan.ThreatsEngaged} threats engaged, {plan.MergedSchedule.Count} rounds, prob={plan.OverallProbability:P0}");
|
||||
|
||||
var runner = gameObject.AddComponent<SimulationRunner>();
|
||||
runner.Awake();
|
||||
|
||||
@ -65,31 +65,46 @@ namespace CounterDrone.Unity
|
||||
// 推荐方案
|
||||
var detail = scenario.GetTaskDetail(taskId);
|
||||
var ammoCatalog = db.Table<AmmunitionSpec>().ToList();
|
||||
var rec = new DefaultDefenseAdvisor(ammoCatalog).Recommend(new ThreatProfile
|
||||
var planner = new DefaultDefensePlanner(ammoCatalog);
|
||||
var droneGroup = new DroneGroup
|
||||
{
|
||||
Environment = detail.Scene, Targets = detail.Targets,
|
||||
Route = detail.Routes[0], Waypoints = detail.WaypointGroups["default"],
|
||||
GroupId = "default",
|
||||
Target = detail.Targets[0],
|
||||
Route = detail.Routes[0],
|
||||
Waypoints = detail.WaypointGroups["default"],
|
||||
};
|
||||
var fireUnits = new List<FireUnit>
|
||||
{
|
||||
new FireUnit { Id = "u0", Type = PlatformType.GroundBased, Position = new AVector3(5000, 0, 50), MuzzleVelocity = 800, TotalMunitions = 1, AmmoTypes = new() { AerosolType.InertGas, AerosolType.ActiveMaterial, AerosolType.ActiveFuel } },
|
||||
};
|
||||
var result = planner.Plan(fireUnits, new List<DroneGroup> { droneGroup }, detail.Scene);
|
||||
scenario.SaveCloudDispersal(taskId, new CloudDispersal
|
||||
{
|
||||
PositionX = (droneGroup.Waypoints[0].PosX + droneGroup.Waypoints[^1].PosX) / 2,
|
||||
PositionY = (droneGroup.Waypoints[0].PosY + droneGroup.Waypoints[^1].PosY) / 2,
|
||||
PositionZ = (droneGroup.Waypoints[0].PosZ + droneGroup.Waypoints[^1].PosZ) / 2,
|
||||
DisperseHeight = droneGroup.Target.TypicalAltitude,
|
||||
Source = "Algorithm",
|
||||
});
|
||||
|
||||
scenario.SaveCloudDispersal(taskId, rec.Best.RecommendedCloud);
|
||||
|
||||
var equips = new List<EquipmentDeployment>();
|
||||
foreach (var p in rec.Best.Platforms)
|
||||
foreach (var u in fireUnits)
|
||||
equips.Add(new EquipmentDeployment
|
||||
{
|
||||
EquipmentRole = (int)EquipmentRole.LaunchPlatform,
|
||||
PlatformType = (int)p.Type, Quantity = 1,
|
||||
PositionX = p.Position.X, PositionY = p.Position.Y, PositionZ = p.Position.Z,
|
||||
AerosolType = (int)rec.Best.RecommendedAerosolType,
|
||||
MunitionCount = 1, Cooldown = p.Cooldown, MuzzleVelocity = p.MuzzleVelocity,
|
||||
CruiseSpeed = p.CruiseSpeed > 0 ? p.CruiseSpeed : null,
|
||||
ReleaseAltitude = p.ReleaseAltitude > 0 ? p.ReleaseAltitude : null,
|
||||
PlatformType = (int)u.Type, Quantity = 1,
|
||||
PositionX = u.Position.X, PositionY = u.Position.Y, PositionZ = u.Position.Z,
|
||||
AerosolType = (int)u.AmmoTypes[0],
|
||||
MunitionCount = u.TotalMunitions, Cooldown = u.Cooldown, MuzzleVelocity = u.MuzzleVelocity,
|
||||
CruiseSpeed = u.CruiseSpeed > 0 ? u.CruiseSpeed : null,
|
||||
ReleaseAltitude = u.ReleaseAltitude > 0 ? u.ReleaseAltitude : null,
|
||||
});
|
||||
scenario.SaveDeployment(taskId, equips);
|
||||
|
||||
db.Close();
|
||||
|
||||
Debug.Log($"推荐方案: {rec.Best.RecommendedAerosolType}, {rec.Best.Platforms.Count}门炮, 拦截概率 {rec.Best.InterceptProbability:P0}");
|
||||
var plan = result.Best;
|
||||
Debug.Log($"规划方案: {plan.ThreatsEngaged} 威胁被分配, {plan.MergedSchedule.Count} 发, 概率 {plan.OverallProbability:P0}");
|
||||
|
||||
// 运行仿真
|
||||
Runner = GetComponent<SimulationRunner>();
|
||||
|
||||
@ -14,8 +14,8 @@ namespace CounterDrone.Core.Tests
|
||||
var damage = AlgorithmFactory.Create<IDamageModel>();
|
||||
Assert.IsType<DamageModelRouter>(damage);
|
||||
|
||||
var advisor = AlgorithmFactory.Create<IDefenseAdvisor>();
|
||||
Assert.IsType<DefaultDefenseAdvisor>(advisor);
|
||||
var planner = AlgorithmFactory.Create<IDefensePlanner>();
|
||||
Assert.IsType<DefaultDefensePlanner>(planner);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@ -1,74 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using CounterDrone.Core.Algorithms;
|
||||
using CounterDrone.Core.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace CounterDrone.Core.Tests
|
||||
{
|
||||
public class DefenseAdvisorRealityTests
|
||||
{
|
||||
private static readonly List<AmmunitionSpec> TestAmmo = DefaultAmmunition.GetAll();
|
||||
|
||||
[Fact]
|
||||
public void PistonDrone_RecommendsReachableCloudPosition()
|
||||
{
|
||||
var threat = new ThreatProfile
|
||||
{
|
||||
Environment = new CombatScene
|
||||
{
|
||||
WindSpeed = 3,
|
||||
WindDirection = (int)WindDirection.W,
|
||||
},
|
||||
Targets = new List<TargetConfig>
|
||||
{
|
||||
new TargetConfig
|
||||
{
|
||||
TargetType = (int)TargetType.Piston,
|
||||
PowerType = (int)PowerType.Piston,
|
||||
Quantity = 1, TypicalSpeed = 200, TypicalAltitude = 500,
|
||||
},
|
||||
},
|
||||
Route = new RoutePlan { FormationMode = (int)FormationMode.Single },
|
||||
Waypoints = new List<Waypoint>
|
||||
{
|
||||
new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 },
|
||||
new Waypoint { PosX = 20000, PosY = 500, PosZ = 0, Speed = 200 },
|
||||
},
|
||||
};
|
||||
|
||||
var advisor = new DefaultDefenseAdvisor(TestAmmo);
|
||||
var rec = advisor.Recommend(threat);
|
||||
|
||||
// 1. 选型正确
|
||||
Assert.Equal(AerosolType.InertGas, rec.Best.RecommendedAerosolType);
|
||||
|
||||
// 2. 云团位置应在航路上
|
||||
var c = rec.Best.RecommendedCloud;
|
||||
Assert.True(c.PositionX > 0 && c.PositionX < 20000,
|
||||
$"云团 X={c.PositionX} 应在航路范围内");
|
||||
Assert.True(c.PositionY >= 400 && c.PositionY <= 600,
|
||||
$"云团 Y={c.PositionY} 应接近飞行高度 500");
|
||||
|
||||
// 3. 平台部署应有合理位置
|
||||
Assert.NotEmpty(rec.Best.Platforms);
|
||||
var p = rec.Best.Platforms[0];
|
||||
|
||||
// 4. 炮弹能打到吗?用运动学验证
|
||||
var muzzleV = 800f;
|
||||
var releaseAlt = (float)c.DisperseHeight;
|
||||
var targetDist = (float)System.Math.Sqrt(
|
||||
(c.PositionX - p.Position.X) * (c.PositionX - p.Position.X) +
|
||||
(c.PositionZ - p.Position.Z) * (c.PositionZ - p.Position.Z));
|
||||
|
||||
var launchAngle = Kinematics.CalculateLaunchAngle(targetDist, muzzleV, releaseAlt);
|
||||
Assert.True(launchAngle > 0 && launchAngle < 1.57f,
|
||||
$"发射角 {launchAngle * 180 / System.Math.PI:F1}° 应在 0-90°");
|
||||
|
||||
// 5. 弹道能到达释放高度
|
||||
var peakHeight = muzzleV * muzzleV * (float)System.Math.Sin(launchAngle)
|
||||
* (float)System.Math.Sin(launchAngle) / (2f * 9.81f);
|
||||
Assert.True(peakHeight >= releaseAlt,
|
||||
$"弹道顶点 {peakHeight:F0}m ≥ 释放高度 {releaseAlt:F0}m");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,66 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using CounterDrone.Core.Algorithms;
|
||||
using CounterDrone.Core.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace CounterDrone.Core.Tests
|
||||
{
|
||||
public class DefenseAdvisorTests
|
||||
{
|
||||
private static readonly List<AmmunitionSpec> TestAmmo = DefaultAmmunition.GetAll();
|
||||
|
||||
private ThreatProfile CreateSimpleThreat(PowerType powerType = PowerType.Piston)
|
||||
{
|
||||
return new ThreatProfile
|
||||
{
|
||||
Environment = new CombatScene { WindSpeed = 5.0, WindDirection = (int)WindDirection.E },
|
||||
Targets = new List<TargetConfig>
|
||||
{
|
||||
new TargetConfig { TargetType = (int)TargetType.Piston, PowerType = (int)powerType, Quantity = 3, TypicalSpeed = 120.0, TypicalAltitude = 500.0 },
|
||||
},
|
||||
Route = new RoutePlan { FormationMode = (int)FormationMode.Single },
|
||||
Waypoints = new List<Waypoint>
|
||||
{
|
||||
new Waypoint { PosX = 0, PosY = 500, PosZ = 100, Altitude = 500, Speed = 120 },
|
||||
new Waypoint { PosX = 10000, PosY = 500, PosZ = 100, Altitude = 500, Speed = 120 },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
[Fact] public void Piston_InertGas() { var r = new DefaultDefenseAdvisor(TestAmmo).Recommend(CreateSimpleThreat(PowerType.Piston)); Assert.Equal(AerosolType.InertGas, r.Best.RecommendedAerosolType); Assert.Contains("惰性气体", r.Best.AerosolRationale); Assert.True(r.Best.InterceptProbability > 0); }
|
||||
[Fact] public void Jet_ActiveMaterial() { var r = new DefaultDefenseAdvisor(TestAmmo).Recommend(CreateSimpleThreat(PowerType.Jet)); Assert.Equal(AerosolType.ActiveMaterial, r.Best.RecommendedAerosolType); Assert.Contains("活性材料", r.Best.AerosolRationale); }
|
||||
[Fact] public void Electric_InertGas() { var r = new DefaultDefenseAdvisor(TestAmmo).Recommend(CreateSimpleThreat(PowerType.Electric)); Assert.Equal(AerosolType.InertGas, r.Best.RecommendedAerosolType); Assert.Contains("惰性气体", r.Best.AerosolRationale); }
|
||||
[Fact] public void Best_Critical_Ordered() { var r = new DefaultDefenseAdvisor(TestAmmo).Recommend(CreateSimpleThreat()); Assert.True(r.Best.InterceptProbability >= r.Critical.InterceptProbability); }
|
||||
[Fact] public void Cloud_OnRoute() { var r = new DefaultDefenseAdvisor(TestAmmo).Recommend(CreateSimpleThreat()); Assert.True(r.Best.RecommendedCloud.PositionX > 1000 && r.Best.RecommendedCloud.PositionX < 9000); }
|
||||
[Fact] public void Has_Platforms() { var r = new DefaultDefenseAdvisor(TestAmmo).Recommend(CreateSimpleThreat()); Assert.NotEmpty(r.Best.Platforms); }
|
||||
[Fact] public void Has_Detection() { var r = new DefaultDefenseAdvisor(TestAmmo).Recommend(CreateSimpleThreat()); Assert.NotEmpty(r.Best.Detections); }
|
||||
[Fact] public void Source_Algorithm() { var r = new DefaultDefenseAdvisor(TestAmmo).Recommend(CreateSimpleThreat()); Assert.Equal("Algorithm", r.Best.RecommendedCloud.Source); }
|
||||
|
||||
[Fact]
|
||||
public void NoTargets_ReturnsFailure()
|
||||
{
|
||||
var threat = CreateSimpleThreat();
|
||||
threat.Targets.Clear();
|
||||
var r = new DefaultDefenseAdvisor(TestAmmo).Recommend(threat);
|
||||
Assert.Equal(0f, r.Best.InterceptProbability);
|
||||
Assert.Contains("失败", r.Best.SummaryRationale);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoWaypoints_ReturnsFailure()
|
||||
{
|
||||
var threat = CreateSimpleThreat();
|
||||
threat.Waypoints.Clear();
|
||||
var r = new DefaultDefenseAdvisor(TestAmmo).Recommend(threat);
|
||||
Assert.Equal(0f, r.Best.InterceptProbability);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoAmmo_ReturnsFailure()
|
||||
{
|
||||
var r = new DefaultDefenseAdvisor(new List<AmmunitionSpec>()).Recommend(CreateSimpleThreat());
|
||||
Assert.Equal(0f, r.Best.InterceptProbability);
|
||||
Assert.Contains("未找到", r.Best.SummaryRationale);
|
||||
}
|
||||
}
|
||||
}
|
||||
143
test/unit/CounterDrone.Core.Tests/DefensePlannerTests.cs
Normal file
143
test/unit/CounterDrone.Core.Tests/DefensePlannerTests.cs
Normal file
@ -0,0 +1,143 @@
|
||||
using System.Collections.Generic;
|
||||
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 List<FireUnit> MakeGroundUnits(int count = 5)
|
||||
{
|
||||
var units = new List<FireUnit>();
|
||||
for (int i = 0; i < count; i++)
|
||||
units.Add(new FireUnit
|
||||
{
|
||||
Id = $"u{i}",
|
||||
Type = PlatformType.GroundBased,
|
||||
Position = new Vector3(5000 + i * 50, 0, 50),
|
||||
MuzzleVelocity = 800f,
|
||||
TotalMunitions = 3,
|
||||
AmmoTypes = new List<AerosolType> { AerosolType.InertGas, AerosolType.ActiveMaterial, AerosolType.ActiveFuel },
|
||||
});
|
||||
return units;
|
||||
}
|
||||
|
||||
private DroneGroup MakeThreat(PowerType powerType = PowerType.Piston, float speed = 120f)
|
||||
{
|
||||
return new DroneGroup
|
||||
{
|
||||
GroupId = "default",
|
||||
Target = new TargetConfig
|
||||
{
|
||||
TargetType = (int)TargetType.Piston,
|
||||
PowerType = (int)powerType,
|
||||
Quantity = 1,
|
||||
TypicalSpeed = speed,
|
||||
TypicalAltitude = 500,
|
||||
},
|
||||
Waypoints = new List<Waypoint>
|
||||
{
|
||||
new Waypoint { PosX = 0, PosY = 500, PosZ = 100, Altitude = 500, Speed = speed },
|
||||
new Waypoint { PosX = 10000, PosY = 500, PosZ = 100, Altitude = 500, Speed = speed },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Piston_Engaged()
|
||||
{
|
||||
var result = new DefaultDefensePlanner(TestAmmo).Plan(
|
||||
MakeGroundUnits(),
|
||||
new List<DroneGroup> { MakeThreat(PowerType.Piston) },
|
||||
new CombatScene { WindSpeed = 5 });
|
||||
Assert.True(result.Best.ThreatsEngaged > 0);
|
||||
Assert.Contains("分配", result.Best.Summary);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jet_Engaged()
|
||||
{
|
||||
var result = new DefaultDefensePlanner(TestAmmo).Plan(
|
||||
MakeGroundUnits(),
|
||||
new List<DroneGroup> { MakeThreat(PowerType.Jet, 500) },
|
||||
new CombatScene());
|
||||
Assert.True(result.Best.ThreatsEngaged > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Electric_Engaged()
|
||||
{
|
||||
var result = new DefaultDefensePlanner(TestAmmo).Plan(
|
||||
MakeGroundUnits(),
|
||||
new List<DroneGroup> { MakeThreat(PowerType.Electric) },
|
||||
new CombatScene());
|
||||
Assert.True(result.Best.ThreatsEngaged > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Best_HigherThanCritical()
|
||||
{
|
||||
var result = new DefaultDefensePlanner(TestAmmo).Plan(
|
||||
MakeGroundUnits(10),
|
||||
new List<DroneGroup> { MakeThreat() },
|
||||
new CombatScene());
|
||||
Assert.True(result.Best.ThreatsEngaged > 0);
|
||||
Assert.True(result.Critical.ThreatsEngaged > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Has_FireSchedule()
|
||||
{
|
||||
var result = new DefaultDefensePlanner(TestAmmo).Plan(
|
||||
MakeGroundUnits(),
|
||||
new List<DroneGroup> { MakeThreat() },
|
||||
new CombatScene());
|
||||
Assert.NotEmpty(result.Best.MergedSchedule);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Has_Assignments()
|
||||
{
|
||||
var result = new DefaultDefensePlanner(TestAmmo).Plan(
|
||||
MakeGroundUnits(),
|
||||
new List<DroneGroup> { MakeThreat() },
|
||||
new CombatScene());
|
||||
Assert.NotEmpty(result.Best.Assignments);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Critical_Within50Percent()
|
||||
{
|
||||
var result = new DefaultDefensePlanner(TestAmmo).Plan(
|
||||
MakeGroundUnits(10),
|
||||
new List<DroneGroup> { MakeThreat() },
|
||||
new CombatScene());
|
||||
// Critical 方案概率标记为 0.5(阈值下限)
|
||||
Assert.True(result.Critical.OverallProbability >= 0.45f);
|
||||
Assert.True(result.Critical.Assignments.Count > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoUnits_ThreatsUnengaged()
|
||||
{
|
||||
var result = new DefaultDefensePlanner(TestAmmo).Plan(
|
||||
new List<FireUnit>(),
|
||||
new List<DroneGroup> { MakeThreat() },
|
||||
new CombatScene());
|
||||
Assert.Equal(1, result.Best.ThreatsUnengaged);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoThreats_EmptyPlan()
|
||||
{
|
||||
var result = new DefaultDefensePlanner(TestAmmo).Plan(
|
||||
MakeGroundUnits(),
|
||||
new List<DroneGroup>(),
|
||||
new CombatScene());
|
||||
Assert.Equal(0, result.Best.ThreatsEngaged);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,60 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CounterDrone.Core.Algorithms;
|
||||
using CounterDrone.Core.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace CounterDrone.Core.Tests
|
||||
{
|
||||
public class DispersionAdvisorRealityTests
|
||||
{
|
||||
private ThreatProfile CreatePistonThreat(float speed = 200, float length = 20000)
|
||||
{
|
||||
return new ThreatProfile
|
||||
{
|
||||
Environment = new CombatScene { WindSpeed = 3, WindDirection = (int)WindDirection.W },
|
||||
Targets = new List<TargetConfig>
|
||||
{
|
||||
new TargetConfig { PowerType = (int)PowerType.Piston, Quantity = 1,
|
||||
TypicalSpeed = speed, TypicalAltitude = 500 },
|
||||
},
|
||||
Route = new RoutePlan { FormationMode = (int)FormationMode.Single },
|
||||
Waypoints = new List<Waypoint>
|
||||
{
|
||||
new Waypoint { PosX = 0, PosY = 500, PosZ = 0 },
|
||||
new Waypoint { PosX = length, PosY = 500, PosZ = 0 },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Piston_200kmh_20km_ProducesReasonableSalvo()
|
||||
{
|
||||
var advisor = new DefaultDefenseAdvisor(DefaultAmmunition.GetAll());
|
||||
var rec = advisor.Recommend(CreatePistonThreat());
|
||||
|
||||
Assert.Equal(AerosolType.InertGas, rec.Best.RecommendedAerosolType);
|
||||
Assert.NotEmpty(rec.Best.FireSchedule);
|
||||
Assert.True(rec.Best.FireSchedule.Count >= 1, $"齐射数={rec.Best.FireSchedule.Count},应≥1");
|
||||
|
||||
// 发射计划中的云团位置应在航路范围内且沿航路展开
|
||||
var positions = rec.Best.FireSchedule.Select(f => f.TargetX).OrderBy(x => x).ToList();
|
||||
Assert.True(positions.First() > 0, "第一个云团应在航路起点之后");
|
||||
Assert.True(positions.Last() < 20000, "最后一个云团应在航路终点之前");
|
||||
Assert.True(positions.Last() - positions.First() > 10, "云团应沿航路展开");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Timing_AccountsForExpansion()
|
||||
{
|
||||
var advisor = new DefaultDefenseAdvisor(DefaultAmmunition.GetAll());
|
||||
var rec = advisor.Recommend(CreatePistonThreat());
|
||||
|
||||
var timing = rec.Best.RecommendedCloud.RecommendedTiming!.Value;
|
||||
var halfTime = 20000f / (200f / 3.6f) / 2f;
|
||||
|
||||
Assert.True(timing < halfTime - 5,
|
||||
$"推荐时机={timing:F1}s 应早于中点={halfTime:F1}s(预留飞行+膨胀时间)");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -77,53 +77,88 @@ namespace CounterDrone.Core.Tests
|
||||
return _lastFireSchedule ?? new List<FireEvent>();
|
||||
}
|
||||
|
||||
/// <summary>将 DefenseAdvisor 推荐方案写入想定</summary>
|
||||
private DefenseRecommendation GetAndApplyRecommendation(PlatformType? preferredPlatform = null)
|
||||
/// <summary>创建默认火力单元并调用 Planner 生成方案</summary>
|
||||
private DefensePlan ApplyDefensePlan(PlatformType? preferredPlatform = null)
|
||||
{
|
||||
var detail = _scenario.GetTaskDetail(_taskId)!;
|
||||
var threat = new ThreatProfile
|
||||
|
||||
// 从已保存的 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))
|
||||
{
|
||||
Environment = detail.Scene,
|
||||
Targets = detail.Targets,
|
||||
Route = detail.Routes[0],
|
||||
Waypoints = detail.WaypointGroups["default"],
|
||||
PreferredPlatformType = preferredPlatform,
|
||||
};
|
||||
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
|
||||
fireUnits.Add(new FireUnit
|
||||
{
|
||||
EquipmentRole = (int)EquipmentRole.Detection,
|
||||
Quantity = d.Quantity,
|
||||
PositionX = d.Position.X,
|
||||
PositionY = d.Position.Y,
|
||||
PositionZ = d.Position.Z,
|
||||
DetectionRadius = d.DetectionRadius,
|
||||
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,
|
||||
});
|
||||
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 > 0 ? p.MuzzleVelocity : null,
|
||||
CruiseSpeed = p.CruiseSpeed > 0 ? p.CruiseSpeed : null,
|
||||
ReleaseAltitude = p.ReleaseAltitude > 0 ? p.ReleaseAltitude : null,
|
||||
});
|
||||
_scenario.SaveDeployment(_taskId, equips);
|
||||
_lastFireSchedule = rec.Best.FireSchedule;
|
||||
}
|
||||
|
||||
return rec;
|
||||
// 如果没装备,生成默认的地面单元
|
||||
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(
|
||||
@ -247,30 +282,22 @@ namespace CounterDrone.Core.Tests
|
||||
new Waypoint { PosX = 20000, PosY = 500, PosZ = 0, Speed = 200 },
|
||||
});
|
||||
|
||||
var rec = GetAndApplyRecommendation();
|
||||
// 部署地基单元
|
||||
_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 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 plan = ApplyDefensePlan();
|
||||
Assert.True(plan.ThreatsEngaged > 0);
|
||||
Assert.NotEmpty(plan.MergedSchedule);
|
||||
|
||||
var eng = RunSimulation(8000);
|
||||
|
||||
@ -278,7 +305,7 @@ namespace CounterDrone.Core.Tests
|
||||
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}, " +
|
||||
var msg = $"Piston: launched={launched}, clouds={cloudsGenerated}, " +
|
||||
$"droneStatus={drone.Status}, hp={drone.Hp:F3}, posX={drone.PosX:F0}, " +
|
||||
$"cloudCount={eng.Clouds.Count}";
|
||||
|
||||
@ -314,17 +341,21 @@ namespace CounterDrone.Core.Tests
|
||||
new Waypoint { PosX = 30000, PosY = 800, PosZ = 0, Speed = 500 },
|
||||
});
|
||||
|
||||
var rec = GetAndApplyRecommendation();
|
||||
_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 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 plan = ApplyDefensePlan();
|
||||
Assert.True(plan.ThreatsEngaged > 0);
|
||||
Assert.NotEmpty(plan.MergedSchedule);
|
||||
|
||||
var eng = RunSimulation(8000);
|
||||
|
||||
@ -332,7 +363,7 @@ namespace CounterDrone.Core.Tests
|
||||
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}, " +
|
||||
var msg = $"Jet: launched={launched}, clouds={cloudsGenerated}, " +
|
||||
$"droneStatus={drone.Status}, hp={drone.Hp:F3}, posX={drone.PosX:F0}, " +
|
||||
$"cloudCount={eng.Clouds.Count}";
|
||||
|
||||
@ -367,14 +398,22 @@ namespace CounterDrone.Core.Tests
|
||||
new Waypoint { PosX = 15000, PosY = 500, PosZ = 0, Speed = 300 },
|
||||
});
|
||||
|
||||
// 用推荐算法生成空基方案
|
||||
var rec = GetAndApplyRecommendation(PlatformType.AirBased);
|
||||
Assert.NotNull(rec.Best.FireSchedule);
|
||||
Assert.True(rec.Best.FireSchedule.Count > 0);
|
||||
Assert.Equal(PlatformType.AirBased, rec.Best.Platforms[0].Type);
|
||||
Assert.True(rec.Best.Platforms[0].CruiseSpeed > 0);
|
||||
// 部署空基平台单元
|
||||
_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,
|
||||
},
|
||||
});
|
||||
|
||||
_lastFireSchedule = rec.Best.FireSchedule;
|
||||
var plan = ApplyDefensePlan(PlatformType.AirBased);
|
||||
Assert.True(plan.ThreatsEngaged > 0);
|
||||
Assert.NotEmpty(plan.MergedSchedule);
|
||||
|
||||
var eng = RunSimulation(8000);
|
||||
|
||||
@ -382,7 +421,7 @@ namespace CounterDrone.Core.Tests
|
||||
var cloudsGenerated = eng.Events.Count(e => e.Type == SimEventType.CloudGenerated);
|
||||
var drone = eng.Drones[0];
|
||||
|
||||
var msg = $"AirBased: platforms={rec.Best.Platforms.Count}, launched={launched}, clouds={cloudsGenerated}, " +
|
||||
var msg = $"AirBased: launched={launched}, clouds={cloudsGenerated}, " +
|
||||
$"droneStatus={drone.Status}, hp={drone.Hp:F3}, simTime={eng.SimulationTime:F1}s";
|
||||
|
||||
Assert.True(launched > 0, msg);
|
||||
|
||||
@ -1,47 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using CounterDrone.Core.Algorithms;
|
||||
using CounterDrone.Core.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace CounterDrone.Core.Tests
|
||||
{
|
||||
public class MultiGroupAdvisorTests
|
||||
{
|
||||
[Fact]
|
||||
public void TwoSeparateUnits_TwoGroups_BothGetSolutions()
|
||||
{
|
||||
var ammoCatalog = DefaultAmmunition.GetAll();
|
||||
Console.WriteLine($"Ammo: inert={ammoCatalog[0].BurstChargeKg}kg, active={ammoCatalog[1].BurstChargeKg}kg");
|
||||
|
||||
var advisor = new DefaultDefenseAdvisor(ammoCatalog);
|
||||
|
||||
var g1 = new DroneGroup
|
||||
{
|
||||
GroupId = "g1", Target = new TargetConfig { PowerType = (int)PowerType.Piston, Quantity = 1, TypicalSpeed = 200, TypicalAltitude = 500 },
|
||||
Route = new RoutePlan { FormationMode = (int)FormationMode.Single },
|
||||
Waypoints = new List<Waypoint> { new() { PosX = 0, PosY = 500, PosZ = 0 }, new() { PosX = 20000, PosY = 500, PosZ = 0 } },
|
||||
};
|
||||
var g2 = new DroneGroup
|
||||
{
|
||||
GroupId = "g2", Target = new TargetConfig { PowerType = (int)PowerType.Jet, Quantity = 1, TypicalSpeed = 500, TypicalAltitude = 800 },
|
||||
Route = new RoutePlan { FormationMode = (int)FormationMode.Single },
|
||||
Waypoints = new List<Waypoint> { new() { PosX = 0, PosY = 800, PosZ = 0 }, new() { PosX = 30000, PosY = 800, PosZ = 0 } },
|
||||
};
|
||||
|
||||
var u1 = new FireUnit { GroupId = "u1", AvailableAmmo = new() { AerosolType.InertGas, AerosolType.ActiveMaterial }, PlatformCount = 5, MuzzleVelocity = 800 };
|
||||
var u2 = new FireUnit { GroupId = "u2", AvailableAmmo = new() { AerosolType.ActiveMaterial }, PlatformCount = 4, MuzzleVelocity = 800 };
|
||||
|
||||
var rec = advisor.RecommendMultiGroup(
|
||||
new List<DroneGroup> { g1, g2 },
|
||||
new List<FireUnit> { u1, u2 },
|
||||
new CombatScene { WindSpeed = 3 });
|
||||
|
||||
var msg = $"Solutions={rec.GroupSolutions.Count}, Merged={rec.MergedFireSchedule.Count}";
|
||||
foreach (var s in rec.GroupSolutions)
|
||||
msg += $" | {s.RecommendedAerosolType}: plat={s.Platforms.Count} fire={s.FireSchedule.Count}";
|
||||
Assert.True(rec.GroupSolutions.Count == 2, msg);
|
||||
Assert.NotEmpty(rec.MergedFireSchedule);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user