MoveOnDecision.cs 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. using System.Collections.Generic;
  2. using System.Linq;
  3. namespace MazeGen
  4. {
  5. /// <summary>
  6. /// Picks an exit and moves through it. This is the catch-all decision that always
  7. /// executes if no other decision takes precedence. It handles:
  8. /// - Choosing between explore/follow/backtrack exits
  9. /// - Filtering by threat level
  10. /// - Moving through the chosen exit
  11. /// </summary>
  12. public class MoveOnDecision : AgentDecision
  13. {
  14. private MazeGenerator _generator;
  15. private MazeAgentController _controller;
  16. private float _exploreChance;
  17. private float _retreatThreshold;
  18. public override string DecisionName => "Move On";
  19. public MoveOnDecision(MazeGenerator generator, MazeAgentController controller, float exploreChance, float retreatThreshold)
  20. {
  21. _generator = generator;
  22. _controller = controller;
  23. _exploreChance = exploreChance;
  24. _retreatThreshold = retreatThreshold;
  25. }
  26. public override bool CanExecute(MazeAgent agent)
  27. {
  28. // Always can execute, but only if no other decision consumed the turn
  29. return agent != null && agent.CurrentRoom != null;
  30. }
  31. public override bool Execute(MazeAgent agent)
  32. {
  33. Room room = agent.CurrentRoom;
  34. Exit choice = ChooseExit(agent, room);
  35. if (choice == null)
  36. {
  37. // No usable door - waiting
  38. return false;
  39. }
  40. // Try to use the exit (generates room if needed)
  41. _controller.TryUseExit(choice);
  42. return true;
  43. }
  44. public override void Think(MazeAgent agent)
  45. {
  46. Room room = agent.CurrentRoom;
  47. Exit choice = ChooseExit(agent, room);
  48. if (choice == null)
  49. {
  50. agent.Thoughts.Add(ThoughtKind.Move, $"No usable door out of room {room.Id} - waiting.");
  51. return;
  52. }
  53. string heading = CompassName(choice.DirectionDegrees);
  54. Room known = choice.ConnectedRoomId.HasValue ? _generator.Graph.Get(choice.ConnectedRoomId.Value) : null;
  55. string destination = known != null
  56. ? $"room {known.Id}{(agent.HasVisited(known.Id) ? " (been there)" : " (new to me)")}"
  57. : "whatever is behind it (never opened)";
  58. agent.Thoughts.Add(ThoughtKind.Move, $"Taking the {heading} door into {destination}.");
  59. }
  60. private Exit ChooseExit(MazeAgent agent, Room room)
  61. {
  62. Exit reverseExit = FindReverseExit(agent, room);
  63. var explore = new List<Exit>();
  64. var follow = new List<Exit>();
  65. var backtrack = new List<Exit>();
  66. foreach (var exit in room.Exits)
  67. {
  68. if (exit == reverseExit) continue;
  69. if (exit.IsBlocked) continue;
  70. if (!exit.IsUsed)
  71. {
  72. explore.Add(exit);
  73. continue;
  74. }
  75. bool leadsToVisited = exit.ConnectedRoomId.HasValue && agent.HasVisited(exit.ConnectedRoomId.Value);
  76. (leadsToVisited ? backtrack : follow).Add(exit);
  77. }
  78. // Filter by threat
  79. explore = FilterByThreat(agent, explore);
  80. follow = FilterByThreat(agent, follow);
  81. backtrack = FilterByThreat(agent, backtrack);
  82. if (explore.Count > 0 && UnityEngine.Random.value < _exploreChance)
  83. return RandomPick(explore);
  84. if (follow.Count > 0)
  85. return LeastVisitedPick(agent, follow);
  86. if (explore.Count > 0)
  87. return RandomPick(explore);
  88. if (backtrack.Count > 0)
  89. return ChooseBacktrack(agent, backtrack);
  90. return reverseExit;
  91. }
  92. private static Exit FindReverseExit(MazeAgent agent, Room room)
  93. {
  94. int? reverseExitId = agent.LastUsedExit?.ConnectedExitId;
  95. if (!reverseExitId.HasValue) return null;
  96. return room.Exits.Find(e => e.Id == reverseExitId.Value);
  97. }
  98. private Exit ChooseBacktrack(MazeAgent agent, System.Collections.Generic.List<Exit> backtrack)
  99. {
  100. var promising = backtrack.Where(e => LeadsTowardUnexploredOptions(agent, e)).ToList();
  101. return LeastVisitedPick(agent, promising.Count > 0 ? promising : backtrack);
  102. }
  103. private bool LeadsTowardUnexploredOptions(MazeAgent agent, Exit exit)
  104. {
  105. if (!exit.ConnectedRoomId.HasValue) return false;
  106. Room target = _generator.Graph.Get(exit.ConnectedRoomId.Value);
  107. if (target == null) return false;
  108. int reverseExitId = exit.ConnectedExitId ?? -1;
  109. foreach (var candidate in target.Exits)
  110. {
  111. if (candidate.Id == reverseExitId) continue;
  112. if (!candidate.IsUsed) return true;
  113. if (candidate.ConnectedRoomId.HasValue && !agent.HasVisited(candidate.ConnectedRoomId.Value)) return true;
  114. }
  115. return false;
  116. }
  117. private static Exit LeastVisitedPick(MazeAgent agent, System.Collections.Generic.List<Exit> exits)
  118. {
  119. int bestVisits = int.MaxValue;
  120. var bestExits = new System.Collections.Generic.List<Exit>();
  121. foreach (var exit in exits)
  122. {
  123. int visits = exit.ConnectedRoomId.HasValue ? agent.VisitCount(exit.ConnectedRoomId.Value) : 0;
  124. if (visits < bestVisits)
  125. {
  126. bestVisits = visits;
  127. bestExits.Clear();
  128. bestExits.Add(exit);
  129. }
  130. else if (visits == bestVisits)
  131. {
  132. bestExits.Add(exit);
  133. }
  134. }
  135. return RandomPick(bestExits);
  136. }
  137. private System.Collections.Generic.List<Exit> FilterByThreat(MazeAgent agent, System.Collections.Generic.List<Exit> exits)
  138. {
  139. var safeExits = new System.Collections.Generic.List<Exit>();
  140. foreach (var exit in exits)
  141. {
  142. if (ShouldTakeExit(agent, exit))
  143. safeExits.Add(exit);
  144. }
  145. return safeExits.Count > 0 ? safeExits : exits;
  146. }
  147. private bool ShouldTakeExit(MazeAgent agent, Exit exit)
  148. {
  149. if (!exit.ConnectedRoomId.HasValue)
  150. return true;
  151. Room nextRoom = _generator.Graph.Get(exit.ConnectedRoomId.Value);
  152. if (nextRoom == null || nextRoom.Encounter == null || !nextRoom.Encounter.HasLivingMonsters)
  153. return true;
  154. float perceivedCR = nextRoom.Encounter.GetPerceivedChallengeRating(agent.CharacterSheet);
  155. float effectiveThreshold = GetEffectiveReteatThreshold(agent);
  156. if (perceivedCR <= effectiveThreshold)
  157. return true;
  158. float retreatChance = UnityEngine.Mathf.Clamp01((perceivedCR - effectiveThreshold) / 2.0f);
  159. return UnityEngine.Random.value > retreatChance;
  160. }
  161. private static Exit RandomPick(System.Collections.Generic.List<Exit> exits)
  162. => exits[UnityEngine.Random.Range(0, exits.Count)];
  163. private static string CompassName(float directionDegrees)
  164. {
  165. int normalized = UnityEngine.Mathf.RoundToInt(UnityEngine.Mathf.Repeat(directionDegrees, 360f) / 90f) % 4;
  166. switch (normalized)
  167. {
  168. case 0: return "east";
  169. case 1: return "north";
  170. case 2: return "west";
  171. default: return "south";
  172. }
  173. }
  174. private float GetEffectiveReteatThreshold(MazeAgent agent)
  175. {
  176. CharacterSheet sheet = agent.CharacterSheet;
  177. float maxHealth = sheet.Stats.MaxHealth;
  178. if (maxHealth <= 0) return _retreatThreshold;
  179. float healthPercent = sheet.Stats.CurrentHealth / maxHealth;
  180. if (healthPercent < 0.3f)
  181. return _retreatThreshold * 0.5f;
  182. else if (healthPercent < 0.5f)
  183. {
  184. float t = (healthPercent - 0.3f) / 0.2f;
  185. return UnityEngine.Mathf.Lerp(_retreatThreshold * 0.5f, _retreatThreshold, t);
  186. }
  187. return _retreatThreshold;
  188. }
  189. }
  190. }