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.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;
}
}
}