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