Phase 3: 算法层 — 高斯烟团扩散模型、3种毁伤模型、DefenseAdvisor、AlgorithmFactory,73个测试全部通过

This commit is contained in:
tian 2026-06-11 14:17:43 +08:00
parent b33a6a87e1
commit dc4ebc3f09
16 changed files with 985 additions and 2 deletions

View File

@ -0,0 +1,37 @@
using System;
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms
{
/// <summary>吸入式爆炸 — 指数累积型:暴露时间越长伤害越高</summary>
public class ActiveFuelDamageModel : IDamageModel
{
private const float EffectiveThreshold = 0.03f; // 有效浓度阈值
private const float BaseRate = 0.02f; // 基础速率
private const float ExpFactor = 0.3f; // 指数增长因子
public float CalculateDamage(TargetType droneType, PowerType powerType,
AerosolType aerosolType, float cloudDensity, float exposureTime, float deltaTime)
{
if (aerosolType != AerosolType.ActiveFuel) return 0f;
if (cloudDensity < EffectiveThreshold) return 0f;
// 伤害 = 基础速率 × e^(暴露时间 × 因子) × deltaTime
// 在云团中待得越久伤害指数增长
var exponential = (float)System.Math.Exp(exposureTime * ExpFactor);
var damage = BaseRate * exponential * deltaTime;
// 高速目标更脆弱
var sensitivity = droneType == TargetType.HighSpeed ? 1.5f : 1.0f;
return damage * sensitivity;
}
public DamageStage GetDamageStage(float accumulatedDamage)
{
if (accumulatedDamage >= 1.0f) return DamageStage.Destroyed;
if (accumulatedDamage >= 0.6f) return DamageStage.AttitudeLoss;
if (accumulatedDamage >= 0.3f) return DamageStage.EngineAnomaly;
return DamageStage.Normal;
}
}
}

View File

@ -0,0 +1,39 @@
using System;
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms
{
/// <summary>爆燃式 — 触发型:双条件满足后瞬间高伤害</summary>
public class ActiveMaterialDamageModel : IDamageModel
{
private const float TriggerThreshold = 0.1f; // 触发浓度阈值
private const float BurstDamage = 0.6f; // 一次爆发伤害
private const float ResidualRate = 0.02f; // 后续余伤速率
private bool _triggered;
public float CalculateDamage(TargetType droneType, PowerType powerType,
AerosolType aerosolType, float cloudDensity, float exposureTime, float deltaTime)
{
if (aerosolType != AerosolType.ActiveMaterial) return 0f;
if (cloudDensity < TriggerThreshold) return 0f;
// 喷气发动机高温触发爆燃,更敏感
if (!_triggered)
{
_triggered = true;
var sensitivity = powerType == PowerType.Jet ? 1.3f : 1.0f;
return BurstDamage * sensitivity;
}
return ResidualRate * deltaTime;
}
public DamageStage GetDamageStage(float accumulatedDamage)
{
if (accumulatedDamage >= 1.0f) return DamageStage.Destroyed;
if (accumulatedDamage >= 0.5f) return DamageStage.AttitudeLoss; // 爆燃后更快进入失控
if (accumulatedDamage >= 0.2f) return DamageStage.EngineAnomaly;
return DamageStage.Normal;
}
}
}

View File

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
namespace CounterDrone.Core.Algorithms
{
/// <summary>算法工厂 — 统一管理算法组件的创建与替换</summary>
public static class AlgorithmFactory
{
private static readonly Dictionary<Type, Type> _registry = new()
{
{ typeof(ICloudDispersionModel), typeof(GaussianPuffDispersion) },
{ typeof(IDamageModel), typeof(DamageModelRouter) },
{ typeof(IDefenseAdvisor), typeof(DefaultDefenseAdvisor) },
};
/// <summary>替换某个算法实现(第三方接入点)</summary>
public static void Register<TInterface, TImpl>() where TImpl : TInterface, new()
{
_registry[typeof(TInterface)] = typeof(TImpl);
}
/// <summary>创建算法实例</summary>
public static T Create<T>() where T : class
{
if (_registry.TryGetValue(typeof(T), out var implType))
return (T)Activator.CreateInstance(implType)!;
throw new InvalidOperationException(
$"No implementation registered for {typeof(T).Name}");
}
}
}

