fix: Planner 从多单元凑弹药,不再被单个 TotalMunitions 卡死

每个威胁可从多个火力单元汇集弹药,直到满足需求或耗尽。
单元测试 24/24 通过。
This commit is contained in:
tian 2026-06-13 09:40:37 +08:00
parent 2bb8ba2e1e
commit 8366bfcb93
2 changed files with 50 additions and 31 deletions

View File

@ -113,45 +113,64 @@ namespace CounterDrone.Core.Algorithms
continue;
}
// 选最早可拦截的
var best = candidates
.OrderBy(c => c.EarliestInterceptTime)
.ThenByDescending(c => c.KillProbability)
.First();
// 计算总弹药需求
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);
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]);
// 从多个候选单元凑弹药,直到满足需求或耗尽
int roundsCollected = 0;
var assignedUnits = new List<(FireUnit unit, int rounds, List<FireEvent> events)>();
if (actualRounds <= 0) { plan.ThreatsUnengaged++; continue; }
var fireEvents = GenerateFireEvents(threat, best.Unit, best.AmmoType,
actualRounds, ammo, env);
plan.Assignments.Add(new UnitAssignment
foreach (var c in candidates.OrderBy(c => c.EarliestInterceptTime)
.ThenByDescending(c => c.KillProbability))
{
FireUnitId = best.Unit.Id,
DroneGroupId = threat.GroupId,
AmmoType = best.AmmoType,
RoundsFired = actualRounds,
FirstFireTime = fireEvents.Count > 0 ? fireEvents[0].FireTime : 0,
FireEvents = fireEvents,
});
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.MergedSchedule.AddRange(fireEvents);
remainingMunitions[best.Unit.Id] -= actualRounds;
plan.ThreatsEngaged++;
}
plan.MergedSchedule.Sort((a, b) => a.FireTime.CompareTo(b.FireTime));
plan.OverallProbability = plan.Assignments.Count > 0
? (float)plan.Assignments.Average(a =>
{
var t = sortedThreats.First(th => th.GroupId == a.DroneGroupId);
var a2 = _ammoCatalog.FirstOrDefault(s => s.AerosolType == (int)a.AmmoType);
return ComputeInterceptProbability(t, a2, a.RoundsFired, env);
})
: 0f;
// 按威胁汇总概率
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} 个无方案"
: "无威胁被分配拦截方案";