477 lines
21 KiB
C#
477 lines
21 KiB
C#
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>();
|
||
}
|
||
|
||
private static readonly Dictionary<PowerType, AerosolType> MatchTable = new()
|
||
{
|
||
{ PowerType.Electric, AerosolType.InertGas },
|
||
{ PowerType.Piston, AerosolType.InertGas },
|
||
{ PowerType.Jet, AerosolType.ActiveMaterial },
|
||
};
|
||
|
||
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: 临界方案
|
||
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-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;
|
||
|
||
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 neededAmmo = MatchAmmo((PowerType)threat.Target.PowerType);
|
||
var ammo = _ammoCatalog.FirstOrDefault(a => a.AerosolType == (int)neededAmmo);
|
||
int totalRoundsNeeded = CalcRoundsNeeded(threat, ammo, env,
|
||
candidates[0].Unit.Type == PlatformType.AirBased);
|
||
|
||
// 从多个候选单元凑弹药,直到满足需求或耗尽
|
||
int roundsCollected = 0;
|
||
var assignedUnits = new List<(FireUnit unit, int rounds, List<FireEvent> events)>();
|
||
|
||
foreach (var c in candidates.OrderBy(c => c.EarliestInterceptTime)
|
||
.ThenByDescending(c => c.KillProbability))
|
||
{
|
||
if (roundsCollected >= totalRoundsNeeded) break;
|
||
int remaining = remainingMunitions.GetValueOrDefault(c.Unit.Id, 0);
|
||
if (remaining <= 0) continue;
|
||
|
||
int toTake = Math.Min(remaining, totalRoundsNeeded - roundsCollected);
|
||
var fireEvents = GenerateFireEvents(threat, c.Unit, c.AmmoType, toTake, ammo, env);
|
||
assignedUnits.Add((c.Unit, toTake, fireEvents));
|
||
remainingMunitions[c.Unit.Id] -= toTake;
|
||
roundsCollected += toTake;
|
||
}
|
||
|
||
if (roundsCollected <= 0) { plan.ThreatsUnengaged++; continue; }
|
||
|
||
foreach (var (unit, rounds, fireEvents) in assignedUnits)
|
||
{
|
||
plan.Assignments.Add(new UnitAssignment
|
||
{
|
||
FireUnitId = unit.Id,
|
||
DroneGroupId = threat.GroupId,
|
||
AmmoType = neededAmmo,
|
||
RoundsFired = rounds,
|
||
FirstFireTime = fireEvents.Count > 0 ? fireEvents[0].FireTime : 0,
|
||
FireEvents = fireEvents,
|
||
});
|
||
plan.MergedSchedule.AddRange(fireEvents);
|
||
}
|
||
|
||
plan.ThreatsEngaged++;
|
||
}
|
||
|
||
plan.MergedSchedule.Sort((a, b) => a.FireTime.CompareTo(b.FireTime));
|
||
|
||
// 按威胁汇总概率
|
||
var threatProbs = new List<float>();
|
||
foreach (var threat in sortedThreats)
|
||
{
|
||
var a2 = _ammoCatalog.FirstOrDefault(s =>
|
||
s.AerosolType == (int)MatchAmmo((PowerType)threat.Target.PowerType));
|
||
int totalRounds = plan.Assignments
|
||
.Where(a => a.DroneGroupId == threat.GroupId)
|
||
.Sum(a => a.RoundsFired);
|
||
if (totalRounds > 0)
|
||
threatProbs.Add(ComputeInterceptProbability(threat, a2, totalRounds, env));
|
||
}
|
||
plan.OverallProbability = threatProbs.Count > 0 ? threatProbs.Average() : 0f;
|
||
plan.Summary = plan.ThreatsEngaged > 0
|
||
? $"分配 {plan.ThreatsEngaged} 个威胁,{plan.ThreatsUnengaged} 个无方案"
|
||
: "无威胁被分配拦截方案";
|
||
|
||
return plan;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// 候选生成(使用真实物理)
|
||
// ═══════════════════════════════════════════════
|
||
|
||
private List<InterceptCandidate> GenerateCandidates(DroneGroup threat,
|
||
List<FireUnit> availableUnits, CombatScene env)
|
||
{
|
||
var candidates = new List<InterceptCandidate>();
|
||
var neededAmmo = MatchAmmo((PowerType)threat.Target.PowerType);
|
||
var ammo = _ammoCatalog.FirstOrDefault(a => a.AerosolType == (int)neededAmmo);
|
||
if (ammo == null) return candidates;
|
||
|
||
var ammoEff = ComputeEffectiveRadius(threat, ammo, env);
|
||
|
||
foreach (var unit in availableUnits)
|
||
{
|
||
if (!unit.AmmoTypes.Contains(neededAmmo)) continue;
|
||
|
||
if (unit.Type == PlatformType.AirBased)
|
||
{
|
||
var c = BuildAirBasedCandidate(threat, unit, neededAmmo, ammo, ammoEff, env);
|
||
if (c != null) candidates.Add(c);
|
||
}
|
||
else
|
||
{
|
||
var c = BuildGroundBasedCandidate(threat, unit, neededAmmo, ammo, ammoEff, env);
|
||
if (c != null) candidates.Add(c);
|
||
}
|
||
}
|
||
|
||
return candidates;
|
||
}
|
||
|
||
private InterceptCandidate? BuildGroundBasedCandidate(DroneGroup threat,
|
||
FireUnit unit, AerosolType ammoType, AmmunitionSpec ammo,
|
||
(float radius, float expTime) ammoEff, 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;
|
||
|
||
float avgSpeed = (float)threat.Target.TypicalSpeed / 3.6f;
|
||
float neededExposure = ammoType == AerosolType.ActiveMaterial ? 2f : 6f;
|
||
float actualExposure = ammoEff.radius * 2f / avgSpeed;
|
||
float prob = Math.Min(0.95f, actualExposure / neededExposure);
|
||
|
||
return new InterceptCandidate
|
||
{
|
||
Unit = unit,
|
||
AmmoType = ammoType,
|
||
EarliestInterceptTime = interceptTime,
|
||
CoverageDuration = ammoEff.radius * 2f,
|
||
KillProbability = prob,
|
||
FlightTime = shellTime,
|
||
ShellFlightTime = shellTime,
|
||
};
|
||
}
|
||
|
||
private InterceptCandidate? BuildAirBasedCandidate(DroneGroup threat,
|
||
FireUnit unit, AerosolType ammoType, AmmunitionSpec ammo,
|
||
(float radius, float expTime) ammoEff, 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;
|
||
|
||
float avgSpeed = (float)threat.Target.TypicalSpeed / 3.6f;
|
||
float neededExposure = ammoType == AerosolType.ActiveMaterial ? 2f : 6f;
|
||
float actualExposure = ammoEff.radius * 2f / avgSpeed;
|
||
float prob = Math.Min(0.95f, actualExposure / neededExposure);
|
||
|
||
return new InterceptCandidate
|
||
{
|
||
Unit = unit,
|
||
AmmoType = ammoType,
|
||
EarliestInterceptTime = interceptTime,
|
||
CoverageDuration = ammoEff.radius * 2f,
|
||
KillProbability = prob,
|
||
FlightTime = totalTime,
|
||
ShellFlightTime = fallTime,
|
||
};
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// 弹药计算(使用 AmmunitionSpec + 环境参数)
|
||
// ═══════════════════════════════════════════════
|
||
|
||
private (float effectiveRadius, float expansionTime) ComputeEffectiveRadius(
|
||
DroneGroup threat, AmmunitionSpec ammo, CombatScene env)
|
||
{
|
||
float totalFlightTime = threat.ArrivalTime * 2f;
|
||
float halfTime = totalFlightTime / 2f;
|
||
float windSpeed = (float)env.WindSpeed;
|
||
var weather = (WeatherType)env.WeatherType;
|
||
float avgSpeed = (float)threat.Target.TypicalSpeed / 3.6f;
|
||
|
||
// Phase 1 初始半径(基于爆发药 TNT 当量)
|
||
float r0 = 3.3f * (float)Math.Pow(Math.Max(0.01, (float)ammo.BurstChargeKg), 0.32);
|
||
float k = (float)ammo.TurbulentExpansionK;
|
||
float maxDur = (float)ammo.MaxDuration;
|
||
|
||
// Phase 2 湍流膨胀后的半径
|
||
float phase2Time = Math.Min(halfTime, 30f);
|
||
float rPhase2 = r0 + k * (float)Math.Sqrt(Math.Max(0, phase2Time));
|
||
|
||
float expansionTime = (float)Math.Pow((rPhase2 - r0) / Math.Max(k, 0.01f), 2);
|
||
float effectiveR = rPhase2;
|
||
|
||
// Phase 3 高斯扩散
|
||
if (halfTime > 30f)
|
||
{
|
||
float x = Math.Max(1f, windSpeed * (halfTime - 30f));
|
||
var cls = Kinematics.GetStabilityClass(weather, windSpeed);
|
||
float sY = Kinematics.SigmaY(cls, x);
|
||
float sZ = Kinematics.SigmaZ(cls, x);
|
||
float peakC = Kinematics.GaussianPeakConcentration((float)ammo.SourceStrength, sY, sZ);
|
||
float threshold = (float)ammo.EffectiveConcentration;
|
||
if (peakC > threshold)
|
||
{
|
||
float sigma = (sY + sZ) / 2f;
|
||
effectiveR = rPhase2 + sigma * (float)Math.Sqrt(2f * Math.Log(peakC / threshold));
|
||
}
|
||
}
|
||
|
||
return (effectiveR, expansionTime);
|
||
}
|
||
|
||
private int CalcRoundsNeeded(DroneGroup threat, AmmunitionSpec? ammo,
|
||
CombatScene env, bool isAirBased)
|
||
{
|
||
if (ammo == null) return 1;
|
||
|
||
float avgSpeed = (float)threat.Target.TypicalSpeed / 3.6f;
|
||
var (effectiveR, _) = ComputeEffectiveRadius(threat, ammo, env);
|
||
float spacing = effectiveR * 1.5f;
|
||
|
||
var aerosolType = (AerosolType)ammo.AerosolType;
|
||
float neededExposure = aerosolType == AerosolType.ActiveMaterial ? 2f : 6f;
|
||
float requiredCoverage = neededExposure * avgSpeed;
|
||
|
||
return Math.Max(1,
|
||
(int)Math.Ceiling((requiredCoverage - 2f * effectiveR) / spacing) + 1);
|
||
}
|
||
|
||
private float ComputeInterceptProbability(DroneGroup threat,
|
||
AmmunitionSpec? ammo, int rounds, CombatScene env)
|
||
{
|
||
if (ammo == null) return 0f;
|
||
|
||
float avgSpeed = (float)threat.Target.TypicalSpeed / 3.6f;
|
||
var (effectiveR, _) = ComputeEffectiveRadius(threat, ammo, env);
|
||
float spacing = effectiveR * 1.5f;
|
||
float actualCoverage = spacing * (rounds - 1) + 2f * effectiveR;
|
||
float actualExposure = actualCoverage / avgSpeed;
|
||
|
||
var aerosolType = (AerosolType)ammo.AerosolType;
|
||
float neededExposure = aerosolType == AerosolType.ActiveMaterial ? 2f : 6f;
|
||
|
||
return Math.Min(0.95f, actualExposure / neededExposure);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// 发射事件生成(真实物理)
|
||
// ═══════════════════════════════════════════════
|
||
|
||
private List<FireEvent> GenerateFireEvents(DroneGroup threat, FireUnit unit,
|
||
AerosolType ammoType, int rounds, AmmunitionSpec? ammo, CombatScene env)
|
||
{
|
||
var events = new List<FireEvent>();
|
||
if (ammo == null) return events;
|
||
|
||
var mid = ThreatMidpoint(threat);
|
||
var (effectiveR, expansionTime) = ComputeEffectiveRadius(threat, ammo, env);
|
||
float spacing = effectiveR * 1.5f;
|
||
|
||
// 推荐时机:威胁到达中点时云团应已形成
|
||
float recommendedTiming = threat.ArrivalTime - expansionTime;
|
||
if (recommendedTiming < 0.1f) recommendedTiming = 0.1f;
|
||
|
||
for (int i = 0; i < rounds; i++)
|
||
{
|
||
float offset = (i - (rounds - 1) / 2f) * spacing;
|
||
float tx = mid.X + offset;
|
||
float tz = mid.Z;
|
||
float fireTime;
|
||
|
||
if (unit.Type == PlatformType.AirBased)
|
||
{
|
||
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,
|
||
tx, releaseAlt, tz);
|
||
float cruiseSpd = unit.CruiseSpeed > 0.1f ? unit.CruiseSpeed : 55f;
|
||
float flightTime = flightDist / cruiseSpd;
|
||
float fallTime = Kinematics.AirDropFallTime(releaseAlt, (float)threat.Target.TypicalAltitude);
|
||
fireTime = recommendedTiming - flightTime - fallTime;
|
||
}
|
||
else
|
||
{
|
||
float dx = tx - unit.Position.X;
|
||
float dz = tz - unit.Position.Z;
|
||
float dist = (float)Math.Sqrt(dx * dx + dz * dz);
|
||
float shellTime = dist / (unit.MuzzleVelocity > 0.1f ? unit.MuzzleVelocity : 800f);
|
||
fireTime = recommendedTiming - shellTime;
|
||
}
|
||
|
||
if (fireTime < 0.1f) fireTime = 0.1f;
|
||
|
||
events.Add(new FireEvent
|
||
{
|
||
FireTime = fireTime,
|
||
PlatformIndex = i % rounds,
|
||
TargetX = tx,
|
||
TargetY = mid.Y,
|
||
TargetZ = tz,
|
||
MuzzleVelocity = unit.MuzzleVelocity,
|
||
});
|
||
}
|
||
|
||
return events;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// Step 5: 临界方案(概率阈值 50%)
|
||
// ═══════════════════════════════════════════════
|
||
|
||
private DefensePlan DeriveCritical(DefensePlan best)
|
||
{
|
||
var critical = new DefensePlan
|
||
{
|
||
ThreatsEngaged = best.ThreatsEngaged,
|
||
ThreatsUnengaged = best.ThreatsUnengaged,
|
||
};
|
||
|
||
foreach (var assignment in best.Assignments)
|
||
{
|
||
int rounds = assignment.RoundsFired;
|
||
while (rounds > 1)
|
||
{
|
||
// 简化:弹药减半 → 概率减半
|
||
if ((float)rounds / assignment.RoundsFired < 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);
|
||
}
|
||
}
|
||
}
|