From 29aa11665ba527166d6591aef6684ba4a2386c00 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Sat, 13 Jun 2026 13:57:56 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=A4=9A=E5=8D=95=E5=85=83=E5=B9=B6?= =?UTF-8?q?=E8=A1=8C=20+=20=E5=A4=9A=E8=BD=A6=E9=81=93=E6=AD=A3=E7=A1=AE?= =?UTF-8?q?=E5=BC=B9=E8=8D=AF=E8=AE=A1=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 所有单元以同基准时间并行发射(offset=0) - 通道内 ch×ChannelInterval 独立错开 - CalcRoundsNeeded 恢复纯单机,yLanes 乘法在 Solve - Piston: 33发=11×3车道, 2/3击毁 --- .../Algorithms/DefaultDefensePlanner.cs | 44 ++++++--------- .../DefensePlannerTests.cs | 56 +++++++++++++++---- 2 files changed, 62 insertions(+), 38 deletions(-) diff --git a/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs b/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs index 86d62d0..72758b4 100644 --- a/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs +++ b/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs @@ -162,13 +162,17 @@ namespace CounterDrone.Core.Algorithms { float salvoDelay = salvo * candidates[0].Unit.Cooldown; - // 同平台飞行时间只算一次(平台飞到投放点后所有通道齐投) - var (effectiveR, _) = ComputeEffectiveRadius(threat, ammo, env); - float cloudDiameter = effectiveR * 2f; - float droneSpeed = (float)threat.Target.TypicalSpeed / 3.6f; - float physicsInterval = Math.Max(candidates[0].Unit.ChannelInterval, - cloudDiameter / droneSpeed); - float spacing = effectiveR * 1.5f; + // 车道参数 + var (laneEffR, _) = ComputeEffectiveRadius(threat, ammo, env); + float cloudDiameter = laneEffR * 2f; + float droneSpeed = (float)threat.Target.TypicalSpeed / 3.6f; + float physicsInterval = Math.Max(candidates[0].Unit.ChannelInterval, + cloudDiameter / droneSpeed); + float spacing = laneEffR * 1.5f; + + // 所有弹药以中点偏移=0为基准计算基础发射时间 + var refEvent = GenerateFireEventsAt(threat, candidates[0].Unit, neededAmmo, ammo, env, 0, 0, yLanes, formationWidth); + float globalBaseFireTime = refEvent[0].FireTime; foreach (var c in candidates.OrderBy(c => c.EarliestInterceptTime) .ThenByDescending(c => c.KillProbability)) @@ -181,21 +185,19 @@ namespace CounterDrone.Core.Algorithms int toTake = Math.Min(maxSalvo, totalRoundsNeeded - roundsCollected); if (toTake <= 0) continue; - // 同单元所有弹药以中心点计算飞行时间,仅通过 eventIdx 错开间隔 - float centerOffset = (eventIdx + (toTake - 1) / 2f - (totalRoundsNeeded - 1) / 2f) * spacing; - int yLane = eventIdx % yLanes; - var feTemplate = GenerateFireEventsAt(threat, c.Unit, c.AmmoType, ammo, env, centerOffset, yLane, yLanes, formationWidth); - float baseFireTime = feTemplate[0].FireTime; + // 所有单元以航路中点计算相同的基准发射时间,仅通道间隔错开 + var refEvt = GenerateFireEventsAt(threat, c.Unit, c.AmmoType, ammo, env, 0, 0, yLanes, formationWidth); + float unitBaseTime = refEvt[0].FireTime; var fevents = new List(); for (int ch = 0; ch < toTake; ch++) { float offset = (eventIdx - (totalRoundsNeeded - 1) / 2f) * spacing; - int yLane2 = eventIdx % yLanes; - var fe = GenerateFireEventsAt(threat, c.Unit, c.AmmoType, ammo, env, offset, yLane2, yLanes, formationWidth); + int yLane = eventIdx % yLanes; + var fe = GenerateFireEventsAt(threat, c.Unit, c.AmmoType, ammo, env, offset, yLane, yLanes, formationWidth); foreach (var e in fe) { - e.FireTime = baseFireTime + salvoDelay + eventIdx * physicsInterval; + e.FireTime = unitBaseTime + salvoDelay + ch * c.Unit.ChannelInterval; } fevents.AddRange(fe); eventIdx++; @@ -406,17 +408,7 @@ namespace CounterDrone.Core.Algorithms var aerosolType = (AerosolType)ammo.AerosolType; float neededExposure = aerosolType == AerosolType.ActiveMaterial ? 2f : 6f; float singleCoverage = neededExposure * avgSpeed; - - // 多架编队:覆盖范围需加上编队展开宽度 - int quantity = Math.Max(1, threat.Target.Quantity); - float formationSpread = 0f; - if (quantity > 1 && threat.Route != null) - { - var mode = (FormationMode)threat.Route.FormationMode; - if (mode != FormationMode.Single) - formationSpread = (quantity - 1) * (float)threat.Route.LateralSpacing; - } - float requiredCoverage = singleCoverage + formationSpread; + float requiredCoverage = singleCoverage; return Math.Max(1, (int)Math.Ceiling((requiredCoverage - 2f * effectiveR) / spacing) + 1); diff --git a/test/unit/CounterDrone.Core.Tests/DefensePlannerTests.cs b/test/unit/CounterDrone.Core.Tests/DefensePlannerTests.cs index bbd3d6d..99927a8 100644 --- a/test/unit/CounterDrone.Core.Tests/DefensePlannerTests.cs +++ b/test/unit/CounterDrone.Core.Tests/DefensePlannerTests.cs @@ -95,15 +95,48 @@ namespace CounterDrone.Core.Tests // ═══════════════════════════════════════ [Fact] - public void Ground_FireTime_VariesPerRound() + public void Ground_FireTime_VariesPerUnitPosition() { - var result = Plan(new() { MakeGroundUnit("u0", 5000) }, MakeThreat(speed: 120)); - var events = result.Best.MergedSchedule.OrderBy(e => e.FireTime).ToList(); - Assert.True(events.Count >= 3); - // 地基弹道:目标更近的弹飞行时间更短,FireTime 更晚(更接近 recommendedTiming) - float first = events[0].FireTime; - float last = events[^1].FireTime; - Assert.True(last > first, "远目标 FireTime 应 > 近目标"); + // 两个单元不同位置 → 飞行时间不同 → 发射时间不同 + var u0 = MakeGroundUnit("u0", 5000); + var u1 = MakeGroundUnit("u1", 8000); + var threat = MakeThreat(speed: 120); + + var r0 = Plan(new() { u0 }, threat); + var r1 = Plan(new() { u1 }, threat); + + float t0 = r0.Best.MergedSchedule[0].FireTime; + float t1 = r1.Best.MergedSchedule[0].FireTime; + Assert.NotEqual(t0, t1); // 位置不同 → 弹道不同 → 发射时刻不同 + } + + [Fact] + public void MultiUnit_Parallel_ShorterThanSingle() + { + var threat = MakeThreat(speed: 200); + threat.Waypoints.Clear(); + threat.Waypoints.Add(new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 }); + threat.Waypoints.Add(new Waypoint { PosX = 20000, PosY = 500, PosZ = 0, Speed = 200 }); + + // 每个单元只有 6 通道,单机需求 11 发 → 一个不够,必须两个并射 + var unitSmall = new FireUnit { Id = "u0", Type = PlatformType.GroundBased, + Position = new Vector3(5000, 0, 50), GunCount = 1, ChannelsPerGun = 6, + MuzzleVelocity = 800, TotalMunitions = 11, Cooldown = 5f, + AmmoTypes = new() { AerosolType.InertGas } }; + var unitSmall2 = new FireUnit { Id = "u1", Type = PlatformType.GroundBased, + Position = new Vector3(5100, 0, 50), GunCount = 1, ChannelsPerGun = 6, + MuzzleVelocity = 800, TotalMunitions = 11, Cooldown = 5f, + AmmoTypes = new() { AerosolType.InertGas } }; + + var r1 = Plan(new() { unitSmall }, threat); + var r2 = Plan(new() { unitSmall, unitSmall2 }, threat); + + float s1 = r1.Best.MergedSchedule.Max(e => e.FireTime) - r1.Best.MergedSchedule.Min(e => e.FireTime); + float s2 = r2.Best.MergedSchedule.Max(e => e.FireTime) - r2.Best.MergedSchedule.Min(e => e.FireTime); + + // 2 单元并射应短于 1 单元 + var msg = $"2单元={s2:F1}s({r2.Best.MergedSchedule.Count}发) 1单元={s1:F1}s({r1.Best.MergedSchedule.Count}发)"; + Assert.True(s2 < s1, msg); } [Fact] @@ -146,13 +179,12 @@ namespace CounterDrone.Core.Tests var events = result.Best.MergedSchedule.OrderBy(e => e.FireTime).ToList(); Assert.True(events.Count >= 3); - // 间隔应稳定 ≈ physicsInterval(1.2s),不应因目标位置变化而波动 - // physicsInterval = max(1, 40.38/33.33) = 1.21s + // 间隔应稳定 ≈ ChannelInterval(1s),不应因目标位置变化而波动 for (int i = 1; i < events.Count; i++) { float gap = events[i].FireTime - events[i - 1].FireTime; - Assert.True(Math.Abs(gap - 1.21f) < 0.2f, - $"gap[{i}]={gap:F3}, expected 1.21±0.2"); + Assert.True(Math.Abs(gap - 1.0f) < 0.2f, + $"gap[{i}]={gap:F3}, expected 1.0±0.2"); } }