From 79ce3cc3eccd481196e934d6d1332a64d79cda41 Mon Sep 17 00:00:00 2001 From: Wojciech Kubicki Date: Fri, 26 Apr 2024 14:43:39 +0200 Subject: [PATCH] feat: automate tractor to find path using A* while avoiding vegetable tiles --- src/field.py | 3 +- src/images/question.jpg | Bin 0 -> 7907 bytes src/tile.py | 23 ++-- src/tractor.py | 235 ++++++++++++++++++++++++++-------------- 4 files changed, 170 insertions(+), 91 deletions(-) create mode 100644 src/images/question.jpg diff --git a/src/field.py b/src/field.py index 387d9d37..4c5cc9fd 100644 --- a/src/field.py +++ b/src/field.py @@ -5,8 +5,9 @@ from tractor import Tractor class Field: def __init__(self): self.tiles = pygame.sprite.Group() + # TODO: enable resizing field grid from 16x16 to any size for x in range(256): - self.tiles.add(Tile(x, 'grass', self)) + self.tiles.add(Tile(x, self)) self.tractor = Tractor(self) diff --git a/src/images/question.jpg b/src/images/question.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1e6fee6a6963aa5863d3d1761b2465f4b4a49dff GIT binary patch literal 7907 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 0: self.rect.y -= TILE_SIZE - self.log_info() + # self.log_info() elif self.direction == "south" and self.rect.y < 15 * TILE_SIZE: self.rect.y += TILE_SIZE - self.log_info() + # self.log_info() elif self.direction == "west" and self.rect.x > 0: self.rect.x -= TILE_SIZE - self.log_info() + # self.log_info() elif self.direction == "east" and self.rect.x < 15 * TILE_SIZE: self.rect.x += TILE_SIZE - self.log_info() + # self.log_info() def update(self): - keys = pygame.key.get_pressed() - if keys[pygame.K_LEFT]: - self.rotate('left') - if keys[pygame.K_RIGHT]: - self.rotate('right') - if keys[pygame.K_UP]: - self.move() - if keys[pygame.K_r]: - self.water = 50 - print(f"💧 replenished water level: {self.water} litres\n") + if self.action_index == len(self.actions): + return + + action = self.actions[self.action_index] + match (action): + case ('move'): + self.move() + case ('left'): + self.rotate('left') + case ('right'): + self.rotate('right') + self.action_index += 1 + + return def log_info(self): @@ -71,12 +90,9 @@ class Tractor(pygame.sprite.Sprite): print(f"❗ {water_needed - self.water} more litres of water needed to water {current_tile.type}") # print out what are the neighbors of the current tile and their effect on growth - neighbors = self.get_neighbors_list() + neighbors = self.get_neighbors_types() modifier = multi_sasiedzi(current_tile.type, neighbors)[0]['Mul'] print(f"🌱 the growth modifier for {current_tile.type} on this tile is ~{modifier:.2f} based on its neighbors: {', '.join(neighbors)}") - - self.BFS((14,14)) - print() # empty line at end of log statement @@ -87,7 +103,18 @@ class Tractor(pygame.sprite.Sprite): return current_tile - def get_neighbors_list(self): + def cost_of_entering_node(self, coordinates: tuple[int, int]) -> int: + x, y = coordinates + cost: int + match (self.field.tiles.sprites()[y * 16 + x].type): + case ('grass'): + cost = 1 + case _: + cost = 100 + return cost + + + def get_neighbors_types(self) -> list: x = self.rect.x // TILE_SIZE y = self.rect.y // TILE_SIZE neighbors = [] @@ -140,70 +167,114 @@ class Tractor(pygame.sprite.Sprite): self.image = pygame.transform.scale(self.image, (TILE_SIZE, TILE_SIZE)) self.direction = 'north' - def turn_right(self, direction): - directions = ["east", "south", "west", "north"] - current_index = directions.index(direction) - new_index = (current_index + 1) % 4 - return directions[new_index] - def turn_left(self, direction): - directions = ["east", "south", "west", "north"] - current_index = directions.index(direction) - new_index = (current_index - 1) % 4 - return directions[new_index] +# https://www.redblobgames.com/pathfinding/a-star/implementation.html + def a_star(self): + fringe: list[tuple[int, tuple[int, int]]] = [] + heapq.heappush(fringe, (0, self.start)) + came_from: dict[tuple[int, int], Optional[tuple[int, int]]] = {} + cost_so_far: dict[tuple[int, int], int] = {} + came_from[self.start] = None + cost_so_far[self.start] = 0 - def generate_succesors(self, state): - x, y, direction = state - successors = [] - - if direction == "east" and x < 15: - successors.append(((x + 1, y, direction), "forward")) - elif direction == "west" and x > 0: - successors.append(((x - 1, y, direction), "forward")) - elif direction == "north" and y > 0: - successors.append(((x, y - 1, direction), "forward")) - elif direction == "south" and y < 15: - successors.append(((x, y + 1, direction), "forward")) - - if direction == "east" and y > 0: - successors.append(((x, y, self.turn_left(direction)), "left")) - elif direction == "west" and y < 15: - successors.append(((x, y, self.turn_left(direction)), "left")) - elif direction == "north" and x > 0: - successors.append(((x, y, self.turn_left(direction)), "left")) - elif direction == "south" and x < 15: - successors.append(((x, y, self.turn_left(direction)), "left")) - - if direction == "east" and y < 15: - successors.append(((x, y, self.turn_right(direction)), "right")) - elif direction == "west" and y > 0: - successors.append(((x, y, self.turn_right(direction)), "right")) - elif direction == "north" and x < 15: - successors.append(((x, y, self.turn_right(direction)), "right")) - elif direction == "south" and x > 0: - successors.append(((x, y, self.turn_right(direction)), "right")) - - return successors - - - def BFS(self, end): - x = self.rect.x // TILE_SIZE - y = self.rect.y // TILE_SIZE - start = (x, y, self.direction) - - fringe = deque() - path = [] - fringe.append(start) while fringe: - if (fringe[0])[0] == end[0] and (fringe[0])[1] == end[1]: - return path - successors = self.generate_succesors(fringe[0]) - print(fringe[0]) - print(successors, '<-----tutaj następniki') - break - + current: tuple[int, int] = heapq.heappop(fringe)[1] + + if current == self.final: + break + + # next_node: tuple[int, int] + for next_node in self.neighboring_nodes(coordinates=current): + enter_cost = self.cost_of_entering_node(coordinates=next_node) + new_cost: int = cost_so_far[current] + enter_cost + if next_node not in cost_so_far or new_cost < cost_so_far[next_node]: + cost_so_far[next_node] = new_cost + priority = new_cost + self.manhattan_cost(current) + heapq.heappush(fringe, (priority, next_node)) + came_from[next_node] = current + + return came_from, cost_so_far - + def manhattan_cost(self, coordinates: tuple[int, int]) -> int: + current_x, current_y = coordinates + final_x, final_y = self.final + return abs(current_x - final_x) + abs(current_y - final_y) + def neighboring_nodes(self, coordinates: tuple[int, int]): + x, y = coordinates + neighbors = [] + + # nodes appended clockwise: up, right, bottom, left + if y < 15: + neighbors.append((x, y+1)) + if x < 15: + neighbors.append((x+1, y)) + if y > 0: + neighbors.append((x, y-1)) + if x > 0: + neighbors.append((x-1, y)) + + return neighbors + + + def reconstruct_path(self, came_from: dict[tuple[int, int], tuple[int, int]]) -> list[tuple[int, int]]: + current: tuple[int, int] = self.final + path: list[tuple[int, int]] = [] + if self.final not in came_from: # no path was found + return [] + while current != self.start: + path.append(current) + current = came_from[current] + path.append(self.start) + path.reverse() + return path + + + def recreate_actions(self, path: list[tuple[int, int]]) -> list[str]: + actions: list[str] = [] + agent_direction = self.direction + + for i in range(len(path) - 1): + x, y = path[i] + next_x, next_y = path[i+1] + + # find out which way the tractor should be facing to move onto next_node tile + proper_direction: str + if x > next_x: + proper_direction = 'west' + elif x < next_x: + proper_direction = 'east' + elif y > next_y: + proper_direction = 'north' + else: # y < next_y + proper_direction = 'south' + + # find the fastest way to rotate to correct direction + if agent_direction != proper_direction: + match (agent_direction, proper_direction): + case ('north', 'east'): + actions.append('right') + case ('north', 'west'): + actions.append('left') + case ('east', 'south'): + actions.append('right') + case ('east', 'north'): + actions.append('left') + case ('south', 'west'): + actions.append('right') + case ('south', 'east'): + actions.append('left') + case ('west', 'north'): + actions.append('right') + case ('west', 'south'): + actions.append('left') + case _: + actions.append('right') + actions.append('right') + + agent_direction = proper_direction + actions.append('move') + + return actions