View File

@ -0,0 +1,74 @@
using System.Collections.Generic;
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms
{
/// <summary>威胁画像 — 防御推荐算法的输入</summary>
public class ThreatProfile
{
public CombatScene Environment { get; set; } = new();
public List<TargetConfig> Targets { get; set; } = new();
public RoutePlan Route { get; set; } = new();
public List<Waypoint> Waypoints { get; set; } = new();
public List<ControlZone> ControlZones { get; set; } = new();
}
/// <summary>防御推荐方案</summary>
public class DefenseRecommendation
{
public DefenseSolution Best { get; set; } = new();
public DefenseSolution Critical { get; set; } = new();
}
/// <summary>单套防御方案</summary>
public class DefenseSolution
{
public AerosolType RecommendedAerosolType { get; set; }
public string AerosolRationale { get; set; } = string.Empty;
public CloudDispersal RecommendedCloud { get; set; } = new();
public List<RecommendedPlatform> Platforms { get; set; } = new();
public List<RecommendedDetection> Detections { get; set; } = new();
public float InterceptProbability { get; set; }
public string SummaryRationale { get; set; } = string.Empty;
}
public class RecommendedPlatform
{
public PlatformType Type { get; set; }
public Vector3 Position { get; set; }
public int Quantity { get; set; }
public int MunitionCount { get; set; }
public float CoverageVolume { get; set; }
}
public class RecommendedDetection
{
public Vector3 Position { get; set; }
public float DetectionRadius { get; set; }
public int Quantity { get; set; }
}
/// <summary>三维向量(纯 C#,不依赖 UnityEngine</summary>
public struct Vector3
{
public float X, Y, Z;
public Vector3(float x, float y, float z) { X = x; Y = y; Z = z; }
public static Vector3 operator +(Vector3 a, Vector3 b) => new(a.X + b.X, a.Y + b.Y, a.Z + b.Z);
public static Vector3 operator -(Vector3 a, Vector3 b) => new(a.X - b.X, a.Y - b.Y, a.Z - b.Z);
public static Vector3 operator *(Vector3 a, float s) => new(a.X * s, a.Y * s, a.Z * s);
public float Length => (float)System.Math.Sqrt(X * X + Y * Y + Z * Z);
public float DistanceTo(Vector3 other) => (this - other).Length;
}
/// <summary>粒子渲染参数</summary>
public class ParticleParams
{
public float EmitRate { get; set; } = 100;
public float Opacity { get; set; } = 1.0f;
public string ColorHex { get; set; } = "#FFFFFF";
public float SizeMultiplier { get; set; } = 1.0f;
}
}

View File

@ -0,0 +1,32 @@
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms
{
/// <summary>毁伤模型路由器 — 根据气溶胶类型分派</summary>
public class DamageModelRouter : IDamageModel
{
private readonly InertGasDamageModel _inertGas = new();
private readonly ActiveMaterialDamageModel _activeMaterial = new();
private readonly ActiveFuelDamageModel _activeFuel = new();
public float CalculateDamage(TargetType droneType, PowerType powerType,
AerosolType aerosolType, float cloudDensity, float exposureTime, float deltaTime)
{
return aerosolType switch
{
AerosolType.InertGas => _inertGas.CalculateDamage(droneType, powerType, aerosolType, cloudDensity, exposureTime, deltaTime),
AerosolType.ActiveMaterial => _activeMaterial.CalculateDamage(droneType, powerType, aerosolType, cloudDensity, exposureTime, deltaTime),
AerosolType.ActiveFuel => _activeFuel.CalculateDamage(droneType, powerType, aerosolType, cloudDensity, exposureTime, deltaTime),
_ => 0f,
};
}
public DamageStage GetDamageStage(float accumulatedDamage)
{
if (accumulatedDamage >= 1.0f) return DamageStage.Destroyed;
if (accumulatedDamage >= 0.6f) return DamageStage.AttitudeLoss;
if (accumulatedDamage >= 0.25f) return DamageStage.EngineAnomaly;
return DamageStage.Normal;
}
}
}

View File

