using System.Collections.Generic;
using System.Linq;
namespace MazeGen
{
///
/// Picks an exit and moves through it. This is the catch-all decision that always
/// executes if no other decision takes precedence. It handles:
/// - Choosing between explore/follow/backtrack exits
/// - Filtering by threat level
/// - Moving through the chosen exit
///
public class MoveOnDecision : AgentDecision
{
private MazeGenerator _generator;
private MazeAgentController _controller;
private float _exploreChance;
private float _retreatThreshold;
public override string DecisionName => "Move On";
public MoveOnDecision(MazeGenerator generator, MazeAgentController controller, float exploreChance, float retreatThreshold)
{
_generator = generator;
_controller = controller;
_exploreChance = exploreChance;
_retreatThreshold = retreatThreshold;
}
public override bool CanExecute(MazeAgent agent)
{
// Always can execute, but only if no other decision consumed the turn
return agent != null && agent.CurrentRoom != null;
}
public override bool Execute(MazeAgent agent)
{
Room room = agent.CurrentRoom;
Exit choice = ChooseExit(agent, room);
if (choice == null)
{
// No usable door - waiting
return false;
}
// Try to use the exit (generates room if needed)
_controller.TryUseExit(choice);
return true;
}
public override void Think(MazeAgent agent)
{
Room room = agent.CurrentRoom;
Exit choice = ChooseExit(agent, room);
if (choice == null)
{
agent.Thoughts.Add(ThoughtKind.Move, $"No usable door out of room {room.Id} - waiting.");
return;
}
string heading = CompassName(choice.DirectionDegrees);
Room known = choice.ConnectedRoomId.HasValue ? _generator.Graph.Get(choice.ConnectedRoomId.Value) : null;
string destination = known != null
? $"room {known.Id}{(agent.HasVisited(known.Id) ? " (been there)" : " (new to me)")}"
: "whatever is behind it (never opened)";
agent.Thoughts.Add(ThoughtKind.Move, $"Taking the {heading} door into {destination}.");
}
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;
if (!exit.IsUsed)
{
explore.Add(exit);
continue;
}
bool leadsToVisited = exit.ConnectedRoomId.HasValue && agent.HasVisited(exit.ConnectedRoomId.Value);
(leadsToVisited ? backtrack : follow).Add(exit);
}
// Filter by threat
explore = FilterByThreat(agent, explore);
follow = FilterByThreat(agent, follow);
backtrack = FilterByThreat(agent, backtrack);
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);
return reverseExit;
}
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, System.Collections.Generic.List backtrack)
{
var promising = backtrack.Where(e => LeadsTowardUnexploredOptions(agent, e)).ToList();
return LeastVisitedPick(agent, promising.Count > 0 ? promising : backtrack);
}
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, System.Collections.Generic.List exits)
{
int bestVisits = int.MaxValue;
var bestExits = new System.Collections.Generic.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 System.Collections.Generic.List FilterByThreat(MazeAgent agent, System.Collections.Generic.List exits)
{
var safeExits = new System.Collections.Generic.List();
foreach (var exit in exits)
{
if (ShouldTakeExit(agent, exit))
safeExits.Add(exit);
}
return safeExits.Count > 0 ? safeExits : exits;
}
private bool ShouldTakeExit(MazeAgent agent, Exit exit)
{
if (!exit.ConnectedRoomId.HasValue)
return true;
Room nextRoom = _generator.Graph.Get(exit.ConnectedRoomId.Value);
if (nextRoom == null || nextRoom.Encounter == null || !nextRoom.Encounter.HasLivingMonsters)
return true;
float perceivedCR = nextRoom.Encounter.GetPerceivedChallengeRating(agent.CharacterSheet);
float effectiveThreshold = GetEffectiveReteatThreshold(agent);
if (perceivedCR <= effectiveThreshold)
return true;
float retreatChance = UnityEngine.Mathf.Clamp01((perceivedCR - effectiveThreshold) / 2.0f);
return UnityEngine.Random.value > retreatChance;
}
private static Exit RandomPick(System.Collections.Generic.List exits)
=> exits[UnityEngine.Random.Range(0, exits.Count)];
private static string CompassName(float directionDegrees)
{
int normalized = UnityEngine.Mathf.RoundToInt(UnityEngine.Mathf.Repeat(directionDegrees, 360f) / 90f) % 4;
switch (normalized)
{
case 0: return "east";
case 1: return "north";
case 2: return "west";
default: return "south";
}
}
private float GetEffectiveReteatThreshold(MazeAgent agent)
{
CharacterSheet sheet = agent.CharacterSheet;
float maxHealth = sheet.Stats.MaxHealth;
if (maxHealth <= 0) return _retreatThreshold;
float healthPercent = sheet.Stats.CurrentHealth / maxHealth;
if (healthPercent < 0.3f)
return _retreatThreshold * 0.5f;
else if (healthPercent < 0.5f)
{
float t = (healthPercent - 0.3f) / 0.2f;
return UnityEngine.Mathf.Lerp(_retreatThreshold * 0.5f, _retreatThreshold, t);
}
return _retreatThreshold;
}
}
}