200 lines
9.3 KiB
C#
200 lines
9.3 KiB
C#
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using NUnit.Framework;
|
|
using TKS.OutGame;
|
|
using UnityEngine;
|
|
using UnityEngine.SceneManagement;
|
|
using UnityEngine.TestTools;
|
|
|
|
namespace TKS.Tests
|
|
{
|
|
/// <summary>
|
|
/// 적 AI 예측 시뮬레이션(PD #1098 · 전술규칙_적AI_기획서 §2) — 전술 항목이 점수에 반영되는지(함정 쪽 튕기기 · 창병 아군 밀어 전진 · 기병 일렬 휩쓸기 ·
|
|
/// 궁병 목책 엄폐 · 책사 덤불 제거) · 가상 실행이 상태를 되돌리는지 · 성능(적 10기 턴 결정 ≤ 1.5 초). 가중치 = ai_weights.csv(AiWeights 자산).
|
|
/// </summary>
|
|
public class TacticalAITests
|
|
{
|
|
BattleManager bm;
|
|
GimmickLayer L;
|
|
AiWeights W;
|
|
|
|
[UnitySetUp]
|
|
public IEnumerator SetUp()
|
|
{
|
|
WorldProgress.Reset();
|
|
SaveStore.UseMemory(true);
|
|
SceneManager.LoadScene("Battle");
|
|
yield return null;
|
|
yield return null;
|
|
bm = BattleManager.Instance;
|
|
var wm = WorldMapScreen.Instance;
|
|
if (wm != null && wm.HasData) wm.EnterStage(0, 0);
|
|
yield return null;
|
|
W = AiWeights.Load();
|
|
Assert.IsNotNull(W, "AiWeights 자산(TKS/Import Gimmicks)");
|
|
Time.timeScale = 20f;
|
|
}
|
|
|
|
[UnityTearDown]
|
|
public IEnumerator TearDown()
|
|
{
|
|
Time.timeScale = 1f;
|
|
yield return null;
|
|
}
|
|
|
|
IEnumerator Fresh()
|
|
{
|
|
bm.StopAllCoroutines();
|
|
bm.ClearUnits();
|
|
bm.ForcePhase(BattlePhase.EnemyTurn);
|
|
L = GimmickLayer.Ensure(bm);
|
|
L.Clear();
|
|
yield return null;
|
|
}
|
|
|
|
AiPlan Best(Unit e)
|
|
{
|
|
List<AiPlan> list;
|
|
var p = TacticalAI.Evaluate(bm, e, bm.AliveCount(Faction.Enemy), out list);
|
|
Assert.IsNotNull(p, "후보 있음");
|
|
return p;
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Infantry_BouncesPlayerIntoTrap_AndSimRestoresState()
|
|
{
|
|
yield return Fresh();
|
|
var trap = L.Spawn("pit_trap", GimmickShape.Circle(new Vector2(0f, 3.4f), 0.6f));
|
|
var e = bm.SpawnUnit(UnitType.Infantry, Faction.Enemy, new Vector2(0f, -1.5f), "적보병");
|
|
var victim = bm.SpawnUnit(UnitType.Spear, Faction.Player, new Vector2(0f, 1.2f), "아군창병");
|
|
var other = bm.SpawnUnit(UnitType.Infantry, Faction.Player, new Vector2(-4f, -1.5f), "아군보병");
|
|
Vector2 v0 = victim.Position, e0 = e.Position;
|
|
int hv = victim.Hp, he = e.Hp;
|
|
var best = Best(e);
|
|
Assert.IsTrue(best.Why.Contains("기믹 Trap"), "함정 쪽으로 튕겨내는 수가 최고 점수(" + best.Why + ")");
|
|
Assert.AreEqual(v0, victim.Position, "가상 실행 뒤 위치 복원");
|
|
Assert.AreEqual(e0, e.Position);
|
|
Assert.AreEqual(hv, victim.Hp, "HP 복원");
|
|
Assert.AreEqual(he, e.Hp);
|
|
Assert.IsFalse(trap.Removed, "기믹 상태 복원");
|
|
var vs = L.StateOf(victim);
|
|
Assert.IsTrue(vs == null || vs.Stun == 0, "행동 불가 복원");
|
|
|
|
var o = new ActionOutcome();
|
|
yield return bm.ResolveForTest(e, best.Target, false, o);
|
|
Assert.IsTrue(trap.Revealed, "실제 실행 = 함정 발동");
|
|
Assert.Greater(L.StateOf(victim).Stun, 0);
|
|
Assert.IsTrue(other.IsAlive);
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Spear_PushesOwnAllyTowardPlayers_SupportScore()
|
|
{
|
|
yield return Fresh();
|
|
var spear = bm.SpawnUnit(UnitType.Spear, Faction.Enemy, new Vector2(0f, 4f), "적창병");
|
|
var ally = bm.SpawnUnit(UnitType.Infantry, Faction.Enemy, new Vector2(0f, 2.5f), "적보병");
|
|
var p = bm.SpawnUnit(UnitType.Infantry, Faction.Player, new Vector2(0f, -4f), "아군");
|
|
var best = Best(spear);
|
|
Assert.IsTrue(best.Why.Contains("서포트"), "창병 = 아군을 밀어 적진에 빨리 닿게(" + best.Why + ")");
|
|
Assert.AreEqual(0, best.Group, "서포터 그룹 = 먼저 움직인다");
|
|
Assert.IsTrue(p.IsAlive && ally.IsAlive);
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Cavalry_SweepsLinedUpPlayers()
|
|
{
|
|
yield return Fresh();
|
|
var cav = bm.SpawnUnit(UnitType.Cavalry, Faction.Enemy, new Vector2(0f, 4f), "적기병");
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
var u = bm.SpawnUnit(UnitType.Infantry, Faction.Player, new Vector2(0f, 1.5f - i * 1.5f), "줄" + i);
|
|
u.MaxHp = u.Hp = 5000;
|
|
}
|
|
bm.SpawnUnit(UnitType.Infantry, Faction.Player, new Vector2(5f, 3f), "따로");
|
|
var best = Best(cav);
|
|
int n = 0;
|
|
var m = System.Text.RegularExpressions.Regex.Match(best.Why, "관통 (\\d+)");
|
|
if (m.Success) n = int.Parse(m.Groups[1].Value);
|
|
Assert.GreaterOrEqual(n, 2, "일렬로 선 적을 한 번에 꿰뚫는 수(" + best.Why + ")");
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Archer_BehindPalisade_CoverScore()
|
|
{
|
|
yield return Fresh();
|
|
var a = bm.SpawnUnit(UnitType.Archer, Faction.Enemy, new Vector2(0f, 3f), "적궁병");
|
|
var p = bm.SpawnUnit(UnitType.Infantry, Faction.Player, new Vector2(0f, -0.8f), "아군");
|
|
p.MaxHp = p.Hp = 5000;
|
|
float open = Best(a).Score;
|
|
L.Spawn("palisade", GimmickShape.Box(new Vector2(0f, 1.6f), new Vector2(6f, 0.4f)));
|
|
var best = Best(a);
|
|
Assert.IsTrue(best.Why.Contains("목책 엄폐"), "목책 뒤 사격(" + best.Why + ")");
|
|
Assert.AreEqual(open + W["archer_cover"], best.Score, 1.5f, "엄폐 가중치만큼 점수 +");
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Strategist_ClearsThorn_OpensAllyRoute()
|
|
{
|
|
yield return Fresh();
|
|
var st = bm.SpawnUnit(UnitType.Strategist, Faction.Enemy, new Vector2(0f, 3.5f), "적책사");
|
|
L.Spawn("thorn_bush", GimmickShape.Box(Vector2.zero, new Vector2(3f, 0.6f)));
|
|
var p = bm.SpawnUnit(UnitType.Infantry, Faction.Player, new Vector2(0f, -0.9f), "아군");
|
|
p.MaxHp = p.Hp = 5000;
|
|
bm.SpawnUnit(UnitType.Infantry, Faction.Enemy, new Vector2(1.6f, 1.9f), "적보병");
|
|
var best = Best(st);
|
|
Assert.IsTrue(best.Why.Contains("덤불 제거"), "책사 = 덤불을 지워 아군 돌격로를 연다(" + best.Why + ")");
|
|
Assert.AreEqual(0, best.Group, "서포터 그룹");
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Performance_10Enemies_TurnDecisions_Under1500ms()
|
|
{
|
|
yield return Fresh();
|
|
var pending = new List<Unit>();
|
|
var types = new[] { UnitType.Infantry, UnitType.Spear, UnitType.Cavalry, UnitType.Archer, UnitType.Strategist };
|
|
for (int i = 0; i < 10; i++)
|
|
pending.Add(bm.SpawnUnit(types[i % 5], Faction.Enemy, new Vector2(-8f + (i % 5) * 4f, 2f + (i / 5) * 2.5f), "적" + i));
|
|
for (int i = 0; i < 5; i++) bm.SpawnUnit(types[i], Faction.Player, new Vector2(-8f + i * 4f, -3f), "아군" + i);
|
|
L.Spawn("palisade", GimmickShape.Box(new Vector2(-4f, 0f), new Vector2(4f, 0.4f)));
|
|
L.Spawn("swamp", GimmickShape.Box(new Vector2(4f, 0f), new Vector2(6f, 2f)));
|
|
L.Spawn("pit_trap", GimmickShape.Circle(new Vector2(0f, -1f), 0.6f));
|
|
var sw = Stopwatch.StartNew();
|
|
int decisions = 0;
|
|
var rng = new System.Random(3);
|
|
while (pending.Count > 0)
|
|
{
|
|
var plan = TacticalAI.Next(bm, pending, 0.6f, 30f, rng);
|
|
if (plan == null) break;
|
|
pending.Remove(plan.Unit);
|
|
decisions++;
|
|
}
|
|
sw.Stop();
|
|
UnityEngine.Debug.Log("[TacticalAITests] 적 10기 턴 결정 " + decisions + "회 · " + sw.ElapsedMilliseconds + " ms");
|
|
Assert.AreEqual(10, decisions);
|
|
Assert.LessOrEqual(sw.ElapsedMilliseconds, 1500, "적 10기 턴(1기마다 재평가) ≤ 1.5 초");
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator EnemyTurn_UsesTacticalAI_AndBossActiveGate()
|
|
{
|
|
yield return Fresh();
|
|
var p = bm.SpawnUnit(UnitType.Infantry, Faction.Player, new Vector2(0f, -3f), "아군");
|
|
var e = bm.SpawnUnit(UnitType.Infantry, Faction.Enemy, new Vector2(0f, 2f), "적");
|
|
p.MarkActed();
|
|
bm.LastAiPlan = null;
|
|
yield return bm.RunEnemyTurn();
|
|
Assert.IsNotNull(bm.LastAiPlan, "적 턴 = 예측 AI 결정");
|
|
Assert.AreEqual(e, bm.LastAiPlan.Unit);
|
|
Assert.AreEqual(BattlePhase.PlayerTurn, bm.Phase);
|
|
|
|
var boss = new BalanceConfig.EnemySpawn { isBoss = true };
|
|
var ch1 = new BalanceConfig.StageDef { chapter = 1 };
|
|
var ch11 = new BalanceConfig.StageDef { chapter = Mathf.RoundToInt(W["active_max_chapter"]) + 1 };
|
|
Assert.IsTrue(TacticalAI.BossUsesActive(ch1, boss), "1부 보스 = 액티브 사용");
|
|
Assert.IsFalse(TacticalAI.BossUsesActive(ch11, boss), "2부 = 아직");
|
|
Assert.IsFalse(TacticalAI.BossUsesActive(ch1, new BalanceConfig.EnemySpawn { isGeneral = true }), "보스만");
|
|
}
|
|
}
|
|
}
|