@ -0,0 +1,176 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms
{
public class DefaultDefenseAdvisor : IDefenseAdvisor
{
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)
{
if (threat.Targets.Count == 0 || threat.Waypoints.Count < 2)
return new DefenseRecommendation();
var target = threat.Targets[0];
var route = threat.Waypoints;
// Step A气溶胶选型
var powerType = (PowerType)target.PowerType;
var aerosolType = MatchTable.TryGetValue(powerType, out var match)
? match : AerosolType.InertGas;
var rationale = powerType switch
{
PowerType.Electric or PowerType.Piston =>
$"{powerType}发动机依赖氧气,推荐惰性气体窒息方案(吸入式灭火)",
PowerType.Jet =>
$"{powerType}发动机高温表面可触发活性材料爆燃反应",
_ => "默认推荐惰性气体方案",
};
// Step B时空交汇优化
var start = ToV3(route[0]);
var end = ToV3(route[^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; // km/h → m/s
var flightTime = routeLength / avgSpeed;
// 最佳值:航路中点
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 = 60,
Source = "Algorithm",
PositionMode = (int)PositionMode.AlgorithmRecommended,
RecommendedTiming = flightTime / 2f,
};
var bestProb = EstimateInterceptProbability(route, avgSpeed, mid, 50f, 60f);
// 最危险值:航路 1/4 处
var criticalPos = new Vector3(
start.X + (end.X - start.X) * 0.25f,
start.Y + (end.Y - start.Y) * 0.25f,
start.Z + (end.Z - start.Z) * 0.25f);
var criticalCloud = new CloudDispersal
{
AerosolType = (int)aerosolType,
PositionX = criticalPos.X,
PositionY = criticalPos.Y,
PositionZ = criticalPos.Z,
DisperseHeight = (float)target.TypicalAltitude,
TriggerMode = (int)TriggerMode.Time,
Duration = 60,
Source = "Algorithm",
PositionMode = (int)PositionMode.AlgorithmRecommended,
RecommendedTiming = flightTime / 4f,
};
var criticalProb = EstimateInterceptProbability(route, avgSpeed, criticalPos, 50f, 60f);
// Step C反推平台部署
var bestPlatforms = RecommendPlatforms(bestCloud, target);
return new DefenseRecommendation
{
Best = new DefenseSolution
{
RecommendedAerosolType = aerosolType,
AerosolRationale = rationale,
RecommendedCloud = bestCloud,
Platforms = bestPlatforms,
Detections = new List<RecommendedDetection>
{
new RecommendedDetection
{
Position = new Vector3(mid.X, mid.Y, 0),
DetectionRadius = System.Math.Max(3000f, routeLength * 0.4f),
Quantity = 1,
}
},
InterceptProbability = bestProb,
SummaryRationale = $"最佳方案:{aerosolType}方案,预计拦截概率 {bestProb:P0}",
},
Critical = new DefenseSolution
{
RecommendedAerosolType = aerosolType,
AerosolRationale = "临界方案",
RecommendedCloud = criticalCloud,
Platforms = RecommendPlatforms(criticalCloud, target),
Detections = new List<RecommendedDetection>(),
InterceptProbability = criticalProb,
SummaryRationale = $"最危险方案:拦截概率仅 {criticalProb:P0}",
},
};
}
private List<RecommendedPlatform> RecommendPlatforms(CloudDispersal cloud, TargetConfig target)
{
// 简化:默认地基火炮
var platforms = new List<RecommendedPlatform>();
var targetCount = target.Quantity;
var platformsNeeded = Math.Max(1, (int)Math.Ceiling(targetCount / 2.0));
for (int i = 0; i < platformsNeeded; i++)
{
platforms.Add(new RecommendedPlatform
{
Type = PlatformType.GroundBased,
Position = new Vector3((float)(cloud.PositionX + i * 200), 0, 50),
Quantity = 1,
MunitionCount = 3,
CoverageVolume = 500000f,
});
}
return platforms;
}
private float EstimateInterceptProbability(List<Waypoint> route, float avgSpeed,
Vector3 cloudPos, float cloudRadius, float cloudDuration)
{
// 简化估算:计算目标在云团中的穿越时间比例
float totalDist = 0;
float inCloudDist = 0;
for (int i = 0; i < route.Count - 1; i++)
{
var segStart = ToV3(route[i]);
var segEnd = ToV3(route[i + 1]);
var segLen = segStart.DistanceTo(segEnd);
totalDist += segLen;
// 简化:检查线段中点是否在云团内
var segMid = new Vector3(
(segStart.X + segEnd.X) / 2f,
(segStart.Y + segEnd.Y) / 2f,
(segStart.Z + segEnd.Z) / 2f);
if (segMid.DistanceTo(cloudPos) <= cloudRadius)
inCloudDist += segLen;
}
return totalDist > 0 ? System.Math.Min(0.95f, inCloudDist / totalDist) : 0f;
}
private static Vector3 ToV3(Waypoint wp)
{
return new Vector3((float)wp.PosX, (float)wp.PosY, (float)wp.PosZ);
}
}
}

