feat(enemy): 选招器支持防重复折扣——WeightedRandomAntiRepeat
上一次选中的招按 EnemyStatsSO.attackAntiRepeatFactor 折扣权重(默认 0.3)。 折扣把唯一候选压到 0 时退化为 Priority 路径,不会出现敌人永不出手。 阶段切换清空防重复记忆。取代 BossBase.UseBossSkillWeighted 里的同名逻辑。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -104,5 +104,84 @@ namespace BaseGames.Tests.EditMode.Enemies
|
||||
for (int i = 0; i < 50; i++)
|
||||
Assert.AreSame(ok, Sel(bad, ok).Select(true, true, AttackSelectionMode.WeightedRandom));
|
||||
}
|
||||
|
||||
// ── WeightedRandomAntiRepeat ─────────────────────────────────────────
|
||||
|
||||
static EnemyAttackSelector SelAntiRepeat(float factor, params IAttackCandidate[] cs)
|
||||
=> new EnemyAttackSelector(new List<IAttackCandidate>(cs), factor);
|
||||
|
||||
[Test]
|
||||
public void AntiRepeat_ZeroFactor_NeverPicksSameTwiceWhenAlternativeExists()
|
||||
{
|
||||
var a = new FakeCandidate { WeightV = 1f };
|
||||
var b = new FakeCandidate { WeightV = 1f };
|
||||
var s = SelAntiRepeat(0f, a, b);
|
||||
|
||||
var prev = s.Select(true, true, AttackSelectionMode.WeightedRandomAntiRepeat);
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var cur = s.Select(true, true, AttackSelectionMode.WeightedRandomAntiRepeat);
|
||||
Assert.AreNotSame(prev, cur, "系数为 0 时不应连续选中同一招");
|
||||
prev = cur;
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AntiRepeat_ZeroFactor_SingleCandidate_StillReturnsIt()
|
||||
{
|
||||
// 只有一招时折扣会把权重压到 0,WeightedPick 返回 -1;
|
||||
// 必须退化为 Priority 路径把它选出来,否则敌人永远不出手。
|
||||
var only = new FakeCandidate { WeightV = 1f };
|
||||
var s = SelAntiRepeat(0f, only);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
Assert.AreSame(only, s.Select(true, true, AttackSelectionMode.WeightedRandomAntiRepeat));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AntiRepeat_DiscountsPreviousPick_ButDoesNotBanIt()
|
||||
{
|
||||
// 系数 0.5:a 被选过后权重折半,但仍有机会再被选中。
|
||||
var a = new FakeCandidate { WeightV = 1f };
|
||||
var b = new FakeCandidate { WeightV = 1f };
|
||||
var s = SelAntiRepeat(0.5f, a, b);
|
||||
|
||||
s.Select(true, true, AttackSelectionMode.WeightedRandomAntiRepeat);
|
||||
int repeats = 0;
|
||||
var prev = s.LastPicked;
|
||||
for (int i = 0; i < 400; i++)
|
||||
{
|
||||
var cur = s.Select(true, true, AttackSelectionMode.WeightedRandomAntiRepeat);
|
||||
if (ReferenceEquals(cur, prev)) repeats++;
|
||||
prev = cur;
|
||||
}
|
||||
Assert.Greater(repeats, 0, "折扣不应等于禁用");
|
||||
Assert.Less (repeats, 200, "折扣后重复率应低于无折扣时的约 50%");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AntiRepeat_IneligiblePrevious_DoesNotBlockOthers()
|
||||
{
|
||||
var a = new FakeCandidate { WeightV = 1f };
|
||||
var b = new FakeCandidate { WeightV = 1f };
|
||||
var s = SelAntiRepeat(0f, a, b);
|
||||
|
||||
var first = s.Select(true, true, AttackSelectionMode.WeightedRandomAntiRepeat);
|
||||
((FakeCandidate)first).CanUseV = false; // 上一招进冷却
|
||||
|
||||
var second = s.Select(true, true, AttackSelectionMode.WeightedRandomAntiRepeat);
|
||||
Assert.IsNotNull(second);
|
||||
Assert.AreNotSame(first, second);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PlainWeighted_DoesNotApplyAntiRepeat()
|
||||
{
|
||||
// WeightedRandom 模式下唯一候选恒被选中(回归:折扣不得泄漏到旧模式)
|
||||
var only = new FakeCandidate { WeightV = 1f };
|
||||
var s = SelAntiRepeat(0f, only);
|
||||
for (int i = 0; i < 10; i++)
|
||||
Assert.AreSame(only, s.Select(true, true, AttackSelectionMode.WeightedRandom));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user