236 lines
18 KiB
C#
236 lines
18 KiB
C#
using BardFrontline.Core;
|
|
using System.Numerics;
|
|
using System.Text.Json;
|
|
|
|
static class OptimizationTests
|
|
{
|
|
static Actor Self(params uint[] states) => new(1, "self", 60000, 60000, 0, false, true, false, 0, [.. states]);
|
|
static Actor Enemy(ulong id, uint hp = 30000) => new(id, $"enemy{id}", hp, 60000, 20, true, true, false, 0, []);
|
|
static CombatSnapshot Snap(params Actor[] enemies) => new(Self(), 10000, 10000, true, false, 10, false, enemies, [], 0);
|
|
static ActionDecision? Decide(CombatSnapshot s, Settings c, params Skill[] ready) => new Rotation().Decide(s, c, new Fake(ready));
|
|
static void SkillIs(Skill skill, ActionDecision? d) => Suite.Equal(skill, d!.Skill);
|
|
|
|
public static void Run(Suite suite)
|
|
{
|
|
suite.Run("Optimization defaults preserve automatic HP player-first finisher and backstep preferences", () =>
|
|
{
|
|
var c = JsonSerializer.Deserialize<Settings>("{\"EagleFinisherHp\":9000,\"HelpParty\":true}")!; c.Normalize();
|
|
Suite.True(c.AutoAttack && c.AttackObjectives && !c.IgnoreEnemyGuard && c.HelpParty);
|
|
Suite.True(!c.StableTargets && !c.DetectManualControl && !c.ManualRepelling && !c.FocusDefense && !c.PreferAoe);
|
|
Suite.Equal(BurstStrategy.Normal, c.Burst); Suite.Equal(EagleStrategy.FinisherFirst, c.EagleStrategy);
|
|
Suite.Equal(TargetOrder.LowestHp, c.TargetOrder); Suite.Equal(9000, c.EagleFinisherHp);
|
|
Suite.Equal(0, c.DisabledObjectives.Count);
|
|
});
|
|
suite.Run("New strategy settings serialize and malformed values normalize", () =>
|
|
{
|
|
var c = new Settings { StableTargets = true, DetectManualControl = true, ManualRepelling = true,
|
|
FocusDefense = true, PreferAoe = true, Burst = BurstStrategy.ManualWindow, DisabledObjectives = [ObjectiveKind.Core] };
|
|
var roundTrip = JsonSerializer.Deserialize<Settings>(JsonSerializer.Serialize(c))!;
|
|
Suite.True(roundTrip.ManualRepelling && roundTrip.DisabledObjectives.Contains(ObjectiveKind.Core));
|
|
c.TargetHoldSeconds = float.NaN; c.ManualControlSeconds = float.PositiveInfinity;
|
|
c.FocusAttackers = -1; c.BurstCharges = 50; c.AoeMinTargets = 0; c.Burst = (BurstStrategy)99; c.Normalize();
|
|
Suite.Equal(.6f, c.TargetHoldSeconds); Suite.Equal(3f, c.ManualControlSeconds);
|
|
Suite.Equal(2, c.FocusAttackers); Suite.Equal(4, c.BurstCharges); Suite.Equal(2, c.AoeMinTargets);
|
|
Suite.Equal(BurstStrategy.Normal, c.Burst);
|
|
});
|
|
suite.Run("Stable targeting absorbs small HP jitter but default remains instant", () =>
|
|
{
|
|
var s = Snap(Enemy(10, 21000), Enemy(11, 20000)) with { CurrentTarget = 10, TargetAge = .2 };
|
|
Suite.Equal(11ul, Decide(s, new(), Skill.PowerfulShot)!.TargetId);
|
|
Suite.Equal(10ul, Decide(s, new() { StableTargets = true }, Skill.PowerfulShot)!.TargetId);
|
|
Suite.Equal(11ul, Decide(s with { TargetAge = .7 }, new() { StableTargets = true }, Skill.PowerfulShot)!.TargetId);
|
|
});
|
|
suite.Run("Stable targeting yields immediately to major HP advantage or invalid targets", () =>
|
|
{
|
|
var c = new Settings { StableTargets = true };
|
|
var s = Snap(Enemy(10, 30000), Enemy(11, 20000)) with { CurrentTarget = 10, TargetAge = .1 };
|
|
Suite.Equal(11ul, Decide(s, c, Skill.PowerfulShot)!.TargetId);
|
|
foreach (var old in new[] { Enemy(10, 19000) with { Dead = true }, Enemy(10, 19000) with { Status = [Statuses.Guard] },
|
|
Enemy(10, 19000) with { Targetable = false } })
|
|
Suite.Equal(11ul, Decide(s with { Enemies = [old, Enemy(11, 20000)] }, c, Skill.PowerfulShot)!.TargetId);
|
|
});
|
|
suite.Run("Stability cannot retain an objective over a player or delay an Eagle finisher", () =>
|
|
{
|
|
var c = new Settings { StableTargets = true };
|
|
var s = Snap(Enemy(10, 100) with { IsObjective = true }, Enemy(11, 9000)) with { CurrentTarget = 10, TargetAge = .1 };
|
|
Suite.Equal(11ul, Decide(s, c, Skill.PowerfulShot)!.TargetId);
|
|
SkillIs(Skill.EagleEyeShot, Decide(s with { Player = Self(Statuses.RoleEagle) }, c, Skill.EagleEyeShot, Skill.PowerfulShot));
|
|
});
|
|
suite.Run("Planned and dispatched stable targets agree", () =>
|
|
{
|
|
var s = Snap(Enemy(10, 21000), Enemy(11, 20000)) with { CurrentTarget = 10, TargetAge = .1 };
|
|
var c = new Settings { StableTargets = true }; var f = new Fake(Skill.PowerfulShot);
|
|
Suite.Equal(10ul, TargetSelector.PlanTarget(s, c, f, f)!.Id);
|
|
Suite.Equal(10ul, new Rotation().Decide(s, c, f)!.TargetId);
|
|
});
|
|
suite.Run("Plugin target writes and native requests never trigger automatic takeover", () =>
|
|
{
|
|
var control = new ManualControl(); var c = new Settings { DetectManualControl = true };
|
|
control.Observe(0, 10, 1, false, c); control.OwnTarget(11, .1); control.OwnAction(2);
|
|
control.Observe(.2, 11, 2, false, c); Suite.True(!control.Active(.2));
|
|
control.Observe(.3, 11, 3, true, c); control.Observe(.4, 11, 3, false, c); Suite.True(!control.Active(.4));
|
|
});
|
|
suite.Run("Manual target and foreign action takeover has bounded expiry", () =>
|
|
{
|
|
var control = new ManualControl(); var c = new Settings { DetectManualControl = true, ManualControlSeconds = 3 };
|
|
control.Observe(0, 10, 65535, false, c); control.Observe(1, 11, 65535, false, c);
|
|
Suite.True(control.Active(3.9)); Suite.True(!control.Active(4));
|
|
control.Observe(5, 11, 0, false, c); Suite.True(control.Active(5));
|
|
control.Resume(); Suite.True(!control.Active(5));
|
|
});
|
|
suite.Run("Forced takeover persists across context resets but explicit resume clears it", () =>
|
|
{
|
|
var control = new ManualControl { Forced = true }; control.ResetObservations();
|
|
Suite.True(control.Active(100)); control.Resume(); Suite.True(!control.Active(100));
|
|
control.Observe(0, 10, 1, false, new()); control.Observe(1, 11, 2, false, new()); Suite.True(!control.Active(1));
|
|
});
|
|
suite.Run("Manual control pauses offense survival and target planning", () =>
|
|
{
|
|
var s = Snap(Enemy(10, 100)) with { ManualControl = true, Player = Self(Statuses.RoleEagle) with { Hp = 1000 } };
|
|
var f = new Fake(Actions.All);
|
|
Suite.True(new Rotation().Decide(s, new(), f) == null);
|
|
Suite.True(TargetSelector.PlanTarget(s, new(), f, f) == null);
|
|
});
|
|
suite.Run("Adaptive weave starts at legacy budget and learns observed confirmation latency", () =>
|
|
{
|
|
var w = new WeaveTiming(); Suite.Equal(.65f, w.Budget);
|
|
for (var i = 0; i < 3; i++) { w.Submitted(i * 2, false); w.Observe(i * 2 + .1, false, .6f, false, false); w.Observe(i * 2 + .8, true, 0, false, false); }
|
|
Suite.Equal(3, w.SampleCount); Suite.True(w.Budget > .8f && w.Budget < .9f);
|
|
Suite.True(!w.Allows(.75f, Skill.SilentNocturne, true)); Suite.True(w.Allows(.75f, Skill.SilentNocturne, false));
|
|
Suite.True(w.Allows(.1f, Skill.EncoreOfLight, true)); Suite.True(w.Allows(0, Skill.HarmonicArrow, true));
|
|
});
|
|
suite.Run("Weave learning ignores casts timeouts and pending queues", () =>
|
|
{
|
|
var w = new WeaveTiming(); w.Submitted(0, true); w.Observe(1, true, 0, false, false); Suite.Equal(0, w.SampleCount);
|
|
w.Submitted(2, false); w.Observe(5, false, 0, false, false); Suite.Equal(0, w.SampleCount);
|
|
w.Submitted(6, false); w.Observe(6.6, true, 0, true, false); Suite.Equal(0, w.SampleCount);
|
|
w.Observe(6.8, true, 0, false, false); Suite.Equal(1, w.SampleCount); w.Reset(); Suite.Equal(.65f, w.Budget);
|
|
});
|
|
suite.Run("Adaptive sample window is bounded and budget responds to recent samples", () =>
|
|
{
|
|
var w = new WeaveTiming();
|
|
for (var i = 0; i < 30; i++) { w.Submitted(i * 2, false); w.Observe(i * 2 + (i < 5 ? 1.4 : .55), true, 0, false, false); }
|
|
Suite.Equal(20, w.SampleCount); Suite.True(w.Budget < .65f && w.Budget >= .5f);
|
|
});
|
|
suite.Run("Rival Wings names classify all objective families without localized strings", () =>
|
|
{
|
|
foreach (var (id, kind) in new[] { (6857u, ObjectiveKind.Core), (6860u, ObjectiveKind.Tower), (6864u, ObjectiveKind.Mech),
|
|
(6871u, ObjectiveKind.Infantry), (7891u, ObjectiveKind.Mercenary), (7906u, ObjectiveKind.Mercenary), (99999u, ObjectiveKind.Other) })
|
|
Suite.Equal(kind, Battlefield.Classify(BattlefieldMode.RivalWings, true, false, id));
|
|
Suite.Equal(ObjectiveKind.Mech, Battlefield.Classify(BattlefieldMode.RivalWings, false, true, 0));
|
|
Suite.Equal(ObjectiveKind.Other, Battlefield.Classify(BattlefieldMode.Frontline, true, false, 6857));
|
|
Suite.Equal(ObjectiveKind.Ice, Battlefield.Classify(BattlefieldMode.Frontline, true, false, 4822));
|
|
});
|
|
suite.Run("Objective family switches affect attacks but never hide threats", () =>
|
|
{
|
|
foreach (var kind in Enum.GetValues<ObjectiveKind>())
|
|
{
|
|
var s = Snap(Enemy(10) with { IsObjective = true, ThreateningObjective = true, Kind = kind });
|
|
var c = new Settings { DisabledObjectives = [kind] };
|
|
Suite.True(Decide(s, c, Skill.PowerfulShot) == null); Suite.True(TargetSelector.HasNearbyThreat(s));
|
|
SkillIs(Skill.PowerfulShot, Decide(s, new(), Skill.PowerfulShot));
|
|
}
|
|
});
|
|
suite.Run("Original Harmonic behavior remains immediate and aligned mode has bounded hold", () =>
|
|
{
|
|
var s = Snap(Enemy(10)) with { HarmonicCharges = 1 };
|
|
SkillIs(Skill.HarmonicArrow, Decide(s, new(), Skill.HarmonicArrow, Skill.PowerfulShot));
|
|
var c = new Settings { Burst = BurstStrategy.AlignBuffs };
|
|
SkillIs(Skill.PowerfulShot, Decide(s, c, Skill.HarmonicArrow, Skill.PowerfulShot));
|
|
SkillIs(Skill.HarmonicArrow, Decide(s with { HarmonicReadySeconds = 4 }, c, Skill.HarmonicArrow));
|
|
SkillIs(Skill.HarmonicArrow, Decide(s with { HarmonicCharges = 3 }, c, Skill.HarmonicArrow));
|
|
SkillIs(Skill.HarmonicArrow, Decide(s with { Player = Self(Statuses.Bravery) }, c, Skill.HarmonicArrow));
|
|
});
|
|
suite.Run("Manual burst holds only declared burst skills and releases in window", () =>
|
|
{
|
|
var s = Snap(Enemy(10)) with { Player = Self(Statuses.RoleBravery), LimitFull = true };
|
|
var c = new Settings { Burst = BurstStrategy.ManualWindow };
|
|
SkillIs(Skill.PowerfulShot, Decide(s, c, Skill.HarmonicArrow, Skill.Bravery, Skill.FinalFantasia, Skill.PowerfulShot));
|
|
SkillIs(Skill.Bravery, Decide(s with { BurstWindow = true }, c, Skill.Bravery, Skill.PowerfulShot));
|
|
SkillIs(Skill.EncoreOfLight, Decide(s with { Player = Self(Statuses.EncoreReady) }, c, Skill.EncoreOfLight, Skill.PowerfulShot));
|
|
SkillIs(Skill.EagleEyeShot, Decide(s with { Player = Self(Statuses.RoleEagle), Enemies = [Enemy(10, 9000)] }, c, Skill.EagleEyeShot));
|
|
});
|
|
suite.Run("Aoe circle strategy chooses dense players only when enabled", () =>
|
|
{
|
|
var s = Snap(Enemy(10, 1000) with { Position = new(0, 0, 20) }, Enemy(11, 40000) with { Position = new(10, 0, 10) },
|
|
Enemy(12, 50000) with { Position = new(11, 0, 10) }) with
|
|
{ Player = Self(Statuses.EncoreReady), Shapes = new Dictionary<Skill, AoeShape> { [Skill.EncoreOfLight] = new(false, 5) } };
|
|
Suite.Equal(10ul, Decide(s, new(), Skill.EncoreOfLight)!.TargetId);
|
|
Suite.Equal(11ul, Decide(s, new() { PreferAoe = true }, Skill.EncoreOfLight)!.TargetId);
|
|
Suite.Equal(10ul, Decide(s, new() { PreferAoe = true, AoeMinTargets = 4 }, Skill.EncoreOfLight)!.TargetId);
|
|
});
|
|
suite.Run("Aoe line counts width range direction elevation and immunity", () =>
|
|
{
|
|
var aim = Enemy(10) with { Position = new(0, 0, 10) };
|
|
var s = Snap(aim, Enemy(11) with { Position = new(1, 0, 20) }, Enemy(12) with { Position = new(0, 0, -5) },
|
|
Enemy(13) with { Position = new(4, 0, 10) }, Enemy(14) with { Position = new(0, 8, 10) },
|
|
Enemy(15) with { Position = new(0, 0, 30) }, Enemy(16) with { Position = new(0, 0, 15), Invulnerable = true });
|
|
Suite.Equal(2, Strategy.AoeCount(s, new(), Skill.ApexArrow, aim, new(true, 25, 2)));
|
|
});
|
|
suite.Run("Aoe strategy never diverts from players to a denser objective pack", () =>
|
|
{
|
|
var s = Snap(Enemy(10) with { Position = new(0, 0, 20) }, Enemy(11) with { IsObjective = true }, Enemy(12) with { IsObjective = true })
|
|
with { Player = Self(Statuses.EncoreReady), Shapes = new Dictionary<Skill, AoeShape> { [Skill.EncoreOfLight] = new(false, 5) } };
|
|
Suite.Equal(10ul, Decide(s, new() { PreferAoe = true }, Skill.EncoreOfLight)!.TargetId);
|
|
});
|
|
suite.Run("Focus defense uses enemy target and cast signals without double-counting actors", () =>
|
|
{
|
|
var s = Snap(Enemy(10) with { TargetId = 1, CastTargetId = 1 }, Enemy(11) with { CastTargetId = 1 }, Enemy(12) with { TargetId = 1 },
|
|
Enemy(13) with { TargetId = 1, Enemy = false }, Enemy(14) with { TargetId = 1, Dead = true }, Enemy(15) with { TargetId = 1, Distance = 60 })
|
|
with { Player = Self() with { Hp = 23000 } };
|
|
Suite.Equal(3, Strategy.FocusCount(s));
|
|
SkillIs(Skill.Recuperate, Decide(s, new(), Skill.Guard, Skill.Recuperate));
|
|
SkillIs(Skill.Guard, Decide(s, new() { FocusDefense = true }, Skill.Guard, Skill.Recuperate));
|
|
Suite.True(Decide(s with { AliveSeconds = 4 }, new() { FocusDefense = true }, Skill.Guard, Skill.Recuperate) == null);
|
|
});
|
|
suite.Run("Focus pressure lowers healing threshold but never raises it or removes Guard protection", () =>
|
|
{
|
|
var s = Snap() with { Player = Self() with { Hp = 53000 }, RecentHpLossRatio = .3 };
|
|
Suite.True(Decide(s, new(), Skill.Recuperate) == null);
|
|
SkillIs(Skill.Recuperate, Decide(s, new() { FocusDefense = true }, Skill.Recuperate));
|
|
Suite.Equal(3000, Strategy.HealDeficit(s, new() { FocusDefense = true, RecuperateMissingHp = 3000 }));
|
|
Suite.True(Decide(s with { Player = Self(Statuses.Guard) with { Hp = 53000 } }, new() { FocusDefense = true }, Skill.Recuperate) == null);
|
|
});
|
|
suite.Run("HP pressure expires after two seconds and ignores healing increments", () =>
|
|
{
|
|
var p = new HealthPressure(); Suite.Equal(0d, p.Observe(0, 60000, 60000));
|
|
Suite.Equal(.25, p.Observe(.5, 45000, 60000)); Suite.Equal(.25, p.Observe(1, 55000, 60000));
|
|
Suite.Equal(0d, p.Observe(2.6, 55000, 60000)); p.Reset(); Suite.Equal(0d, p.Observe(3, 1000, 60000));
|
|
});
|
|
suite.Run("Manual backstep switch leaves the rest of the rotation and skill toggle intact", () =>
|
|
{
|
|
var s = Snap(Enemy(10) with { Distance = 8 });
|
|
SkillIs(Skill.RepellingShot, Decide(s, new(), Skill.RepellingShot, Skill.SilentNocturne));
|
|
SkillIs(Skill.SilentNocturne, Decide(s, new() { ManualRepelling = true }, Skill.RepellingShot, Skill.SilentNocturne));
|
|
Suite.True(Decide(s, new() { DisabledSkills = [Skill.RepellingShot] }, Skill.RepellingShot) == null);
|
|
});
|
|
suite.Run("Decision audit contains every skill and does not carry stale results to another frame", () =>
|
|
{
|
|
var s = Snap(Enemy(10)); var c = new Settings();
|
|
var selected = Decide(s, c, Skill.PowerfulShot);
|
|
var rows = DecisionAudit.Evaluate(s, c, selected, "运行", (_, _) => "冷却");
|
|
Suite.Equal(Actions.All.Length, rows.Count); Suite.True(rows.Single(r => r.Skill == Skill.PowerfulShot).Result.Contains("本轮选中"));
|
|
Suite.True(rows.Single(r => r.Skill == Skill.EncoreOfLight).Result.Contains("4312"));
|
|
var paused = DecisionAudit.Evaluate(null, c, null, "非支持区域", (_, _) => throw new Exception("must not probe"));
|
|
Suite.True(paused.All(r => r.TargetId == 0 && r.Result == "非支持区域"));
|
|
});
|
|
suite.Run("Decision audit distinguishes toggle policy native failure and higher priority selection", () =>
|
|
{
|
|
var s = Snap(Enemy(10) with { Distance = 8 }) with { Player = Self(Statuses.EncoreReady) };
|
|
var c = new Settings { ManualRepelling = true, DisabledSkills = [Skill.HarmonicArrow] };
|
|
var selected = Decide(s, c, Skill.EncoreOfLight);
|
|
var rows = DecisionAudit.Evaluate(s, c, selected, "运行", (skill, _) => skill == Skill.SilentNocturne ? "原生状态 582" : null);
|
|
Suite.True(rows.Single(r => r.Skill == Skill.HarmonicArrow).Result.Contains("关闭"));
|
|
Suite.True(rows.Single(r => r.Skill == Skill.RepellingShot).Result.Contains("手动"));
|
|
Suite.True(rows.Single(r => r.Skill == Skill.SilentNocturne).Result.Contains("582"));
|
|
Suite.True(rows.Single(r => r.Skill == Skill.PowerfulShot).Result.Contains("优先选择"));
|
|
});
|
|
suite.Run("Audit never claims Sprint ready with a valid attack target", () =>
|
|
{
|
|
var s = Snap(Enemy(10)) with { Moving = true };
|
|
var rows = DecisionAudit.Evaluate(s, new(), null, "等待", (_, _) => null, (_, _) => true);
|
|
Suite.True(rows.Single(r => r.Skill == Skill.Sprint).Result.Contains("不自动冲刺"));
|
|
});
|
|
}
|
|
}
|