View File

@ -0,0 +1,91 @@
using System;
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms
{
/// <summary>高斯烟团扩散模型 — 简化实现</summary>
public class GaussianPuffDispersion : ICloudDispersionModel
{
private AmmunitionSpec _ammo = null!;
private float _elapsed;
private float _currentRadius;
private Vector3 _center;
private Vector3 _windVelocity;
public Vector3 Center => _center;
public float Radius => _currentRadius;
public float CoreDensity { get; private set; }
public float EffectiveRadius => _currentRadius; // 简化:有效半径 = 物理半径
public ParticleParams Particles { get; } = new();
public bool IsDissipated { get; private set; }
public void Initialize(AmmunitionSpec ammo, CombatScene env, Vector3 releasePos, float releaseTime)
{
_ammo = ammo;
_elapsed = 0f;
_currentRadius = (float)ammo.InitialRadius;
_center = releasePos;
CoreDensity = (float)ammo.CoreDensity;
IsDissipated = false;
// 风速 → 速度矢量
var dir = WindToVector((WindDirection)env.WindDirection);
_windVelocity = dir * (float)env.WindSpeed;
}
public void Tick(float deltaTime, float windSpeed, WindDirection windDir)
{
if (IsDissipated) return;
_elapsed += deltaTime;
// 更新风速(允许动态变化)
var dir = WindToVector(windDir);
_windVelocity = dir * windSpeed;
// 半径膨胀:初始半径 + 扩散速率 × 时间
// 加入大气稳定度因子简化1.0
var dispersionRate = (float)(_ammo.DispersionRateBase * 1.0f);
_currentRadius = (float)_ammo.InitialRadius + dispersionRate * _elapsed;
// 中心随风漂移
_center += _windVelocity * deltaTime;
// 密度衰减:核心密度 × (初始半径 / 当前半径)³
if (_currentRadius > 0.001f)
{
var volumeRatio = (float)System.Math.Pow((float)_ammo.InitialRadius / _currentRadius, 3);
CoreDensity = (float)_ammo.CoreDensity * volumeRatio;
}
// 更新粒子参数
Particles.Opacity = System.Math.Max(0.1f, CoreDensity / (float)_ammo.CoreDensity);
Particles.SizeMultiplier = _currentRadius / (float)_ammo.InitialRadius;
// 消散条件
if (_elapsed >= (float)_ammo.MaxDuration || _currentRadius >= (float)_ammo.MaxRadius)
{
IsDissipated = true;
Particles.Opacity = 0f;
}
}
private static Vector3 WindToVector(WindDirection dir)
{
var angle = dir switch
{
WindDirection.N => 0f,
WindDirection.NE => 45f,
WindDirection.E => 90f,
WindDirection.SE => 135f,
WindDirection.S => 180f,
WindDirection.SW => 225f,
WindDirection.W => 270f,
WindDirection.NW => 315f,
_ => 0f,
};
var rad = angle * (float)System.Math.PI / 180f;
return new Vector3((float)System.Math.Sin(rad), 0, (float)System.Math.Cos(rad));
}
}
}

View File

