class AStar(object): def __init__(self, dimension=None, start_coords=None, end_coords=None, max_cost=config.PATHFIND_LIMIT): self.dimension = dimension self.grid = dimension.grid self.start_node = PathNode(start_coords) self.goal_node = PathNode(end_coords) self.gridspace = GridSpace(self.grid) self.max_cost = max_cost self.path = None self.closed_set = set() goal_state = self.gridspace.get_state_coords(end_coords) if goal_state.can_stand or goal_state.can_hold: self.open_heap = [self.start_node] self.open_set = set([self.start_node]) else: self.open_heap = [] self.open_set = set([]) self.start_node.set_score(0, self.heuristic_cost_estimate(self.start_node, self.goal_node)) self.iter_count = 0 self.t_start = time.time() def reconstruct_path(self, current): nodes = [] nodes.append(current) while current.parent is not None: nodes.append(current.parent) current = current.parent nodes.reverse() return nodes def get_edge_cost(self, node_from, node_to): return config.COST_DIRECT def neighbours(self, node): for state in self.gridspace.neighbours_of(node.coords): if state.coords not in self.closed_set: yield PathNode(state.coords) def heuristic_cost_estimate(self, start, goal): adx = abs(start.coords.x - goal.coords.x) adz = abs(start.coords.z - goal.coords.z) h_diagonal = min(adx, adz) h_straight = adx + adz h = config.COST_DIAGONAL * h_diagonal + config.COST_DIRECT * (h_straight - 2 * h_diagonal) return h def next(self): self.iter_count += 1 if not self.open_set: log.msg("time consumed %s sec, made %d iterations" % (time.time() - self.t_start, self.iter_count)) log.err("Did not find path between %s and %s" % (self.start_node.coords, self.goal_node.coords)) raise StopIteration() x = heapq.heappop(self.open_heap) if x == self.goal_node: self.path = Path(dimension=self.dimension, nodes=self.reconstruct_path(x)) self.gridspace = None log.msg( "finished in %s sec, length %d, made %d iterations" % (time.time() - self.t_start, len(self.path.nodes), self.iter_count) ) log.msg("nodes %s" % self.path.nodes) raise StopIteration() self.open_set.remove(x) self.closed_set.add(x.coords) for y in self.neighbours(x): if y.coords in self.closed_set: continue tentative_g_core = x.g + self.get_edge_cost(x, y) if y not in self.open_set or tentative_g_core < y.g: y.set_score(tentative_g_core, self.heuristic_cost_estimate(y, self.goal_node)) y.parent = x if y not in self.open_set: heapq.heappush(self.open_heap, y) self.open_set.add(y) if y.step > self.max_cost: log.err( "Finding path over limit between %s and %s" % (self.start_node.coords, self.goal_node.coords) ) raise StopIteration()
class AStar(object): #TODO: explore limiting by execution time rather than by path distance def __init__(self, dimension=None, start_coords=None, end_coords=None, path_max=config.PATHFIND_MAX, estimate=True): self.t_start = time.time() self.dimension = dimension self.grid = dimension.grid self.start_node = PathNode(start_coords) self.goal_node = PathNode(end_coords) self.gridspace = GridSpace(self.grid) # keep max_cost between the configured values conf_max, conf_min = config.PATHFIND_MAX, config.PATHFIND_MIN values = [conf_min, path_max, conf_max] self.max_cost = list(sorted(values))[1] self.path = None self.closed_set = set() goal_state = self.gridspace.get_state_coords(end_coords) if goal_state.can_stand or goal_state.can_hold or estimate: self.open_heap = [self.start_node] self.open_set = set([self.start_node]) else: self.open_heap = [] self.open_set = set([]) self.start_node.set_score(0, self.heuristic_cost_estimate( self.start_node, self.goal_node)) self.iter_count = 0 self.best = self.start_node self.estimate = estimate self.excessive = config.PATHFIND_EXEC_TIME_LIMIT self.distance = self.start_node.coords.distance(self.goal_node.coords) self.start = time.time() def get_edge_cost(self, node_from, node_to, x_neighbors=None, y_neighbors=None): return config.COST_DIRECT def neighbours(self, node): for state in self.gridspace.neighbours_of(node.coords): if state.coords not in self.closed_set: yield PathNode(state.coords) def heuristic_cost_estimate(self, start, goal): """Takes a path node, and tries to estimate the cost to the goal.""" # y = start.coords.y - goal.coords.y # adx = abs(start.coords.x - goal.coords.x) # adz = abs(start.coords.z - goal.coords.z) # # fall, rise = (abs(y), 0) if y < 0 else (0, abs(y)) # h_diagonal = min(adx, adz) # h_straight = adx + adz # h = (config.COST_DIAGONAL * h_diagonal + # config.COST_DIRECT * (h_straight - 2 * h_diagonal) + # config.COST_FALL * fall + # config.COST_JUMP * rise) vertical = start.coords.y - goal.coords.y if vertical < 0: vertical = abs(vertical) * config.COST_FALL else: vertical = vertical * config.COST_JUMP distance = start.coords.distance(goal.coords) h = distance + vertical return h def _excessive_path(self, start): """Cheap evalutation of whether this path is excessive, only usable on a scored node.""" # Pathfinding is the most expensive operation in a tick. # self.excessive should ideally be less than 1/20th of a second, but # can realistically go higher. # this means the pathfinding effectiveness of the bot is affected by # the speed of the machine it's on -- and by it's cost. return time.time() - self.start > self.excessive # excessive = start.step > self.distance * self.excessive # if excessive: # debug and log.msg(msg % (start.h, start.g)) # return excessive def report(self): nodes = '' if not self.path: path = '<PATH NOT FOUND>' else: estimated = '' if self.path.estimated else "(estimated)" path = 'path length %s %s' % (self.best.step, estimated) nodes = 'Nodes: %s' % self.path.nodes msg = "Finished in %s sec, %s iterations, %s" log.msg(msg % (time.time() - self.t_start, self.iter_count, path)) debug and log.msg(nodes) def finish(self): estimated = self.best.coords != self.goal_node.coords if not estimated or (estimated and self.estimate): self.path = Path(dimension=self.dimension, nodes=self.best.path, estimated=estimated) self.gridspace = None self.report() def next(self): self.iter_count += 1 if not self.open_set: self.finish() raise StopIteration() x = heapq.heappop(self.open_heap) if x.coords == self.goal_node.coords: self.best = x self.finish() raise StopIteration() self.open_set.remove(x) self.closed_set.add(x.coords) x_neighbours = self.neighbours(x) for y in x_neighbours: if y.coords in self.closed_set: continue tentative_g_core = x.g + self.get_edge_cost(x, y, x_neighbours) if y not in self.open_set or tentative_g_core < y.g: y.set_score(tentative_g_core, self.heuristic_cost_estimate(y, self.goal_node)) if y.h < self.best.h: self.best = y y.parent = x if y not in self.open_set: heapq.heappush(self.open_heap, y) self.open_set.add(y) if self._excessive_path(y): msg = "Find path timed out at %s steps between %s and %s" log.msg(msg % (y.step, self.start_node.coords, self.goal_node.coords)) self.finish() raise StopIteration() if y.step > self.max_cost: msg = "Find path over limit %s between %s and %s" log.msg(msg % (self.max_cost, self.start_node.coords, self.goal_node.coords)) self.finish() raise StopIteration()