using System; using System.Collections.Generic; using System.Linq; using UnityEngine; namespace MazeGen { /// /// Drives one agent's autonomous exploration: waits out a (randomized) initial delay so /// many agents don't all decide in lockstep, then ticks a steady decision interval - /// each time it fires, picks an exit and asks the MazeAgentController to use it. Stops /// deciding once the agent reaches an exit room or the level has ended. /// /// Agents can "peek" into adjacent rooms before entering to see if there are monsters. /// Based on the perceived threat level, they may choose to retreat rather than fight. /// public class AutonomousAgentBrain : MonoBehaviour { private MazeAgentController _controller; private MazeGenerator _generator; private float _decisionInterval; private float _exploreChance; private float _retreatThreshold = 1.5f; // CR above this = retreat (configurable) private Func _isLevelEnded; private float _timer; private Dictionary _peekCount = new Dictionary(); // Track how many times agent peeked at a room public void Initialize(MazeAgentController controller, MazeGenerator generator, float initialDelay, float decisionInterval, float exploreChance, float retreatThreshold, Func isLevelEnded) { _controller = controller; _generator = generator; _decisionInterval = Mathf.Max(0.05f, decisionInterval); _exploreChance = Mathf.Clamp01(exploreChance); _retreatThreshold = Mathf.Max(0, retreatThreshold); _isLevelEnded = isLevelEnded; _timer = Mathf.Max(0f, initialDelay); } private void Update() { if (_controller?.Agent == null || _controller.Agent.IsFinished || _controller.Agent.IsDead) return; if (_isLevelEnded != null && _isLevelEnded()) return; _timer -= Time.deltaTime; if (_timer > 0f) return; _timer = _decisionInterval; Exit choice = ChooseExit(_controller.Agent, _controller.Agent.CurrentRoom); if (choice != null) _controller.TryUseExit(choice); } /// /// Picks the next exit to take. Strongly prefers an unused exit (fresh, unexplored /// territory); failing that, prefers a used exit into a room this agent personally /// hasn't visited yet (following where another agent has already been); only /// backtracks into a room it's already visited as a last resort. `exploreChance` /// adds some randomness so a "follow" opportunity is occasionally taken even when /// exploring is available, rather than being 100% deterministic. /// /// A few anti-stuck rules keep this from looping forever around a small ring of /// already-explored rooms (e.g. a square formed by a loop-closing connection): /// - The door leading straight back to the room the agent just came from is excluded /// from consideration unless it's the only door available at all, so agents don't /// ping-pong between two rooms. /// - When forced to backtrack, exits that "peek" into a room with its own unexplored /// option beyond it (an unused exit, or one leading somewhere this agent hasn't /// personally seen) are preferred over ones that only lead deeper into fully-picked- /// over territory - this steers the agent back out of a dead loop instead of just /// circling it. /// - Ties are broken toward whichever room this agent has personally visited the /// fewest times, so a small loop gets cycled through rather than bounced around. /// /// Also evaluates monster threats in adjacent rooms - if a room looks too dangerous, /// the agent will retreat (choose a different exit) if possible. /// private Exit ChooseExit(MazeAgent agent, Room room) { Exit reverseExit = FindReverseExit(agent, room); var explore = new List(); var follow = new List(); var backtrack = new List(); foreach (var exit in room.Exits) { if (exit == reverseExit) continue; if (exit.IsBlocked) continue; // Skip exits that are permanently unusable if (!exit.IsUsed) { explore.Add(exit); continue; } bool leadsToVisited = exit.ConnectedRoomId.HasValue && agent.HasVisited(exit.ConnectedRoomId.Value); (leadsToVisited ? backtrack : follow).Add(exit); } // Filter exits by threat level - remove dangerous ones if safer options exist explore = FilterByThreat(explore, agent); follow = FilterByThreat(follow, agent); backtrack = FilterByThreat(backtrack, agent); if (explore.Count > 0 && UnityEngine.Random.value < _exploreChance) return RandomPick(explore); if (follow.Count > 0) return LeastVisitedPick(agent, follow); if (explore.Count > 0) return RandomPick(explore); if (backtrack.Count > 0) return ChooseBacktrack(agent, backtrack); // Excluding the reverse door left nothing - it's genuinely the only way out // (e.g. a single-exit dead end), so take it (even if dangerous). return reverseExit; } /// The door in `room` that leads straight back to wherever the agent's last move came from, if any. private static Exit FindReverseExit(MazeAgent agent, Room room) { int? reverseExitId = agent.LastUsedExit?.ConnectedExitId; if (!reverseExitId.HasValue) return null; return room.Exits.Find(e => e.Id == reverseExitId.Value); } private Exit ChooseBacktrack(MazeAgent agent, List backtrack) { var promising = backtrack.Where(e => LeadsTowardUnexploredOptions(agent, e)).ToList(); return LeastVisitedPick(agent, promising.Count > 0 ? promising : backtrack); } /// /// One-step lookahead: does the room beyond `exit` itself have any door (other than /// the one leading straight back here) that's still unused or leads somewhere this /// agent hasn't personally been? If so, backtracking this way still has somewhere /// useful to go; if not, it just leads deeper into already-picked-over territory. /// private bool LeadsTowardUnexploredOptions(MazeAgent agent, Exit exit) { if (!exit.ConnectedRoomId.HasValue) return false; Room target = _generator.Graph.Get(exit.ConnectedRoomId.Value); if (target == null) return false; int reverseExitId = exit.ConnectedExitId ?? -1; foreach (var candidate in target.Exits) { if (candidate.Id == reverseExitId) continue; if (!candidate.IsUsed) return true; if (candidate.ConnectedRoomId.HasValue && !agent.HasVisited(candidate.ConnectedRoomId.Value)) return true; } return false; } private static Exit LeastVisitedPick(MazeAgent agent, List exits) { int bestVisits = int.MaxValue; var bestExits = new List(); foreach (var exit in exits) { int visits = exit.ConnectedRoomId.HasValue ? agent.VisitCount(exit.ConnectedRoomId.Value) : 0; if (visits < bestVisits) { bestVisits = visits; bestExits.Clear(); bestExits.Add(exit); } else if (visits == bestVisits) { bestExits.Add(exit); } } return RandomPick(bestExits); } private static Exit RandomPick(List exits) => exits[UnityEngine.Random.Range(0, exits.Count)]; /// /// Peek into a room (via an exit) to see if there are monsters and how many. /// Returns the perceived challenge rating, or 0 if no monsters/already explored. /// Agent sees incomplete info (60-100% of actual monsters, doesn't know about hidden ones). /// public float PeekThroughExit(Exit exit, MazeAgent agent) { if (!exit.ConnectedRoomId.HasValue) return 0; // Unresolved exit - can't peek yet Room nextRoom = _generator.Graph.Get(exit.ConnectedRoomId.Value); if (nextRoom == null || nextRoom.Encounter == null) return 0; // No encounter in this room return nextRoom.Encounter.GetPerceivedChallengeRating(agent.CharacterSheet); } /// /// Decide if an exit is worth taking based on threat level inside. /// Returns true if the exit is relatively safe or if the agent has no choice, /// false if the agent decides to retreat from this exit. /// private bool ShouldTakeExit(Exit exit, MazeAgent agent) { if (!exit.ConnectedRoomId.HasValue) return true; // Unresolved exit - proceed cautiously Room nextRoom = _generator.Graph.Get(exit.ConnectedRoomId.Value); if (nextRoom == null || nextRoom.Encounter == null) return true; // No encounter, safe to enter // Peek and evaluate threat float perceivedCR = nextRoom.Encounter.GetPerceivedChallengeRating(agent.CharacterSheet); // If threat is below threshold, take it. If above, retreat with some randomness. if (perceivedCR <= _retreatThreshold) return true; // Manageable threat // Higher difficulty = more likely to retreat, but add randomness for personality float retreatChance = Mathf.Clamp01((perceivedCR - _retreatThreshold) / 2.0f); return UnityEngine.Random.value > retreatChance; } /// /// Filter exits: remove those the agent wants to retreat from due to high threat. /// This is called during exit selection so dangerous rooms are avoided when possible. /// private List FilterByThreat(List exits, MazeAgent agent) { var safeExits = new List(); foreach (var exit in exits) { if (ShouldTakeExit(exit, agent)) safeExits.Add(exit); } // If all exits are dangerous, agent must take one anyway (don't get stuck) return safeExits.Count > 0 ? safeExits : exits; } } }