@ -0,0 +1,16 @@
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms
{
public interface ICloudDispersionModel
{
void Initialize(AmmunitionSpec ammo, CombatScene env, Vector3 releasePos, float releaseTime);
void Tick(float deltaTime, float windSpeed, WindDirection windDir);
Vector3 Center { get; }
float Radius { get; }
float CoreDensity { get; }
float EffectiveRadius { get; }
ParticleParams Particles { get; }
bool IsDissipated { get; }
}
}

View File

@ -0,0 +1,12 @@
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms
{
public interface IDamageModel
{
float CalculateDamage(TargetType droneType, PowerType powerType,
AerosolType aerosolType, float cloudDensity, float exposureTime, float deltaTime);
DamageStage GetDamageStage(float accumulatedDamage);
}
}

View File

@ -0,0 +1,9 @@
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms
{
public interface IDefenseAdvisor
{
DefenseRecommendation Recommend(ThreatProfile threat);
}
}

View File

@ -0,0 +1,31 @@
using System;
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms
{
/// <summary>吸入式灭火 — 阈值型:密度达标后线性累积</summary>
public class InertGasDamageModel : IDamageModel
{
private const float EffectiveThreshold = 0.05f; // 有效浓度阈值
private const float DamageRate = 0.15f; // 每秒毁伤率
public float CalculateDamage(TargetType droneType, PowerType powerType,
AerosolType aerosolType, float cloudDensity, float exposureTime, float deltaTime)
{
if (aerosolType != AerosolType.InertGas) return 0f;
if (cloudDensity < EffectiveThreshold) return 0f;
// 活塞发动机对惰性气体最敏感
var sensitivity = powerType == PowerType.Piston ? 1.5f : 1.0f;
return DamageRate * sensitivity * deltaTime;
}
public DamageStage GetDamageStage(float accumulatedDamage)
{
if (accumulatedDamage >= 1.0f) return DamageStage.Destroyed;
if (accumulatedDamage >= 0.6f) return DamageStage.AttitudeLoss;
if (accumulatedDamage >= 0.25f) return DamageStage.EngineAnomaly;
return DamageStage.Normal;
}
}
}

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using CounterDrone.Core.Models;
using CounterDrone.Core.Repository;
@ -210,12 +211,13 @@ namespace CounterDrone.Core.Services
}
}
private static int _counter;
public static string GenerateTaskNumber()
{
var now = DateTime.Now;
var datePart = now.ToString("yyyyMMdd");
var random = new Random();
var seq = random.Next(1, 1000);
var seq = Interlocked.Increment(ref _counter);
return $"SIM-{datePart}-{seq:D3}";
}
}

View File

@ -0,0 +1,49 @@
using CounterDrone.Core.Algorithms;
using Xunit;
namespace CounterDrone.Core.Tests
{
public class AlgorithmFactoryTests
{
[Fact]
public void Create_ReturnsDefaultImplementations()
{
var dispersion = AlgorithmFactory.Create<ICloudDispersionModel>();
Assert.IsType<GaussianPuffDispersion>(dispersion);
var damage = AlgorithmFactory.Create<IDamageModel>();
Assert.IsType<DamageModelRouter>(damage);
var advisor = AlgorithmFactory.Create<IDefenseAdvisor>();
Assert.IsType<DefaultDefenseAdvisor>(advisor);
}
[Fact]
public void Register_ReplacesImplementation()
{
// 替换为自定义实现
AlgorithmFactory.Register<ICloudDispersionModel, MockDispersion>();
var instance = AlgorithmFactory.Create<ICloudDispersionModel>();
Assert.IsType<MockDispersion>(instance);
// 还原
AlgorithmFactory.Register<ICloudDispersionModel, GaussianPuffDispersion>();
}
}
public class MockDispersion : ICloudDispersionModel
{
public Vector3 Center => new();
public float Radius => 100f;
public float CoreDensity => 1f;
public float EffectiveRadius => 100f;
public ParticleParams Particles => new();
public bool IsDissipated => false;
public void Initialize(CounterDrone.Core.Models.AmmunitionSpec ammo,
CounterDrone.Core.Models.CombatScene env, Vector3 releasePos, float releaseTime) { }
public void Tick(float deltaTime, float windSpeed,
CounterDrone.Core.Models.WindDirection windDir) { }
}
}

View File

