|
1 | 1 | using System; |
2 | 2 | using System.Collections.Generic; |
3 | 3 | using System.Linq; |
4 | | -using System.Text; |
| 4 | +using System.Threading; |
| 5 | +using System.Threading.Tasks; |
5 | 6 |
|
6 | 7 | namespace MinecraftClient.Mapping |
7 | 8 | { |
@@ -129,62 +130,120 @@ public static Queue<Location> Move2Steps(Location start, Location goal, ref doub |
129 | 130 | /// <param name="start">Start location</param> |
130 | 131 | /// <param name="goal">Destination location</param> |
131 | 132 | /// <param name="allowUnsafe">Allow possible but unsafe locations</param> |
| 133 | + /// <param name="maxOffset">If no valid path can be found, also allow locations within specified distance of destination</param> |
| 134 | + /// <param name="minOffset">Do not get closer of destination than specified distance</param> |
| 135 | + /// <param name="timeout">How long to wait before stopping computation</param> |
| 136 | + /// <remarks>When location is unreachable, computation will reach timeout, then optionally fallback to a close location within maxOffset</remarks> |
132 | 137 | /// <returns>A list of locations, or null if calculation failed</returns> |
133 | | - public static Queue<Location> CalculatePath(World world, Location start, Location goal, bool allowUnsafe = false) |
| 138 | + public static Queue<Location> CalculatePath(World world, Location start, Location goal, bool allowUnsafe, int maxOffset, int minOffset, TimeSpan timeout) |
134 | 139 | { |
135 | | - Queue<Location> result = null; |
| 140 | + CancellationTokenSource cts = new CancellationTokenSource(); |
| 141 | + Task<Queue<Location>> pathfindingTask = Task.Factory.StartNew(() => Movement.CalculatePath(world, start, goal, allowUnsafe, maxOffset, minOffset, cts.Token)); |
| 142 | + pathfindingTask.Wait(timeout); |
| 143 | + if (!pathfindingTask.IsCompleted) |
| 144 | + { |
| 145 | + cts.Cancel(); |
| 146 | + pathfindingTask.Wait(); |
| 147 | + } |
| 148 | + return pathfindingTask.Result; |
| 149 | + } |
| 150 | + |
| 151 | + /// <summary> |
| 152 | + /// Calculate a path from the start location to the destination location |
| 153 | + /// </summary> |
| 154 | + /// <remarks> |
| 155 | + /// Based on the A* pathfinding algorithm described on Wikipedia |
| 156 | + /// </remarks> |
| 157 | + /// <see href="https://en.wikipedia.org/wiki/A*_search_algorithm#Pseudocode"/> |
| 158 | + /// <param name="start">Start location</param> |
| 159 | + /// <param name="goal">Destination location</param> |
| 160 | + /// <param name="allowUnsafe">Allow possible but unsafe locations</param> |
| 161 | + /// <param name="maxOffset">If no valid path can be found, also allow locations within specified distance of destination</param> |
| 162 | + /// <param name="minOffset">Do not get closer of destination than specified distance</param> |
| 163 | + /// <param name="ct">Token for stopping computation after a certain time</param> |
| 164 | + /// <returns>A list of locations, or null if calculation failed</returns> |
| 165 | + public static Queue<Location> CalculatePath(World world, Location start, Location goal, bool allowUnsafe, int maxOffset, int minOffset, CancellationToken ct) |
| 166 | + { |
| 167 | + |
| 168 | + if (minOffset > maxOffset) |
| 169 | + throw new ArgumentException("minOffset must be lower or equal to maxOffset", "minOffset"); |
| 170 | + |
| 171 | + Location current = new Location(); // Location that is currently processed |
| 172 | + Location closestGoal = new Location(); // Closest Location to the goal. Used for approaching if goal can not be reached or was not found. |
| 173 | + HashSet<Location> ClosedSet = new HashSet<Location>(); // The set of locations already evaluated. |
| 174 | + HashSet<Location> OpenSet = new HashSet<Location>(new[] { start }); // The set of tentative nodes to be evaluated, initially containing the start node |
| 175 | + Dictionary<Location, Location> Came_From = new Dictionary<Location, Location>(); // The map of navigated nodes. |
| 176 | + |
| 177 | + Dictionary<Location, int> g_score = new Dictionary<Location, int>(); //:= map with default value of Infinity |
| 178 | + g_score[start] = 0; // Cost from start along best known path. |
| 179 | + // Estimated total cost from start to goal through y. |
| 180 | + Dictionary<Location, int> f_score = new Dictionary<Location, int>(); //:= map with default value of Infinity |
| 181 | + f_score[start] = (int)start.DistanceSquared(goal); //heuristic_cost_estimate(start, goal) |
136 | 182 |
|
137 | | - AutoTimeout.Perform(() => |
| 183 | + while (OpenSet.Count > 0) |
138 | 184 | { |
139 | | - HashSet<Location> ClosedSet = new HashSet<Location>(); // The set of locations already evaluated. |
140 | | - HashSet<Location> OpenSet = new HashSet<Location>(new[] { start }); // The set of tentative nodes to be evaluated, initially containing the start node |
141 | | - Dictionary<Location, Location> Came_From = new Dictionary<Location, Location>(); // The map of navigated nodes. |
| 185 | + current = //the node in OpenSet having the lowest f_score[] value |
| 186 | + OpenSet.Select(location => f_score.ContainsKey(location) |
| 187 | + ? new KeyValuePair<Location, int>(location, f_score[location]) |
| 188 | + : new KeyValuePair<Location, int>(location, int.MaxValue)) |
| 189 | + .OrderBy(pair => pair.Value).First().Key; |
142 | 190 |
|
143 | | - Dictionary<Location, int> g_score = new Dictionary<Location, int>(); //:= map with default value of Infinity |
144 | | - g_score[start] = 0; // Cost from start along best known path. |
145 | | - // Estimated total cost from start to goal through y. |
146 | | - Dictionary<Location, int> f_score = new Dictionary<Location, int>(); //:= map with default value of Infinity |
147 | | - f_score[start] = (int)start.DistanceSquared(goal); //heuristic_cost_estimate(start, goal) |
| 191 | + // Only assert a value if it is of actual use later |
| 192 | + if (maxOffset > 0 && ClosedSet.Count > 0) |
| 193 | + // Get the block that currently is closest to the goal |
| 194 | + closestGoal = ClosedSet.OrderBy(checkedLocation => checkedLocation.DistanceSquared(goal)).First(); |
148 | 195 |
|
149 | | - while (OpenSet.Count > 0) |
| 196 | + // Stop when goal is reached or we are close enough |
| 197 | + if (current == goal || (minOffset > 0 && current.DistanceSquared(goal) <= minOffset)) |
| 198 | + return ReconstructPath(Came_From, current); |
| 199 | + else if (ct.IsCancellationRequested) |
| 200 | + break; // Return if we are cancelled |
| 201 | + |
| 202 | + OpenSet.Remove(current); |
| 203 | + ClosedSet.Add(current); |
| 204 | + |
| 205 | + foreach (Location neighbor in GetAvailableMoves(world, current, allowUnsafe)) |
150 | 206 | { |
151 | | - Location current = //the node in OpenSet having the lowest f_score[] value |
152 | | - OpenSet.Select(location => f_score.ContainsKey(location) |
153 | | - ? new KeyValuePair<Location, int>(location, f_score[location]) |
154 | | - : new KeyValuePair<Location, int>(location, int.MaxValue)) |
155 | | - .OrderBy(pair => pair.Value).First().Key; |
156 | | - if (current == goal) |
157 | | - { //reconstruct_path(Came_From, goal) |
158 | | - List<Location> total_path = new List<Location>(new[] { current }); |
159 | | - while (Came_From.ContainsKey(current)) |
160 | | - { |
161 | | - current = Came_From[current]; |
162 | | - total_path.Add(current); |
163 | | - } |
164 | | - total_path.Reverse(); |
165 | | - result = new Queue<Location>(total_path); |
166 | | - } |
167 | | - OpenSet.Remove(current); |
168 | | - ClosedSet.Add(current); |
169 | | - foreach (Location neighbor in GetAvailableMoves(world, current, allowUnsafe)) |
170 | | - { |
171 | | - if (ClosedSet.Contains(neighbor)) |
172 | | - continue; // Ignore the neighbor which is already evaluated. |
173 | | - int tentative_g_score = g_score[current] + (int)current.DistanceSquared(neighbor); //dist_between(current,neighbor) // length of this path. |
174 | | - if (!OpenSet.Contains(neighbor)) // Discover a new node |
175 | | - OpenSet.Add(neighbor); |
176 | | - else if (tentative_g_score >= g_score[neighbor]) |
177 | | - continue; // This is not a better path. |
| 207 | + if (ct.IsCancellationRequested) |
| 208 | + break; // Stop searching for blocks if we are cancelled. |
| 209 | + if (ClosedSet.Contains(neighbor)) |
| 210 | + continue; // Ignore the neighbor which is already evaluated. |
| 211 | + int tentative_g_score = g_score[current] + (int)current.DistanceSquared(neighbor); //dist_between(current,neighbor) // length of this path. |
| 212 | + if (!OpenSet.Contains(neighbor)) // Discover a new node |
| 213 | + OpenSet.Add(neighbor); |
| 214 | + else if (tentative_g_score >= g_score[neighbor]) |
| 215 | + continue; // This is not a better path. |
178 | 216 |
|
179 | | - // This path is the best until now. Record it! |
180 | | - Came_From[neighbor] = current; |
181 | | - g_score[neighbor] = tentative_g_score; |
182 | | - f_score[neighbor] = g_score[neighbor] + (int)neighbor.DistanceSquared(goal); //heuristic_cost_estimate(neighbor, goal) |
183 | | - } |
| 217 | + // This path is the best until now. Record it! |
| 218 | + Came_From[neighbor] = current; |
| 219 | + g_score[neighbor] = tentative_g_score; |
| 220 | + f_score[neighbor] = g_score[neighbor] + (int)neighbor.DistanceSquared(goal); //heuristic_cost_estimate(neighbor, goal) |
184 | 221 | } |
185 | | - }, TimeSpan.FromSeconds(5)); |
| 222 | + } |
186 | 223 |
|
187 | | - return result; |
| 224 | + // Goal could not be reached. Set the path to the closest location if close enough |
| 225 | + if (maxOffset == int.MaxValue || goal.DistanceSquared(closestGoal) <= maxOffset) |
| 226 | + return ReconstructPath(Came_From, closestGoal); |
| 227 | + else |
| 228 | + return null; |
| 229 | + } |
| 230 | + |
| 231 | + /// <summary> |
| 232 | + /// Helper function for CalculatePath(). Backtrack from goal to start to reconstruct a step-by-step path. |
| 233 | + /// </summary> |
| 234 | + /// <param name="Came_From">The collection of Locations that leads back to the start</param> |
| 235 | + /// <param name="current">Endpoint of our later walk</param> |
| 236 | + /// <returns>the path that leads to current from the start position</returns> |
| 237 | + private static Queue<Location> ReconstructPath(Dictionary<Location, Location> Came_From, Location current) |
| 238 | + { |
| 239 | + List<Location> total_path = new List<Location>(new[] { current }); |
| 240 | + while (Came_From.ContainsKey(current)) |
| 241 | + { |
| 242 | + current = Came_From[current]; |
| 243 | + total_path.Add(current); |
| 244 | + } |
| 245 | + total_path.Reverse(); |
| 246 | + return new Queue<Location>(total_path); |
188 | 247 | } |
189 | 248 |
|
190 | 249 | /* ========= LOCATION PROPERTIES ========= */ |
|
0 commit comments