def test_longest_path_to_tail(self): m = Map(6, 6) m.create_food(Pos(4, 4)) s = Snake(m, Direc.RIGHT, [Pos(1, 3), Pos(1, 2), Pos(1, 1)], [PointType.HEAD_R, PointType.BODY_HOR, PointType.BODY_HOR]) solver = PathSolver(s) # noinspection PyUnusedLocal act_path = solver.longest_path_to_tail() act_path = solver.longest_path_to_tail() # Check idempotency expect_path = [ Direc.RIGHT, Direc.DOWN, Direc.DOWN, Direc.DOWN, Direc.LEFT, Direc.LEFT, Direc.LEFT, Direc.UP, Direc.RIGHT, Direc.RIGHT, Direc.UP, Direc.LEFT, Direc.LEFT, Direc.UP ] assert m.point(s.tail()).type == PointType.BODY_HOR assert len(act_path) == len(expect_path) for i, direc in enumerate(act_path): assert direc == expect_path[i] # Empty path because the tail has not been 'emptied' yet, # so it is not a valid position to be added to the queue. # This means that after all the evaluations, None of them check the snake tail # Because it is not valid, according to the function, since # it is part of the body. # Therefore, we pass the path finders through a function called path_to # to remove the 'tail' at that location so it can be evaluated. assert not solver.longest_path_to(s.tail())
def __init__(self, snake, shortcuts=True): if snake.map.num_rows % 2 != 0 or snake.map.num_cols % 2 != 0: raise ValueError('num_rows and num_cols must be even.') super().__init__(snake) self.__shortcuts = shortcuts self.__path_solver = PathSolver(snake) self.__table = [[_TableCell() for _ in range(snake.map.num_cols)] for _ in range(snake.map.num_rows)] self.__build_cycle()
class GreedySolver(BaseSolver): """ A greedy little snake that only seeks to eat food. smh. """ def __init__(self, snake): super().__init__(snake) self.__path_solver = PathSolver(snake) def next_direc(self): """ Get the next direction to move in. :return: A direction of type Direc. """ # Clone the snake. s_copy, m_copy = self.snake.copy() # Step 1: Get the path to the food. If path 1 exists, move to step 2. # Otherwise, move to step 4. self.__path_solver.snake = self.snake # That's my snake you're looking at! path_to_food = self.__path_solver.shortest_path_to_food() if path_to_food: # Step 2: Make a virtual snake to eat the food along the path. s_copy.move_path(path_to_food) if m_copy.is_full(): return path_to_food[0] # Step 3: Calculate the longest path from head to tail after eating food. # If that longest path exists, then move along that path. # Otherwise, go to step 4. self.__path_solver.snake = s_copy path_to_tail = self.__path_solver.longest_path_to_tail() if len(path_to_tail) > 1: return path_to_food[0] # Step 4: Calculate the longest path from head to tail. # Remember, there is no path to the food right now that will guarantee our survival. # Therefore, we do one full loop, and then check again. # If that path exists, then move along that path. # Else, move to step 5. self.__path_solver.snake = self.snake path_to_tail = self.__path_solver.longest_path_to_tail() if len(path_to_tail) > 1: return path_to_tail[0] # Step 5: RUN AWAY! No, seriously, get as far away as you can from the food. head = self.snake.head() direc, max_dist = self.snake.direc, -1 for adj in head.all_adj(): if self.map.is_safe(adj): dist = Pos.manhattan_distance(adj, self.map.food) if dist > max_dist: max_dist = dist direc = head.direction_to(adj) return direc
def test_shortest(): m = Map(7, 7) m.create_food(Pos(5, 5)) s = Snake(m, Direc.RIGHT, [Pos(2, 3), Pos(2, 2), Pos(2, 1)], [PointType.HEAD_R, PointType.BODY_HOR, PointType.BODY_HOR]) solver = PathSolver(s) act_path = solver.shortest_path_to_food() act_path = solver.shortest_path_to_food() # Check idempotence expect_path = [ Direc.RIGHT, Direc.RIGHT, Direc.DOWN, Direc.DOWN, Direc.DOWN ] assert len(act_path) == len(expect_path) for i, direc in enumerate(act_path): assert direc == expect_path[i] assert solver.table[5][1].dist == 5 assert solver.table[5][1].dist == solver.table[5][5].dist # Empty path assert not solver.shortest_path_to(s.tail())
def test_longest(): m = Map(6, 6) m.create_food(Pos(4, 4)) s = Snake(m, Direc.RIGHT, [Pos(1, 3), Pos(1, 2), Pos(1, 1)], [PointType.HEAD_R, PointType.BODY_HOR, PointType.BODY_HOR]) solver = PathSolver(s) act_path = solver.longest_path_to_tail() act_path = solver.longest_path_to_tail() # Check idempotence expect_path = [ Direc.RIGHT, Direc.DOWN, Direc.DOWN, Direc.DOWN, Direc.LEFT, Direc.LEFT, Direc.LEFT, Direc.UP, Direc.RIGHT, Direc.RIGHT, Direc.UP, Direc.LEFT, Direc.LEFT, Direc.UP ] assert m.point(s.tail()).type == PointType.BODY_HOR assert len(act_path) == len(expect_path) for i, direc in enumerate(act_path): assert direc == expect_path[i] # Empty path assert not solver.longest_path_to(s.tail())
def test_shortest_path_to_food(self): m = Map(7, 7) m.create_food(Pos(5, 5)) s = Snake(m, Direc.RIGHT, [Pos(2, 3), Pos(2, 2), Pos(2, 1)], [PointType.HEAD_R, PointType.BODY_HOR, PointType.BODY_HOR]) solver = PathSolver(s) # noinspection PyUnusedLocal act_path = solver.shortest_path_to_food() act_path = solver.shortest_path_to_food() # Check idempotency # Idempotency is pretty much checking for side effects. # You're checking whether or not the algorithm will produce the same result if it is run multiple times. # We do this multiple times to verify that the table has been properly reset. expect_path = [ Direc.RIGHT, Direc.RIGHT, Direc.DOWN, Direc.DOWN, Direc.DOWN ] assert len(act_path) == len(expect_path) for i, direc in enumerate(act_path): assert direc == expect_path[i] assert solver.table[5][1].dist == 5 assert solver.table[5][1].dist == solver.table[5][5].dist # Empty path assert not solver.shortest_path_to(s.tail())
def __init__(self, snake): super().__init__(snake) self.__path_solver = PathSolver(snake)
class HamiltonSolver(BaseSolver): """ A snake called Hamilton. It goes around in big, big circles. """ def __init__(self, snake, shortcuts=True): if snake.map.num_rows % 2 != 0 or snake.map.num_cols % 2 != 0: raise ValueError('num_rows and num_cols must be even.') super().__init__(snake) self.__shortcuts = shortcuts self.__path_solver = PathSolver(snake) self.__table = [[_TableCell() for _ in range(snake.map.num_cols)] for _ in range(snake.map.num_rows)] self.__build_cycle() @property def table(self): """ :return: The poor table cells, all bunched up and forced to be numbers and strings. """ return self.__table def next_direc(self): """ Gets the next direction, taking shortcuts if it won't disrupt the cycle. :return: The next direction to go in of type Direc. """ head = self.snake.head() nxt_direc = self.__table[head.x][head.y].direc # We should take shortcuts if the snake isn't too long, to speed up gameplay. if self.__shortcuts and self.snake.len() < 0.5 * self.map.capacity: path = self.__path_solver.shortest_path_to_food() # Check if there is a path from the head to the food. if path: tail, nxt, food = self.snake.tail(), head.adj(path[0]), self.map.food tail_idx = self.__table[tail.x][tail.y].idx # Get the location of the tail on the hamiltonian cycle. head_idx = self.__table[head.x][head.y].idx # Get the location of the head on the hamiltonian cycle. nxt_idx = self.__table[nxt.x][nxt.y].idx # Get the location of the next move, # if Hamilton were to follow the shortest path to the food. food_idx = self.__table[food.x][food.y].idx # Get the location of the food on the hamiltonian cycle. if not (len(path) == 1 and abs(food_idx - tail_idx) == 1): # Make sure that either the path does not lead to instant death. # This is because if it takes exactly one move to get to the food, # and the tail is next on the hamiltonian cycle, # the tail will "freeze" and so the head, which will continue to follow the hamiltonian cycle # (remember, it does not have any self-preserving capabilities like the greedy solver), # will smash the tail and will result in certain death. head_idx_rel = self.__relative_dst(tail_idx, head_idx, self.map.capacity) nxt_idx_rel = self.__relative_dst(tail_idx, nxt_idx, self.map.capacity) food_idx_rel = self.__relative_dst(tail_idx, food_idx, self.map.capacity) if head_idx_rel < nxt_idx_rel <= food_idx_rel: # Average 1786.16 for 100 tests. # if head_idx < nxt_idx <= food_idx: # Fails quite often. Still trying to figure out why. # If the next move suggested by the bfs solver would jump the head closer to the food, # and wouldn't overshoot the food, then go ahead. nxt_direc = path[0] return nxt_direc def __build_cycle(self): """ Build a hamiltonian cycle on the map. 0 is the head, and capacity of the map is the end of the cycle. :return: Void. """ path = self.__path_solver.longest_path_to_tail() # The longest path to the tail, assuming that the map is square and even, # guarantees that the path will pass through every single point on the map. cur, cnt = self.snake.head(), 0 for direc in path: self.__table[cur.x][cur.y].idx = cnt self.__table[cur.x][cur.y].direc = direc cur = cur.adj(direc) cnt += 1 tail = self.snake.tail() self.__table[tail.x][tail.y].idx = cnt self.__table[tail.x][tail.y].direc = self.snake.direc # With this we have a cycle that's composed of the whole grid. # It might seem a little boring, but eh. @staticmethod def __relative_dst(tail, x, size): """ Gets the distance from tail to x on the hamiltonian cycle. :param tail: The position of the tail on the hamiltonian cycle of type Integer. :param x: An index on the hamiltonian cycle of type integer. :param size: The length of the cycle. This is so that if the tail is ahead of x at some point, we can establish the total number of steps needed to get from tail to x following the hamiltonian cycle. :return: Relative distance from the tail to the location of type Integer. """ if tail > x: x += size return x - tail