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:
parent
61ae257f54
commit
bf3452013d
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user