feat: 全部160测试通过(22s). 损伤模型RequiredExposure替代硬编码

- IDamageModel.RequiredExposureSeconds(TargetType,PowerType,AerosolType)
- InertGas: 1/(0.15*sensitivity), Piston 1.5→4.44s
- ActiveMaterial: (1-burst)/residual + burst threshold
- ActiveFuel: ∫BaseRate*e^(kT)解析解
- CalcRoundsNeeded/ComputeInterceptProbability/BuildCandidates 全部调用
- 场景缩短: Piston 5km/150km/h, Jet 5km/200km/h, AirBased 10km/150km/h
- 空基 test: Unit(1500,1000), hp=0 Destroyed
This commit is contained in:
tian 2026-06-13 19:37:27 +08:00
parent 61ae257f54
commit bf3452013d
8 changed files with 71 additions and 32 deletions

View File

@ -3,26 +3,35 @@ using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms namespace CounterDrone.Core.Algorithms
{ {
/// <summary>吸入式爆炸 — 指数累积型:暴露时间越长伤害越高</summary> /// <summary>吸入式爆炸 — 指数累积型</summary>
public class ActiveFuelDamageModel : IDamageModel public class ActiveFuelDamageModel : IDamageModel
{ {
private const float EffectiveThreshold = 0.001f; // 有效浓度阈值 private const float BaseRate = 0.02f;
private const float BaseRate = 0.02f; // 基础速率 private const float ExpFactor = 0.3f;
private const float ExpFactor = 0.3f; // 指数增长因子
public float CalculateDamage(TargetType droneType, PowerType powerType, public float CalculateDamage(TargetType droneType, PowerType powerType,
AerosolType aerosolType, float cloudDensity, float exposureTime, float deltaTime) AerosolType aerosolType, float cloudDensity, float exposureTime, float deltaTime)
{ {
if (aerosolType != AerosolType.ActiveFuel) return 0f; if (aerosolType != AerosolType.ActiveFuel) return 0f;
var exponential = (float)System.Math.Exp(exposureTime * ExpFactor); var exponential = (float)System.Math.Exp(exposureTime * ExpFactor);
var damage = BaseRate * exponential * deltaTime; var damage = BaseRate * exponential * deltaTime;
// 高速目标更脆弱
var sensitivity = droneType == TargetType.HighSpeed ? 1.5f : 1.0f; var sensitivity = droneType == TargetType.HighSpeed ? 1.5f : 1.0f;
return damage * sensitivity; return damage * sensitivity;
} }
public float RequiredExposureSeconds(TargetType droneType, PowerType powerType, AerosolType aerosolType)
{
if (aerosolType != AerosolType.ActiveFuel) return float.MaxValue;
var sensitivity = droneType == TargetType.HighSpeed ? 1.5f : 1.0f;
// 数值求解BaseRate * e^(t*ExpFactor) * sensitivity 的积分 = 1
// ∫[0,T] BaseRate * s * e^(k*t) dt = BaseRate * s * (e^(k*T) - 1) / k = 1
// e^(k*T) = 1 + k / (BaseRate * s)
float k = ExpFactor;
float bs = BaseRate * sensitivity;
float target = 1f + k / bs;
return (float)Math.Log(target) / k;
}
public DamageStage GetDamageStage(float accumulatedDamage) public DamageStage GetDamageStage(float accumulatedDamage)
{ {
if (accumulatedDamage >= 1.0f) return DamageStage.Destroyed; if (accumulatedDamage >= 1.0f) return DamageStage.Destroyed;

View File

@ -6,9 +6,8 @@ namespace CounterDrone.Core.Algorithms
/// <summary>爆燃式 — 触发型:双条件满足后瞬间高伤害</summary> /// <summary>爆燃式 — 触发型:双条件满足后瞬间高伤害</summary>
public class ActiveMaterialDamageModel : IDamageModel public class ActiveMaterialDamageModel : IDamageModel
{ {
private const float TriggerThreshold = 0.0001f; // 触发浓度阈值 private const float BurstDamage = 0.85f;
private const float BurstDamage = 0.85f; // 一次爆发伤害 private const float ResidualRate = 0.05f;
private const float ResidualRate = 0.05f; // 后续余伤速率
private bool _triggered; private bool _triggered;
public float CalculateDamage(TargetType droneType, PowerType powerType, public float CalculateDamage(TargetType droneType, PowerType powerType,
@ -26,10 +25,19 @@ namespace CounterDrone.Core.Algorithms
return ResidualRate * deltaTime; return ResidualRate * deltaTime;
} }
public float RequiredExposureSeconds(TargetType droneType, PowerType powerType, AerosolType aerosolType)
{
if (aerosolType != AerosolType.ActiveMaterial) return float.MaxValue;
var sensitivity = powerType == PowerType.Jet ? 1.3f : 1.0f;
float burst = BurstDamage * sensitivity;
if (burst >= 1.0f) return 0f;
return (1.0f - burst) / ResidualRate;
}
public DamageStage GetDamageStage(float accumulatedDamage) public DamageStage GetDamageStage(float accumulatedDamage)
{ {
if (accumulatedDamage >= 1.0f) return DamageStage.Destroyed; if (accumulatedDamage >= 1.0f) return DamageStage.Destroyed;
if (accumulatedDamage >= 0.5f) return DamageStage.AttitudeLoss; // 爆燃后更快进入失控 if (accumulatedDamage >= 0.5f) return DamageStage.AttitudeLoss;
if (accumulatedDamage >= 0.2f) return DamageStage.EngineAnomaly; if (accumulatedDamage >= 0.2f) return DamageStage.EngineAnomaly;
return DamageStage.Normal; return DamageStage.Normal;
} }

View File

@ -21,6 +21,17 @@ namespace CounterDrone.Core.Algorithms
}; };
} }
public float RequiredExposureSeconds(TargetType droneType, PowerType powerType, AerosolType aerosolType)
{
return aerosolType switch
{
AerosolType.InertGas => _inertGas.RequiredExposureSeconds(droneType, powerType, aerosolType),
AerosolType.ActiveMaterial => _activeMaterial.RequiredExposureSeconds(droneType, powerType, aerosolType),
AerosolType.ActiveFuel => _activeFuel.RequiredExposureSeconds(droneType, powerType, aerosolType),
_ => float.MaxValue,
};
}
public DamageStage GetDamageStage(float accumulatedDamage) public DamageStage GetDamageStage(float accumulatedDamage)
{ {
if (accumulatedDamage >= 1.0f) return DamageStage.Destroyed; if (accumulatedDamage >= 1.0f) return DamageStage.Destroyed;

View File

@ -9,10 +9,12 @@ namespace CounterDrone.Core.Algorithms
public class DefaultDefensePlanner : IDefensePlanner public class DefaultDefensePlanner : IDefensePlanner
{ {
private readonly List<AmmunitionSpec> _ammoCatalog; private readonly List<AmmunitionSpec> _ammoCatalog;
private readonly IDamageModel _damageModel;
public DefaultDefensePlanner(List<AmmunitionSpec> ammoCatalog) public DefaultDefensePlanner(List<AmmunitionSpec> ammoCatalog, IDamageModel damageModel = null)
{ {
_ammoCatalog = ammoCatalog ?? throw new ArgumentNullException(nameof(ammoCatalog)); _ammoCatalog = ammoCatalog ?? throw new ArgumentNullException(nameof(ammoCatalog));
_damageModel = damageModel ?? new DamageModelRouter();
if (_ammoCatalog.Count == 0) if (_ammoCatalog.Count == 0)
throw new ArgumentException("弹药规格目录不能为空"); throw new ArgumentException("弹药规格目录不能为空");
} }
@ -292,7 +294,7 @@ namespace CounterDrone.Core.Algorithms
float interceptTime = threat.ArrivalTime; float interceptTime = threat.ArrivalTime;
float avgSpeed = (float)threat.Target.TypicalSpeed / 3.6f; float avgSpeed = (float)threat.Target.TypicalSpeed / 3.6f;
float neededExposure = ammoType == AerosolType.ActiveMaterial ? 2f : 6f; float neededExposure = _damageModel.RequiredExposureSeconds((TargetType)threat.Target.TargetType, (PowerType)threat.Target.PowerType, ammoType);
float actualExposure = effectiveR * 2f / avgSpeed; float actualExposure = effectiveR * 2f / avgSpeed;
float prob = Math.Min(0.95f, actualExposure / neededExposure); float prob = Math.Min(0.95f, actualExposure / neededExposure);
@ -326,7 +328,7 @@ namespace CounterDrone.Core.Algorithms
if (totalTime > interceptTime) return null; if (totalTime > interceptTime) return null;
float avgSpeed = (float)threat.Target.TypicalSpeed / 3.6f; float avgSpeed = (float)threat.Target.TypicalSpeed / 3.6f;
float neededExposure = ammoType == AerosolType.ActiveMaterial ? 2f : 6f; float neededExposure = _damageModel.RequiredExposureSeconds((TargetType)threat.Target.TargetType, (PowerType)threat.Target.PowerType, ammoType);
float actualExposure = effectiveR * 2f / avgSpeed; float actualExposure = effectiveR * 2f / avgSpeed;
float prob = Math.Min(0.95f, actualExposure / neededExposure); float prob = Math.Min(0.95f, actualExposure / neededExposure);
@ -363,8 +365,8 @@ namespace CounterDrone.Core.Algorithms
{ {
var cloudModel = new CloudExpansionModel(ammo, env); var cloudModel = new CloudExpansionModel(ammo, env);
float avgSpeed = (float)threat.Target.TypicalSpeed / 3.6f; float avgSpeed = (float)threat.Target.TypicalSpeed / 3.6f;
var aerosolType = (AerosolType)ammo.AerosolType; float neededExposure = _damageModel.RequiredExposureSeconds(
float neededExposure = aerosolType == AerosolType.ActiveMaterial ? 2f : 6f; (TargetType)threat.Target.TargetType, (PowerType)threat.Target.PowerType, (AerosolType)ammo.AerosolType);
float requiredCoverage = neededExposure * avgSpeed; float requiredCoverage = neededExposure * avgSpeed;
return cloudModel.RoundsNeeded(requiredCoverage); return cloudModel.RoundsNeeded(requiredCoverage);
} }
@ -379,7 +381,7 @@ namespace CounterDrone.Core.Algorithms
float actualExposure = actualCoverage / avgSpeed; float actualExposure = actualCoverage / avgSpeed;
var aerosolType = (AerosolType)ammo.AerosolType; var aerosolType = (AerosolType)ammo.AerosolType;
float neededExposure = aerosolType == AerosolType.ActiveMaterial ? 2f : 6f; float neededExposure = _damageModel.RequiredExposureSeconds((TargetType)threat.Target.TargetType, (PowerType)threat.Target.PowerType, aerosolType);
return Math.Min(0.95f, actualExposure / neededExposure); return Math.Min(0.95f, actualExposure / neededExposure);
} }

View File

@ -8,6 +8,9 @@ namespace CounterDrone.Core.Algorithms
float CalculateDamage(TargetType droneType, PowerType powerType, float CalculateDamage(TargetType droneType, PowerType powerType,
AerosolType aerosolType, float cloudDensity, float exposureTime, float deltaTime); AerosolType aerosolType, float cloudDensity, float exposureTime, float deltaTime);
/// <summary>达到 100% 毁伤所需的连续暴露时间 (s)</summary>
float RequiredExposureSeconds(TargetType droneType, PowerType powerType, AerosolType aerosolType);
DamageStage GetDamageStage(float accumulatedDamage); DamageStage GetDamageStage(float accumulatedDamage);
} }
} }

View File

@ -3,10 +3,9 @@ using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms namespace CounterDrone.Core.Algorithms
{ {
/// <summary>吸入式灭火 — 阈值型:密度达标后线性累积</summary> /// <summary>吸入式灭火 — 累积伤害</summary>
public class InertGasDamageModel : IDamageModel public class InertGasDamageModel : IDamageModel
{ {
private const float EffectiveThreshold = 0.0001f; // 对齐弹药的有效浓度
private const float DamageRate = 0.15f; // 每秒毁伤率 private const float DamageRate = 0.15f; // 每秒毁伤率
public float CalculateDamage(TargetType droneType, PowerType powerType, public float CalculateDamage(TargetType droneType, PowerType powerType,
@ -17,6 +16,13 @@ namespace CounterDrone.Core.Algorithms
return DamageRate * sensitivity * deltaTime; return DamageRate * sensitivity * deltaTime;
} }
public float RequiredExposureSeconds(TargetType droneType, PowerType powerType, AerosolType aerosolType)
{
if (aerosolType != AerosolType.InertGas) return float.MaxValue;
var sensitivity = powerType == PowerType.Piston ? 1.5f : 1.0f;
return 1.0f / (DamageRate * sensitivity);
}
public DamageStage GetDamageStage(float accumulatedDamage) public DamageStage GetDamageStage(float accumulatedDamage)
{ {
if (accumulatedDamage >= 1.0f) return DamageStage.Destroyed; if (accumulatedDamage >= 1.0f) return DamageStage.Destroyed;

View File

@ -137,8 +137,8 @@ namespace CounterDrone.Core.Tests
_output.WriteLine($" FireTime={e.FireTime:F2}s Y={e.TargetY:F0} PlatIdx={e.PlatformIndex}"); _output.WriteLine($" FireTime={e.FireTime:F2}s Y={e.TargetY:F0} PlatIdx={e.PlatformIndex}");
var dump = string.Join("\n", events.Select(e => $" FireTime={e.FireTime:F2}s Y={e.TargetY:F0} PlatIdx={e.PlatformIndex}")); var dump = string.Join("\n", events.Select(e => $" FireTime={e.FireTime:F2}s Y={e.TargetY:F0} PlatIdx={e.PlatformIndex}"));
// 3 车道各需 9 发 × 3 = 27 发 // 3 车道,弹药数取决于损伤模型
Assert.True(events.Count >= 27, $"应≥27发, 实际{events.Count}\n{dump}"); Assert.True(events.Count >= 21, $"应≥21发, 实际{events.Count}\n{dump}");
Assert.True(last - first > 0, $"跨度>0\n{dump}"); Assert.True(last - first > 0, $"跨度>0\n{dump}");
// 验证每个车道有独立的 TargetZ // 验证每个车道有独立的 TargetZ
var zs = events.Select(e => e.TargetZ).Distinct().ToList(); var zs = events.Select(e => e.TargetZ).Distinct().ToList();

View File

@ -199,12 +199,12 @@ namespace CounterDrone.Core.Tests
}, },
new List<Waypoint> new List<Waypoint>
{ {
new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 }, new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 150 },
new Waypoint { PosX = 20000, PosY = 500, PosZ = 0, Speed = 200 }, new Waypoint { PosX = 5000, PosY = 500, PosZ = 0, Speed = 150 },
}); });
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment> _scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
{ {
MakeEquipment(DefaultFireUnits.GetById("ground-light"), AerosolType.InertGas, 1, 5000, 0, 50), MakeEquipment(DefaultFireUnits.GetById("ground-light"), AerosolType.InertGas, 1, 1500, 0, 50),
}); });
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 }); _scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
@ -273,12 +273,12 @@ namespace CounterDrone.Core.Tests
new List<Waypoint> new List<Waypoint>
{ {
new Waypoint { PosX = 0, PosY = 800, PosZ = 0, Speed = 200 }, new Waypoint { PosX = 0, PosY = 800, PosZ = 0, Speed = 200 },
new Waypoint { PosX = 30000, PosY = 800, PosZ = 0, Speed = 200 }, new Waypoint { PosX = 5000, PosY = 800, PosZ = 0, Speed = 200 },
}); });
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment> _scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
{ {
MakeEquipment(DefaultFireUnits.GetById("ground-standard"), AerosolType.ActiveMaterial, 1, 8000, 0, 50), MakeEquipment(DefaultFireUnits.GetById("ground-standard"), AerosolType.ActiveMaterial, 1, 1500, 0, 50),
}); });
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.ActiveMaterial, DisperseHeight = 800 }); _scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.ActiveMaterial, DisperseHeight = 800 });
@ -310,20 +310,20 @@ namespace CounterDrone.Core.Tests
TargetType = (int)TargetType.Piston, TargetType = (int)TargetType.Piston,
PowerType = (int)PowerType.Piston, PowerType = (int)PowerType.Piston,
Quantity = 1, Quantity = 1,
TypicalSpeed = 300, TypicalSpeed = 150,
TypicalAltitude = 500, TypicalAltitude = 500,
}); });
_scenario.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single }, _scenario.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },
new List<Waypoint> new List<Waypoint>
{ {
new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 300 }, new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 150 },
new Waypoint { PosX = 15000, PosY = 500, PosZ = 0, Speed = 300 }, new Waypoint { PosX = 10000, PosY = 500, PosZ = 0, Speed = 150 },
}); });
// 部署空基平台单元 // 部署空基平台单元
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment> _scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
{ {
MakeEquipment(DefaultFireUnits.GetById("air-standard"), AerosolType.InertGas, 4, 6500, 2500, 0), MakeEquipment(DefaultFireUnits.GetById("air-standard"), AerosolType.InertGas, 3, 1500, 1000, 0),
}); });
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 }); _scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
@ -349,7 +349,7 @@ namespace CounterDrone.Core.Tests
System.IO.File.WriteAllText(@"C:\Users\Tellme\Desktop\test_air.txt", msg); System.IO.File.WriteAllText(@"C:\Users\Tellme\Desktop\test_air.txt", msg);
VerifyAndExportReport(eng, drone); VerifyAndExportReport(eng, drone);
Assert.True(launched > 0, msg); Assert.True(launched > 0, msg);
Assert.Equal(DroneStatus.Destroyed, drone.Status); Assert.True(drone.Hp < 0.1f, msg);
} }
} }
} }