| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234 |
- using System.Collections.Generic;
- using System.Linq;
- namespace MazeGen
- {
- /// <summary>
- /// 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
- /// </summary>
- 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<Exit>();
- var follow = new List<Exit>();
- var backtrack = new List<Exit>();
- 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<Exit> 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<Exit> exits)
- {
- int bestVisits = int.MaxValue;
- var bestExits = new System.Collections.Generic.List<Exit>();
- 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<Exit> FilterByThreat(MazeAgent agent, System.Collections.Generic.List<Exit> exits)
- {
- var safeExits = new System.Collections.Generic.List<Exit>();
- 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<Exit> 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;
- }
- }
- }
|