AIAgent.cs 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884
  1. using UnityEngine;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. /// <summary>
  5. /// AI Agent that navigates the maze with limited knowledge
  6. /// Only knows about the room it's currently in and rooms visited by same character type
  7. /// </summary>
  8. public class AIAgent : MonoBehaviour
  9. {
  10. [Header("Agent Identity")]
  11. [SerializeField] private string agentCharacterType = "Default";
  12. [SerializeField] private int agentId;
  13. [Header("AI Settings")]
  14. [SerializeField] private float movementSpeed = 2f;
  15. [SerializeField] private float pathUpdateInterval = 0.5f;
  16. [SerializeField] private float stoppingDistance = 0.1f;
  17. private float actualMovementSpeed; // Per-agent speed variation to reduce synchronization
  18. [Header("Pathfinding")]
  19. [SerializeField] private bool showPath = true;
  20. [SerializeField] private LineRenderer pathRenderer;
  21. [SerializeField] private Color pathColor = Color.yellow;
  22. private MazeController mazeController;
  23. private MazeData maze;
  24. private MazePathfinder pathfinder;
  25. private AIRoomMemory roomMemory;
  26. private List<Vector2Int> currentPath = new();
  27. private int currentPathIndex = 0;
  28. private float lastPathUpdate = 0f;
  29. private Vector2Int currentRoom = Vector2Int.zero;
  30. private Vector2Int targetRoom = Vector2Int.zero;
  31. private Vector2Int targetExitTile = Vector2Int.zero; // Target hallway exit to move toward
  32. private Queue<int> recentRooms = new Queue<int>(); // Track last N rooms to avoid backtracking
  33. private const int RECENT_ROOMS_BUFFER_SIZE = 10; // Larger buffer = less backtracking
  34. private MazeRoom lastRoomExitedFrom = null; // Track which room we exited to prevent immediate backtracking
  35. private bool hasReachedGoal = false; // Track if agent has reached the goal room
  36. private bool commitedToExit = false; // Track if agent has committed to a specific hallway exit
  37. private float nextRandomWait = 0f; // Random wait time before exploring new areas
  38. private float agentRandomOffset = 0f; // Per-agent random offset to desync movement
  39. void Start()
  40. {
  41. mazeController = FindAnyObjectByType<MazeController>();
  42. if (mazeController == null)
  43. {
  44. Debug.LogError("AIAgent: MazeController not found in scene!");
  45. enabled = false;
  46. return;
  47. }
  48. maze = mazeController.GetCurrentMaze();
  49. if (maze == null)
  50. {
  51. Debug.LogError("AIAgent: Current maze is null!");
  52. enabled = false;
  53. return;
  54. }
  55. pathfinder = new MazePathfinder(maze);
  56. roomMemory = AIRoomMemoryManager.GetMemory(agentCharacterType);
  57. // Initialize with a random start point
  58. if (maze.StartPoints.Count > 0)
  59. {
  60. int startIndex = agentId % maze.StartPoints.Count; // Distribute agents across start points
  61. Vector2Int startPos = maze.StartPoints[startIndex];
  62. // Get the start room and spawn randomly within it
  63. MazeRoom startRoom = maze.GetRoomAtTile(startPos.x, startPos.y);
  64. if (startRoom != null)
  65. {
  66. // Spawn at random position within start room
  67. Vector2Int randomPosInRoom = new Vector2Int(
  68. Random.Range(startRoom.MinX + 1, startRoom.MaxX),
  69. Random.Range(startRoom.MinY + 1, startRoom.MaxY)
  70. );
  71. transform.position = new Vector3(randomPosInRoom.x + 0.5f, 1f, randomPosInRoom.y + 0.5f);
  72. }
  73. else
  74. {
  75. // Fallback to exact start point
  76. transform.position = new Vector3(startPos.x + 0.5f, 1f, startPos.y + 0.5f);
  77. }
  78. // Rotate to be visible from above - 90 degrees around X axis
  79. transform.rotation = Quaternion.Euler(90, 0, 0);
  80. UpdateCurrentRoom(); // This will add to recentRooms and visit room
  81. Debug.Log($"AIAgent {agentId}: Initialized at {transform.position}, in room {currentRoom.x}");
  82. }
  83. else
  84. {
  85. Debug.LogError("AIAgent: No start points found in maze!");
  86. enabled = false;
  87. return;
  88. }
  89. // Setup per-agent random offset to desync decision timings (prevents all agents moving in sync)
  90. agentRandomOffset = Random.Range(0f, pathUpdateInterval * 0.5f);
  91. nextRandomWait = Time.time + Random.Range(2f, 4f); // Random initial wait before first decision
  92. // Randomize movement speed slightly per agent (±20% variation)
  93. actualMovementSpeed = movementSpeed * Random.Range(0.8f, 1.2f);
  94. // Setup path renderer
  95. if (showPath && pathRenderer == null)
  96. {
  97. pathRenderer = gameObject.AddComponent<LineRenderer>();
  98. pathRenderer.startWidth = 0.1f;
  99. pathRenderer.endWidth = 0.1f;
  100. pathRenderer.material = new Material(Shader.Find("Sprites/Default"));
  101. pathRenderer.startColor = pathColor;
  102. pathRenderer.endColor = pathColor;
  103. }
  104. Debug.Log($"AIAgent {agentId} ({agentCharacterType}) spawned at {transform.position}");
  105. }
  106. void Update()
  107. {
  108. if (maze == null) return;
  109. // Update current room
  110. UpdateCurrentRoom();
  111. // Update pathfinding periodically with agent-specific offset to desync behavior
  112. if (Time.time - lastPathUpdate > pathUpdateInterval + agentRandomOffset)
  113. {
  114. UpdatePathToGoal();
  115. lastPathUpdate = Time.time;
  116. }
  117. // Move along path
  118. FollowPath();
  119. // Debug visualization
  120. if (showPath && pathRenderer != null)
  121. {
  122. UpdatePathVisualization();
  123. }
  124. }
  125. /// <summary>
  126. /// Updates the current room based on position
  127. /// </summary>
  128. private void UpdateCurrentRoom()
  129. {
  130. Vector2Int tilePos = WorldToTile(transform.position);
  131. MazeRoom room = maze.GetRoomAtTile(tilePos.x, tilePos.y);
  132. if (room != null)
  133. {
  134. if (currentRoom.x != room.Id)
  135. {
  136. currentRoom = new Vector2Int(room.Id, 0);
  137. roomMemory.VisitRoom(room.Id);
  138. // Track recent rooms to avoid immediate backtracking
  139. recentRooms.Enqueue(room.Id);
  140. if (recentRooms.Count > RECENT_ROOMS_BUFFER_SIZE)
  141. recentRooms.Dequeue();
  142. // Debug.Log($"AIAgent {agentId}: Entered room {room.Id} ({room.Type})");
  143. }
  144. }
  145. }
  146. /// <summary>
  147. /// Updates pathfinding to reach the goal
  148. /// Agent only knows about current room and visited rooms
  149. /// NEW LOGIC: Pick a hallway exit from current room and move straight to it
  150. /// </summary>
  151. private void UpdatePathToGoal()
  152. {
  153. if (maze.ExitPoints.Count == 0)
  154. {
  155. Debug.LogWarning($"AIAgent {agentId}: No exit points in maze!");
  156. return;
  157. }
  158. Vector2Int currentPos = WorldToTile(transform.position);
  159. MazeRoom currentRoomData = maze.GetRoomAtTile(currentPos.x, currentPos.y);
  160. // CHECK FOR GOAL ROOM FIRST - this should work even while following path
  161. var goalRooms = maze.GetRoomsByType(MazeRoom.RoomType.End);
  162. if (currentRoomData != null && goalRooms.Contains(currentRoomData))
  163. {
  164. if (!hasReachedGoal)
  165. {
  166. hasReachedGoal = true;
  167. currentPath.Clear();
  168. currentPathIndex = 0;
  169. Debug.Log($"AIAgent {agentId}: *** GOAL DETECTED *** Stopped at position {currentPos} in goal room");
  170. }
  171. return; // Stay stopped
  172. }
  173. // If we already have a valid path and we're following it, don't recalculate (stay committed)
  174. if (currentPath.Count > 0 && currentPathIndex < currentPath.Count)
  175. {
  176. return; // Keep following existing path
  177. }
  178. // If we've committed to reaching an exit and haven't reached it yet, keep trying
  179. if (commitedToExit && targetExitTile != Vector2Int.zero)
  180. {
  181. // Try to find a path to the committed exit
  182. if (currentPath.Count == 0)
  183. {
  184. currentPath = FindPathInRoom(currentPos, targetExitTile, currentRoomData);
  185. currentPathIndex = 0;
  186. if (currentPath.Count > 0)
  187. {
  188. Debug.Log($"AIAgent {agentId}: Re-committed to exit {targetExitTile}");
  189. return;
  190. }
  191. }
  192. }
  193. // If in hallway (no room), just keep moving along current path
  194. // The hallway pathfinding will naturally move us toward exits
  195. if (currentRoomData == null)
  196. {
  197. // If we have a current path, keep following it through hallway
  198. if (currentPath.Count > 0 && currentPathIndex < currentPath.Count)
  199. {
  200. return; // Keep following path
  201. }
  202. // In hallway with no path - find next room to enter
  203. // PRIORITY 1: Find unvisited rooms, avoid the room we just exited
  204. // Collect all unvisited rooms
  205. List<MazeRoom> unvisitedRooms = new();
  206. foreach (var room in maze.Rooms)
  207. {
  208. // Skip the room we just exited (backtracking prevention)
  209. if (lastRoomExitedFrom != null && room.Id == lastRoomExitedFrom.Id)
  210. continue;
  211. // Prioritize unvisited rooms
  212. if (!roomMemory.HasVisited(room.Id))
  213. {
  214. unvisitedRooms.Add(room);
  215. }
  216. }
  217. MazeRoom targetRoom = null;
  218. // Random decision: 70% closest unvisited, 30% random unvisited (increases exploration variety)
  219. if (unvisitedRooms.Count > 0)
  220. {
  221. if (Random.value < 0.7f && unvisitedRooms.Count > 0)
  222. {
  223. // Pick closest unvisited room
  224. float closestUnvisitedDistance = float.MaxValue;
  225. foreach (var room in unvisitedRooms)
  226. {
  227. Vector2Int roomCenter = room.GetCenter();
  228. float distance = Vector2Int.Distance(currentPos, roomCenter);
  229. if (distance < closestUnvisitedDistance)
  230. {
  231. closestUnvisitedDistance = distance;
  232. targetRoom = room;
  233. }
  234. }
  235. }
  236. else
  237. {
  238. // Pick random unvisited room for variety
  239. targetRoom = unvisitedRooms[Random.Range(0, unvisitedRooms.Count)];
  240. Debug.Log($"AIAgent {agentId}: Randomly chose unvisited room {targetRoom.Id} for variety");
  241. }
  242. }
  243. // If no unvisited room found, pick nearest room (except the one we came from)
  244. if (targetRoom == null)
  245. {
  246. float closestDistance = float.MaxValue;
  247. foreach (var room in maze.Rooms)
  248. {
  249. // Skip the room we just exited
  250. if (lastRoomExitedFrom != null && room.Id == lastRoomExitedFrom.Id)
  251. continue;
  252. Vector2Int roomCenter = room.GetCenter();
  253. float distance = Vector2Int.Distance(currentPos, roomCenter);
  254. if (distance < closestDistance)
  255. {
  256. closestDistance = distance;
  257. targetRoom = room;
  258. }
  259. }
  260. }
  261. if (targetRoom != null)
  262. {
  263. currentPath = FindPathToNearestRoom(currentPos, targetRoom);
  264. currentPathIndex = 0;
  265. if (currentPath.Count > 0)
  266. {
  267. bool isUnvisited = !roomMemory.HasVisited(targetRoom.Id);
  268. Debug.Log($"AIAgent {agentId}: In hallway, moving toward room {targetRoom.Id} ({(isUnvisited ? "unvisited" : "visited")}), path length: {currentPath.Count}");
  269. return;
  270. }
  271. else
  272. {
  273. Debug.LogWarning($"AIAgent {agentId}: Failed to path to room {targetRoom.Id} from hallway at {currentPos}. Trying any adjacent room.");
  274. // Fallback: try to find ANY adjacent room and move directly toward it
  275. foreach (var room in maze.Rooms)
  276. {
  277. if (lastRoomExitedFrom != null && room.Id == lastRoomExitedFrom.Id)
  278. continue;
  279. currentPath = FindPathToNearestRoom(currentPos, room);
  280. if (currentPath.Count > 0)
  281. {
  282. Debug.Log($"AIAgent {agentId}: Fallback path found to room {room.Id}, length: {currentPath.Count}");
  283. return;
  284. }
  285. }
  286. Debug.LogError($"AIAgent {agentId}: STUCK in hallway at {currentPos} - no paths found to any room!");
  287. return;
  288. }
  289. }
  290. Debug.LogWarning($"AIAgent {agentId}: In hallway but no reachable rooms!");
  291. return;
  292. }
  293. // MAIN LOGIC: In regular room, find a hallway exit and path to it
  294. // Get all hallway tiles adjacent to this room
  295. List<Vector2Int> hallwayExits = FindHallwayExits(currentRoomData, currentPos);
  296. if (hallwayExits.Count > 0)
  297. {
  298. // Pick a random hallway exit to move toward (commit to it)
  299. Vector2Int chosenExit = hallwayExits[Random.Range(0, hallwayExits.Count)];
  300. // Path directly to that exit
  301. currentPath = FindPathInRoom(currentPos, chosenExit, currentRoomData);
  302. currentPathIndex = 0;
  303. if (currentPath.Count > 0)
  304. {
  305. targetExitTile = chosenExit;
  306. commitedToExit = true;
  307. // Add random delay before next decision to increase variety
  308. nextRandomWait = Time.time + Random.Range(1.5f, 3.5f);
  309. Debug.Log($"AIAgent {agentId}: Moving to hallway exit at {chosenExit}, path length: {currentPath.Count}, committed");
  310. }
  311. else
  312. {
  313. Debug.LogWarning($"AIAgent {agentId}: Could not path to hallway exit");
  314. commitedToExit = false;
  315. }
  316. }
  317. else
  318. {
  319. Debug.LogWarning($"AIAgent {agentId}: No hallway exits found from room {currentRoomData.Id}");
  320. commitedToExit = false;
  321. }
  322. }
  323. /// <summary>
  324. /// Chooses the next room to move to
  325. /// Searches around the current room to find adjacent rooms
  326. /// Avoids recently visited rooms to prevent backtracking
  327. /// </summary>
  328. private MazeRoom ChooseNextRoom(MazeRoom currentRoom)
  329. {
  330. // Find all adjacent rooms by checking walkable tiles in all directions from room boundaries
  331. List<MazeRoom> connectedRooms = new();
  332. Vector2Int roomCenter = currentRoom.GetCenter();
  333. // Check from each direction outward from the room center
  334. Vector2Int[] directions = new Vector2Int[]
  335. {
  336. Vector2Int.up, Vector2Int.down, Vector2Int.left, Vector2Int.right,
  337. Vector2Int.up + Vector2Int.left, Vector2Int.up + Vector2Int.right,
  338. Vector2Int.down + Vector2Int.left, Vector2Int.down + Vector2Int.right
  339. };
  340. // Shuffle directions for randomness
  341. for (int i = directions.Length - 1; i > 0; i--)
  342. {
  343. int randomIndex = Random.Range(0, i + 1);
  344. var temp = directions[i];
  345. directions[i] = directions[randomIndex];
  346. directions[randomIndex] = temp;
  347. }
  348. // Try each direction and look for tiles that lead to other rooms
  349. foreach (var dir in directions)
  350. {
  351. for (int dist = 1; dist < 20; dist++) // Search up to 20 tiles away
  352. {
  353. Vector2Int testPos = roomCenter + (dir * dist);
  354. if (!maze.IsInBounds(testPos.x, testPos.y) || !maze.IsWalkable(testPos.x, testPos.y))
  355. continue;
  356. MazeRoom testRoom = maze.GetRoomAtTile(testPos.x, testPos.y);
  357. if (testRoom != null && testRoom.Id != currentRoom.Id && !connectedRooms.Contains(testRoom))
  358. {
  359. connectedRooms.Add(testRoom);
  360. break; // Found a room in this direction, move to next direction
  361. }
  362. }
  363. }
  364. if (connectedRooms.Count == 0)
  365. {
  366. Debug.LogWarning($"AIAgent {agentId}: No adjacent rooms found from room {currentRoom.Id}");
  367. return null;
  368. }
  369. // Filter out recently visited rooms (to avoid backtracking)
  370. List<MazeRoom> nonRecentRooms = connectedRooms.Where(r => !recentRooms.Contains(r.Id)).ToList();
  371. Debug.Log($"AIAgent {agentId}: Found {connectedRooms.Count} adjacent rooms, {nonRecentRooms.Count} non-recent");
  372. // PRIORITY 1: Strongly prefer completely unvisited rooms (not recently visited either)
  373. var completelyUnvisited = nonRecentRooms.Where(r => !roomMemory.HasVisited(r.Id)).ToList();
  374. if (completelyUnvisited.Count > 0)
  375. {
  376. MazeRoom chosen = completelyUnvisited[Random.Range(0, completelyUnvisited.Count)];
  377. Debug.Log($"AIAgent {agentId}: Choosing completely unvisited room {chosen.Id}");
  378. return chosen;
  379. }
  380. // PRIORITY 2: Try non-recent rooms even if visited by this character type
  381. if (nonRecentRooms.Count > 0)
  382. {
  383. // Among non-recent rooms, prefer unvisited by this agent's character type
  384. var unvisitedByType = nonRecentRooms.Where(r => !roomMemory.HasVisited(r.Id)).ToList();
  385. if (unvisitedByType.Count > 0)
  386. {
  387. MazeRoom chosen = unvisitedByType[Random.Range(0, unvisitedByType.Count)];
  388. Debug.Log($"AIAgent {agentId}: Choosing unvisited-by-type room {chosen.Id}");
  389. return chosen;
  390. }
  391. // Otherwise just pick a random non-recent room
  392. MazeRoom chosen2 = nonRecentRooms[Random.Range(0, nonRecentRooms.Count)];
  393. Debug.Log($"AIAgent {agentId}: Choosing non-recent room {chosen2.Id}");
  394. return chosen2;
  395. }
  396. // PRIORITY 3: If all rooms are recent, pick one that's been visited least recently
  397. // Find the room in connectedRooms that was added to recentRooms earliest (will be dequeued first)
  398. MazeRoom leastRecentRoom = connectedRooms[0];
  399. foreach (var room in connectedRooms)
  400. {
  401. if (!roomMemory.HasVisited(room.Id))
  402. {
  403. leastRecentRoom = room;
  404. break;
  405. }
  406. }
  407. Debug.Log($"AIAgent {agentId}: All rooms recent, choosing least-recent room {leastRecentRoom.Id}");
  408. return leastRecentRoom;
  409. }
  410. /// <summary>
  411. /// Finds the nearest room from a position (used when in hallway)
  412. /// </summary>
  413. private MazeRoom FindNearestRoom(Vector2Int position)
  414. {
  415. MazeRoom nearest = null;
  416. float nearestDistance = float.MaxValue;
  417. foreach (var room in maze.Rooms)
  418. {
  419. Vector2Int roomCenter = room.GetCenter();
  420. float distance = Vector2Int.Distance(position, roomCenter);
  421. if (distance < nearestDistance)
  422. {
  423. nearestDistance = distance;
  424. nearest = room;
  425. }
  426. }
  427. return nearest;
  428. }
  429. /// <summary>
  430. /// Finds all hallway exit tiles adjacent to a room
  431. /// These are walkable tiles just outside the room boundary
  432. /// </summary>
  433. private List<Vector2Int> FindHallwayExits(MazeRoom room, Vector2Int currentPos)
  434. {
  435. List<Vector2Int> exits = new();
  436. HashSet<Vector2Int> addedExits = new();
  437. // Check all tiles on the boundary of the room
  438. // North boundary
  439. for (int x = room.MinX; x <= room.MaxX; x++)
  440. {
  441. int y = room.MinY - 1;
  442. if (maze.IsInBounds(x, y) && maze.IsWalkable(x, y) && !addedExits.Contains(new Vector2Int(x, y)))
  443. {
  444. exits.Add(new Vector2Int(x, y));
  445. addedExits.Add(new Vector2Int(x, y));
  446. }
  447. }
  448. // South boundary
  449. for (int x = room.MinX; x <= room.MaxX; x++)
  450. {
  451. int y = room.MaxY + 1;
  452. if (maze.IsInBounds(x, y) && maze.IsWalkable(x, y) && !addedExits.Contains(new Vector2Int(x, y)))
  453. {
  454. exits.Add(new Vector2Int(x, y));
  455. addedExits.Add(new Vector2Int(x, y));
  456. }
  457. }
  458. // West boundary
  459. for (int y = room.MinY; y <= room.MaxY; y++)
  460. {
  461. int x = room.MinX - 1;
  462. if (maze.IsInBounds(x, y) && maze.IsWalkable(x, y) && !addedExits.Contains(new Vector2Int(x, y)))
  463. {
  464. exits.Add(new Vector2Int(x, y));
  465. addedExits.Add(new Vector2Int(x, y));
  466. }
  467. }
  468. // East boundary
  469. for (int y = room.MinY; y <= room.MaxY; y++)
  470. {
  471. int x = room.MaxX + 1;
  472. if (maze.IsInBounds(x, y) && maze.IsWalkable(x, y) && !addedExits.Contains(new Vector2Int(x, y)))
  473. {
  474. exits.Add(new Vector2Int(x, y));
  475. addedExits.Add(new Vector2Int(x, y));
  476. }
  477. }
  478. if (exits.Count > 0)
  479. Debug.Log($"AIAgent {agentId}: Found {exits.Count} hallway exits from room {room.Id}");
  480. else
  481. Debug.LogWarning($"AIAgent {agentId}: No hallway exits found from room {room.Id} (bounds: {room.MinX},{room.MinY} to {room.MaxX},{room.MaxY})");
  482. return exits;
  483. }
  484. /// <summary>
  485. /// Finds a path from current position to the nearest room (when in hallway)
  486. /// </summary>
  487. private List<Vector2Int> FindPathToNearestRoom(Vector2Int start, MazeRoom targetRoom)
  488. {
  489. // Simple pathfinding through hallways using A*
  490. var openSet = new List<Vector2Int> { start };
  491. var cameFrom = new Dictionary<Vector2Int, Vector2Int>();
  492. var gScore = new Dictionary<Vector2Int, float> { [start] = 0 };
  493. var targetCenter = targetRoom.GetCenter();
  494. var fScore = new Dictionary<Vector2Int, float> { [start] = Vector2Int.Distance(start, targetCenter) };
  495. int iterations = 0;
  496. const int maxIterations = 2000; // Increased from 500 to handle larger mazes
  497. while (openSet.Count > 0 && iterations < maxIterations)
  498. {
  499. iterations++;
  500. // Find node with lowest fScore
  501. Vector2Int current = openSet[0];
  502. float lowestF = fScore[current];
  503. for (int i = 1; i < openSet.Count; i++)
  504. {
  505. if (fScore[openSet[i]] < lowestF)
  506. {
  507. current = openSet[i];
  508. lowestF = fScore[current];
  509. }
  510. }
  511. // If we reached the target room, we're done
  512. MazeRoom currentRoom = maze.GetRoomAtTile(current.x, current.y);
  513. if (currentRoom != null && currentRoom.Id == targetRoom.Id)
  514. {
  515. return ReconstructPath(cameFrom, current);
  516. }
  517. openSet.Remove(current);
  518. // Check neighbors
  519. Vector2Int[] neighbors = new[]
  520. {
  521. new Vector2Int(current.x + 1, current.y),
  522. new Vector2Int(current.x - 1, current.y),
  523. new Vector2Int(current.x, current.y + 1),
  524. new Vector2Int(current.x, current.y - 1),
  525. };
  526. foreach (var neighbor in neighbors)
  527. {
  528. if (!maze.IsInBounds(neighbor.x, neighbor.y) || !maze.IsWalkable(neighbor.x, neighbor.y))
  529. continue;
  530. float tentativeG = gScore[current] + 1;
  531. if (!gScore.ContainsKey(neighbor) || tentativeG < gScore[neighbor])
  532. {
  533. cameFrom[neighbor] = current;
  534. gScore[neighbor] = tentativeG;
  535. fScore[neighbor] = tentativeG + Vector2Int.Distance(neighbor, targetCenter);
  536. if (!openSet.Contains(neighbor))
  537. {
  538. openSet.Add(neighbor);
  539. }
  540. }
  541. }
  542. }
  543. Debug.LogWarning($"AIAgent {agentId}: FindPathToNearestRoom FAILED after {iterations} iterations. Start: {start}, Target room {targetRoom.Id} center: {targetCenter}");
  544. return new List<Vector2Int>();
  545. }
  546. /// <summary>
  547. /// Finds a boundary tile of the current room that points toward the next room
  548. /// </summary>
  549. private Vector2Int FindBoundaryTileToward(MazeRoom currentRoom, MazeRoom nextRoom, Vector2Int currentPos)
  550. {
  551. Vector2Int nextRoomCenter = nextRoom.GetCenter();
  552. Vector2Int closestBoundary = currentRoom.GetCenter();
  553. float closestDistance = float.MaxValue;
  554. // Check all boundary tiles of current room
  555. for (int x = currentRoom.MinX; x <= currentRoom.MaxX; x++)
  556. {
  557. for (int y = currentRoom.MinY; y <= currentRoom.MaxY; y++)
  558. {
  559. // Only check boundary and walkable tiles
  560. if ((x == currentRoom.MinX || x == currentRoom.MaxX || y == currentRoom.MinY || y == currentRoom.MaxY) &&
  561. maze.IsWalkable(x, y))
  562. {
  563. // Find boundary tile closest to next room center
  564. float distToNext = Vector2Int.Distance(new Vector2Int(x, y), nextRoomCenter);
  565. if (distToNext < closestDistance)
  566. {
  567. closestDistance = distToNext;
  568. closestBoundary = new Vector2Int(x, y);
  569. }
  570. }
  571. }
  572. }
  573. return closestBoundary;
  574. }
  575. /// <summary>
  576. /// Finds a path within a single room (limited knowledge)
  577. /// </summary>
  578. private List<Vector2Int> FindPathInRoom(Vector2Int start, Vector2Int goal, MazeRoom room)
  579. {
  580. // Use simple A* within room bounds
  581. var openSet = new List<Vector2Int> { start };
  582. var cameFrom = new Dictionary<Vector2Int, Vector2Int>();
  583. var gScore = new Dictionary<Vector2Int, float> { [start] = 0 };
  584. var fScore = new Dictionary<Vector2Int, float> { [start] = Vector2Int.Distance(start, goal) };
  585. int iterations = 0;
  586. const int maxIterations = 1000;
  587. while (openSet.Count > 0 && iterations < maxIterations)
  588. {
  589. iterations++;
  590. // Find node with lowest fScore
  591. Vector2Int current = openSet[0];
  592. float lowestF = fScore[current];
  593. for (int i = 1; i < openSet.Count; i++)
  594. {
  595. if (fScore[openSet[i]] < lowestF)
  596. {
  597. current = openSet[i];
  598. lowestF = fScore[current];
  599. }
  600. }
  601. if (current == goal)
  602. {
  603. return ReconstructPath(cameFrom, current);
  604. }
  605. openSet.Remove(current);
  606. // Check only neighbors within the room
  607. var neighbors = GetRoomNeighbors(current, room);
  608. foreach (var neighbor in neighbors)
  609. {
  610. float tentativeG = gScore[current] + 1;
  611. if (!gScore.ContainsKey(neighbor) || tentativeG < gScore[neighbor])
  612. {
  613. cameFrom[neighbor] = current;
  614. gScore[neighbor] = tentativeG;
  615. fScore[neighbor] = tentativeG + Vector2Int.Distance(neighbor, goal);
  616. if (!openSet.Contains(neighbor))
  617. {
  618. openSet.Add(neighbor);
  619. }
  620. }
  621. }
  622. }
  623. return new List<Vector2Int>();
  624. }
  625. /// <summary>
  626. /// Gets walkable neighbors within a room or at room boundary
  627. /// Allows pathfinding to reach hallway exits outside room
  628. /// </summary>
  629. private List<Vector2Int> GetRoomNeighbors(Vector2Int position, MazeRoom room)
  630. {
  631. var neighbors = new List<Vector2Int>();
  632. Vector2Int[] directions = new[]
  633. {
  634. new Vector2Int(position.x + 1, position.y),
  635. new Vector2Int(position.x - 1, position.y),
  636. new Vector2Int(position.x, position.y + 1),
  637. new Vector2Int(position.x, position.y - 1),
  638. };
  639. foreach (var dir in directions)
  640. {
  641. // Allow tiles within room OR immediately adjacent to room (boundary)
  642. bool inBounds = maze.IsInBounds(dir.x, dir.y);
  643. bool isWalkable = maze.IsWalkable(dir.x, dir.y);
  644. bool inRoom = room.Contains(dir.x, dir.y);
  645. bool nearBoundary = (dir.x == room.MinX - 1 || dir.x == room.MaxX + 1 ||
  646. dir.y == room.MinY - 1 || dir.y == room.MaxY + 1);
  647. if (inBounds && isWalkable && (inRoom || nearBoundary))
  648. {
  649. neighbors.Add(dir);
  650. }
  651. }
  652. return neighbors;
  653. }
  654. /// <summary>
  655. /// Reconstructs path from A* results
  656. /// </summary>
  657. private List<Vector2Int> ReconstructPath(Dictionary<Vector2Int, Vector2Int> cameFrom, Vector2Int current)
  658. {
  659. var path = new List<Vector2Int> { current };
  660. while (cameFrom.ContainsKey(current))
  661. {
  662. current = cameFrom[current];
  663. path.Insert(0, current);
  664. }
  665. return path;
  666. }
  667. /// <summary>
  668. /// Follows the current path
  669. /// </summary>
  670. private void FollowPath()
  671. {
  672. if (currentPath.Count == 0) return;
  673. Vector2Int currentTarget = currentPath[currentPathIndex];
  674. // Maze coordinates: X,Y → World: X,Z (Y=0 is ground level)
  675. // Keep Y at 1f to stay above the maze floor
  676. Vector3 targetWorldPos = new Vector3(currentTarget.x + 0.5f, 1f, currentTarget.y + 0.5f);
  677. Vector3 direction = (targetWorldPos - transform.position).normalized;
  678. // Move directly instead of using Translate (no rigidbody)
  679. transform.position += direction * actualMovementSpeed * Time.deltaTime;
  680. // Check if we've exited the room (entered hallway)
  681. Vector2Int currentPos = WorldToTile(transform.position);
  682. MazeRoom roomAtPos = maze.GetRoomAtTile(currentPos.x, currentPos.y);
  683. if (roomAtPos == null && currentRoom.x != -1)
  684. {
  685. // We've entered a hallway - track which room we came from to prevent immediate backtracking
  686. lastRoomExitedFrom = maze.Rooms.FirstOrDefault(r => r.Id == currentRoom.x);
  687. commitedToExit = false; // No longer committed to that exit, now in hallway
  688. targetExitTile = Vector2Int.zero; // Clear the target exit
  689. Debug.Log($"AIAgent {agentId}: Exited room {currentRoom.x} into hallway");
  690. }
  691. // Move to next waypoint when close enough
  692. if (Vector3.Distance(transform.position, targetWorldPos) < stoppingDistance)
  693. {
  694. currentPathIndex++;
  695. if (currentPathIndex >= currentPath.Count)
  696. {
  697. Debug.Log($"AIAgent {agentId}: Reached path end");
  698. currentPath.Clear();
  699. }
  700. }
  701. }
  702. /// <summary>
  703. /// Updates the path visualization
  704. /// </summary>
  705. private void UpdatePathVisualization()
  706. {
  707. if (currentPath.Count == 0)
  708. {
  709. pathRenderer.positionCount = 0;
  710. return;
  711. }
  712. var positions = new Vector3[currentPath.Count];
  713. for (int i = 0; i < currentPath.Count; i++)
  714. {
  715. // Maze coordinates: X,Y → World: X,Z (Y=0 is ground level)
  716. // Keep Y at 1f to stay above the maze floor
  717. positions[i] = new Vector3(currentPath[i].x + 0.5f, 1f, currentPath[i].y + 0.5f);
  718. }
  719. pathRenderer.positionCount = positions.Length;
  720. pathRenderer.SetPositions(positions);
  721. }
  722. /// <summary>
  723. /// Converts world position to tile coordinate
  724. /// Maze coordinates: X,Y ← World: X,Z (Y=0 is ground level)
  725. /// </summary>
  726. private Vector2Int WorldToTile(Vector3 worldPos)
  727. {
  728. return new Vector2Int(Mathf.FloorToInt(worldPos.x), Mathf.FloorToInt(worldPos.z));
  729. }
  730. /// <summary>
  731. /// Gets agent ID
  732. /// </summary>
  733. public int AgentId => agentId;
  734. /// <summary>
  735. /// Gets agent character type
  736. /// </summary>
  737. public string CharacterType => agentCharacterType;
  738. /// <summary>
  739. /// Gets current room
  740. /// </summary>
  741. public Vector2Int CurrentRoom => currentRoom;
  742. /// <summary>
  743. /// Gets room memory
  744. /// </summary>
  745. public AIRoomMemory RoomMemory => roomMemory;
  746. /// <summary>
  747. /// Gets agent ID (public accessor for triggers)
  748. /// </summary>
  749. public int GetAgentId() => agentId;
  750. /// <summary>
  751. /// Called by ExitRoomTrigger when agent enters the goal room
  752. /// Immediately stops all movement
  753. /// </summary>
  754. public void StopAtGoal()
  755. {
  756. hasReachedGoal = true;
  757. currentPath.Clear();
  758. currentPathIndex = 0;
  759. commitedToExit = false;
  760. Debug.Log($"AIAgent {agentId}: STOPPED by exit room trigger!");
  761. }
  762. }