@ -0,0 +1,129 @@
using CounterDrone.Core.Algorithms;
using CounterDrone.Core.Models;
using Xunit;
namespace CounterDrone.Core.Tests
{
public class DamageModelTests
{
[Fact]
public void InertGas_PistonEngine_TakesExtraDamage()
{
var model = new InertGasDamageModel();
// 活塞发动机暴露于惰性气体
var dmg = model.CalculateDamage(TargetType.Piston, PowerType.Piston,
AerosolType.InertGas, cloudDensity: 0.1f, exposureTime: 2f, deltaTime: 1f);
Assert.True(dmg > 0.1f); // 高于基础速率0.15 × 1.5 = 0.225
}
[Fact]
public void InertGas_BelowThreshold_NoDamage()
{
var model = new InertGasDamageModel();
var dmg = model.CalculateDamage(TargetType.Piston, PowerType.Piston,
AerosolType.InertGas, cloudDensity: 0.01f, exposureTime: 2f, deltaTime: 1f);
Assert.Equal(0f, dmg);
}
[Fact]
public void InertGas_WrongAerosolType_NoDamage()
{
var model = new InertGasDamageModel();
var dmg = model.CalculateDamage(TargetType.Piston, PowerType.Piston,
AerosolType.ActiveMaterial, cloudDensity: 0.1f, exposureTime: 2f, deltaTime: 1f);
Assert.Equal(0f, dmg);
}
[Fact]
public void ActiveMaterial_TriggersBurst()
{
var model = new ActiveMaterialDamageModel();
var dmg1 = model.CalculateDamage(TargetType.FixedWing, PowerType.Jet,
AerosolType.ActiveMaterial, cloudDensity: 0.2f, exposureTime: 1f, deltaTime: 1f);
Assert.True(dmg1 >= 0.5f); // 爆发伤害
var dmg2 = model.CalculateDamage(TargetType.FixedWing, PowerType.Jet,
AerosolType.ActiveMaterial, cloudDensity: 0.2f, exposureTime: 2f, deltaTime: 1f);
Assert.True(dmg2 < 0.1f); // 后续余伤很小
}
[Fact]
public void ActiveMaterial_JetEngine_ExtraSensitivity()
{
var model = new ActiveMaterialDamageModel();
var dmg = model.CalculateDamage(TargetType.FixedWing, PowerType.Jet,
AerosolType.ActiveMaterial, cloudDensity: 0.2f, exposureTime: 0f, deltaTime: 0.1f);
// 喷气发动机0.6 × 1.3 = 0.78
Assert.True(dmg > 0.7f);
}
[Fact]
public void ActiveFuel_ExponentialGrowth()
{
var model = new ActiveFuelDamageModel();
var dmg1 = model.CalculateDamage(TargetType.HighSpeed, PowerType.Jet,
AerosolType.ActiveFuel, cloudDensity: 0.1f, exposureTime: 1f, deltaTime: 1f);
var dmg2 = model.CalculateDamage(TargetType.HighSpeed, PowerType.Jet,
AerosolType.ActiveFuel, cloudDensity: 0.1f, exposureTime: 5f, deltaTime: 1f);
// 5 秒后的伤害应显著大于 1 秒后
Assert.True(dmg2 > dmg1 * 2f);
}
[Fact]
public void ActiveFuel_HighSpeedTarget_ExtraSensitive()
{
var model = new ActiveFuelDamageModel();
var dmg = model.CalculateDamage(TargetType.HighSpeed, PowerType.Jet,
AerosolType.ActiveFuel, cloudDensity: 0.1f, exposureTime: 1f, deltaTime: 1f);
Assert.True(dmg > 0.02f); // 基础速率 × 灵敏度
}
[Fact]
public void DamageStage_Progression()
{
var model = new InertGasDamageModel();
Assert.Equal(DamageStage.Normal, model.GetDamageStage(0.1f));
Assert.Equal(DamageStage.EngineAnomaly, model.GetDamageStage(0.3f));
Assert.Equal(DamageStage.AttitudeLoss, model.GetDamageStage(0.7f));
Assert.Equal(DamageStage.Destroyed, model.GetDamageStage(1.0f));
}
[Fact]
public void Router_RoutesToCorrectModel()
{
var router = new DamageModelRouter();
// 惰性气体
var dmg1 = router.CalculateDamage(TargetType.Piston, PowerType.Piston,
AerosolType.InertGas, 0.1f, 1f, 1f);
Assert.True(dmg1 > 0f);
// 活性材料
var dmg2 = router.CalculateDamage(TargetType.FixedWing, PowerType.Jet,
AerosolType.ActiveMaterial, 0.2f, 0f, 1f);
Assert.True(dmg2 > 0f);
// 活性燃料
var dmg3 = router.CalculateDamage(TargetType.HighSpeed, PowerType.Jet,
AerosolType.ActiveFuel, 0.1f, 1f, 1f);
Assert.True(dmg3 > 0f);
}
}
}

