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 { /// /// 적 AI 예측 시뮬레이션(PD #1098 · 전술규칙_적AI_기획서 §2) — 전술 항목이 점수에 반영되는지(함정 쪽 튕기기 · 창병 아군 밀어 전진 · 기병 일렬 휩쓸기 · /// 궁병 목책 엄폐 · 책사 덤불 제거) · 가상 실행이 상태를 되돌리는지 · 성능(적 10기 턴 결정 ≤ 1.5 초). 가중치 = ai_weights.csv(AiWeights 자산). /// 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 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(); 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 }), "보스만"); } } }