View File

@ -0,0 +1,125 @@
using System.Collections.Generic;
using CounterDrone.Core.Algorithms;
using CounterDrone.Core.Models;
using Xunit;
namespace CounterDrone.Core.Tests
{
public class DefenseAdvisorTests
{
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 = 0, PosZ = 100, Altitude = 500, Speed = 120 },
new Waypoint { PosX = 10000, PosY = 0, PosZ = 100, Altitude = 500, Speed = 120 },
},
};
}
[Fact]
public void Recommend_PistonEngine_RecommendsInertGas()
{
var advisor = new DefaultDefenseAdvisor();
var result = advisor.Recommend(CreateSimpleThreat(PowerType.Piston));
Assert.Equal(AerosolType.InertGas, result.Best.RecommendedAerosolType);
Assert.Contains("惰性气体", result.Best.AerosolRationale);
Assert.True(result.Best.InterceptProbability > 0f);
}
[Fact]
public void Recommend_JetEngine_RecommendsActiveMaterial()
{
var advisor = new DefaultDefenseAdvisor();
var result = advisor.Recommend(CreateSimpleThreat(PowerType.Jet));
Assert.Equal(AerosolType.ActiveMaterial, result.Best.RecommendedAerosolType);
Assert.Contains("活性材料", result.Best.AerosolRationale);
}
[Fact]
public void Recommend_ElectricEngine_RecommendsInertGas()
{
var advisor = new DefaultDefenseAdvisor();
var result = advisor.Recommend(CreateSimpleThreat(PowerType.Electric));
Assert.Equal(AerosolType.InertGas, result.Best.RecommendedAerosolType);
Assert.Contains("惰性气体", result.Best.AerosolRationale);
}
[Fact]
public void Recommend_ProducesBestAndCritical()
{
var advisor = new DefaultDefenseAdvisor();
var result = advisor.Recommend(CreateSimpleThreat());
Assert.NotNull(result.Best);
Assert.NotNull(result.Critical);
Assert.True(result.Best.InterceptProbability >= result.Critical.InterceptProbability,
"最佳值拦截概率应 ≥ 最危险值");
}
[Fact]
public void Recommend_CloudPosition_OnRoute()
{
var advisor = new DefaultDefenseAdvisor();
var result = advisor.Recommend(CreateSimpleThreat());
// 最佳位置应在航路中点附近 (5000, 0, 100)
var pos = result.Best.RecommendedCloud;
Assert.True(pos.PositionX > 1000 && pos.PositionX < 9000,
$"PositionX={pos.PositionX} 应在航路范围内");
}
[Fact]
public void Recommend_ReturnsPlatforms()
{
var advisor = new DefaultDefenseAdvisor();
var result = advisor.Recommend(CreateSimpleThreat());
Assert.NotEmpty(result.Best.Platforms);
Assert.Equal(PlatformType.GroundBased, result.Best.Platforms[0].Type);
}
[Fact]
public void Recommend_ReturnsDetection()
{
var advisor = new DefaultDefenseAdvisor();
var result = advisor.Recommend(CreateSimpleThreat());
Assert.NotEmpty(result.Best.Detections);
Assert.True(result.Best.Detections[0].DetectionRadius > 0);
}
[Fact]
public void Recommend_MarksSourceAsAlgorithm()
{
var advisor = new DefaultDefenseAdvisor();
var result = advisor.Recommend(CreateSimpleThreat());
Assert.Equal("Algorithm", result.Best.RecommendedCloud.Source);
Assert.Equal((int)PositionMode.AlgorithmRecommended,
result.Best.RecommendedCloud.PositionMode);
}
}
}

View File

@ -0,0 +1,129 @@
using System;
using System.IO;
using CounterDrone.Core.Algorithms;
using CounterDrone.Core.Models;
using Xunit;
namespace CounterDrone.Core.Tests
{
public class DispersionModelTests
{
private AmmunitionSpec CreateTestAmmo()
{
return new AmmunitionSpec
{
InitialRadius = 50.0,
InitialVolume = 500000.0,
CoreDensity = 1.2,
EdgeDensity = 0.1,
DispersionRateBase = 5.0,
MaxRadius = 200.0,
MaxDuration = 60.0,
EffectiveConcentration = 0.05,
};
}
private CombatScene CreateTestEnv(float windSpeed = 5.0f, WindDirection dir = WindDirection.E)
{
return new CombatScene
{
WindSpeed = windSpeed,
WindDirection = (int)dir,
Temperature = 20,
Humidity = 60,
Pressure = 1013,
};
}
[Fact]
public void Initialize_SetsProperties()
{
var model = new GaussianPuffDispersion();
var ammo = CreateTestAmmo();
var env = CreateTestEnv();
model.Initialize(ammo, env, new Vector3(1000, 0, 300), 0f);
Assert.Equal(50f, model.Radius);
Assert.Equal(1.2f, model.CoreDensity);
Assert.False(model.IsDissipated);
}
[Fact]
public void Tick_ExpandsRadius()
{
var model = new GaussianPuffDispersion();
model.Initialize(CreateTestAmmo(), CreateTestEnv(), new Vector3(0, 0, 0), 0f);
// 5 秒后
model.Tick(5f, 5f, WindDirection.E);
// 初始 50 + 扩散速率 5 × 5s = 75
Assert.True(model.Radius > 55f && model.Radius < 80f);
}
[Fact]
public void Tick_WindDrift_MovesCenter()
{
var model = new GaussianPuffDispersion();
model.Initialize(CreateTestAmmo(), CreateTestEnv(10f, WindDirection.E), new Vector3(0, 0, 0), 0f);
model.Tick(10f, 10f, WindDirection.E);
// 东风 10 m/s × 10s = 100m东是 +X
Assert.True(model.Center.X > 80f);
Assert.True(model.Center.Z < 20f); // 纯东向Z 不应有大偏移
}
[Fact]
public void Tick_DensityDecays()
{
var model = new GaussianPuffDispersion();
model.Initialize(CreateTestAmmo(), CreateTestEnv(0f, WindDirection.N), new Vector3(0, 0, 0), 0f);
model.Tick(20f, 0f, WindDirection.N);
// 半径膨胀后密度应下降
Assert.True(model.CoreDensity < 1.2f);
}
[Fact]
public void Tick_DissipatesAfterMaxDuration()
{
var model = new GaussianPuffDispersion();
var ammo = CreateTestAmmo();
ammo.MaxDuration = 5.0; // 5秒消散
model.Initialize(ammo, CreateTestEnv(), new Vector3(0, 0, 0), 0f);
model.Tick(6f, 0f, WindDirection.N);
Assert.True(model.IsDissipated);
}
[Fact]
public void Tick_DissipatesAtMaxRadius()
{
var model = new GaussianPuffDispersion();
var ammo = CreateTestAmmo();
ammo.MaxRadius = 60.0;
ammo.DispersionRateBase = 100.0; // 快速膨胀
model.Initialize(ammo, CreateTestEnv(), new Vector3(0, 0, 0), 0f);
model.Tick(1f, 0f, WindDirection.N);
Assert.True(model.IsDissipated);
}
[Fact]
public void ParticleOpacity_DecaysWithDensity()
{
var model = new GaussianPuffDispersion();
model.Initialize(CreateTestAmmo(), CreateTestEnv(0f, WindDirection.N), new Vector3(0, 0, 0), 0f);
Assert.Equal(1.0f, model.Particles.Opacity, 0.01f);
model.Tick(30f, 0f, WindDirection.N);
Assert.True(model.Particles.Opacity < 1.0f);
}
}
}