feat: automate tractor to find path using A* while avoiding vegetable tiles

This commit is contained in:
Wojciech Kubicki 2024-04-26 14:43:39 +02:00
parent ac6a8771e6
commit 79ce3cc3ec
4 changed files with 170 additions and 91 deletions

View File

@ -5,8 +5,9 @@ from tractor import Tractor
class Field: class Field:
def __init__(self): def __init__(self):
self.tiles = pygame.sprite.Group() self.tiles = pygame.sprite.Group()
# TODO: enable resizing field grid from 16x16 to any size
for x in range(256): for x in range(256):
self.tiles.add(Tile(x, 'grass', self)) self.tiles.add(Tile(x, self))
self.tractor = Tractor(self) self.tractor = Tractor(self)

BIN
src/images/question.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

@ -7,7 +7,7 @@ from config import TILE_SIZE
class Tile(pygame.sprite.Sprite): class Tile(pygame.sprite.Sprite):
def __init__(self, id, field, type): def __init__(self, id, field):
super().__init__() super().__init__()
self.id = id self.id = id
x = id%16 x = id%16
@ -15,9 +15,13 @@ class Tile(pygame.sprite.Sprite):
self.field = field self.field = field
vegetables = tractor_kb.query(pl.Expr("warzywo(Nazwa_warzywa)")) # temporary solution to have vegetables act as obstacles
random_vegetable = vegetables[random.randint(0, len(vegetables)-1)]['Nazwa_warzywa'] if random.randint(1, 10) % 3 == 0:
self.set_type(random_vegetable) vegetables = tractor_kb.query(pl.Expr("warzywo(Nazwa_warzywa)"))
random_vegetable = vegetables[random.randint(0, len(vegetables)-1)]['Nazwa_warzywa']
self.set_type(random_vegetable)
else:
self.set_type('grass')
self.faza = 'posadzono' self.faza = 'posadzono'
@ -30,9 +34,12 @@ class Tile(pygame.sprite.Sprite):
def set_type(self, type): def set_type(self, type):
self.type = type self.type = type
image_path = f"images/vegetables/{self.type}.png" if self.type == 'grass':
if os.path.exists(image_path): image_path = "images/grass.png"
self.image = pygame.image.load(image_path).convert()
else: else:
self.image = pygame.image.load("images/grass.png").convert() image_path = f"images/vegetables/{self.type}.png"
if not os.path.exists(image_path):
image_path = "images/question.jpg"
self.image = pygame.image.load(image_path).convert()
self.image = pygame.transform.scale(self.image, (TILE_SIZE, TILE_SIZE)) self.image = pygame.transform.scale(self.image, (TILE_SIZE, TILE_SIZE))

View File

@ -4,6 +4,8 @@ from kb import ile_podlac, multi_sasiedzi
from tile import Tile from tile import Tile
from config import TILE_SIZE from config import TILE_SIZE
from collections import deque from collections import deque
import heapq
import random
class Tractor(pygame.sprite.Sprite): class Tractor(pygame.sprite.Sprite):
def __init__(self, field): def __init__(self, field):
@ -15,43 +17,60 @@ class Tractor(pygame.sprite.Sprite):
self.rect = self.image.get_rect() self.rect = self.image.get_rect()
self.direction = 'east' self.direction = 'east'
# TODO: enable tractor to start on other tile than (0,0)
x, y = 0, 0 self.start = (0, 0)
self.rect.topleft = (x, y) self.final = (8, 14)
print('final target @', self.final[0], self.final[1])
self.rect.topleft = (0, 0)
self.water = 50 self.water = 50
came_from, total_cost = self.a_star()
path = self.reconstruct_path(came_from)
self.actions = self.recreate_actions(path)
self.action_index = 0
def draw(self, surface): def draw(self, surface):
surface.blit(self.image, self.rect) surface.blit(self.image, self.rect)
def get_coordinates(self):
x = self.rect.x // TILE_SIZE
y = self.rect.y // TILE_SIZE
return (x,y)
def move(self): def move(self):
if self.direction == "north" and self.rect.y > 0: if self.direction == "north" and self.rect.y > 0:
self.rect.y -= TILE_SIZE self.rect.y -= TILE_SIZE
self.log_info() # self.log_info()
elif self.direction == "south" and self.rect.y < 15 * TILE_SIZE: elif self.direction == "south" and self.rect.y < 15 * TILE_SIZE:
self.rect.y += TILE_SIZE self.rect.y += TILE_SIZE
self.log_info() # self.log_info()
elif self.direction == "west" and self.rect.x > 0: elif self.direction == "west" and self.rect.x > 0:
self.rect.x -= TILE_SIZE self.rect.x -= TILE_SIZE
self.log_info() # self.log_info()
elif self.direction == "east" and self.rect.x < 15 * TILE_SIZE: elif self.direction == "east" and self.rect.x < 15 * TILE_SIZE:
self.rect.x += TILE_SIZE self.rect.x += TILE_SIZE
self.log_info() # self.log_info()
def update(self): def update(self):
keys = pygame.key.get_pressed() if self.action_index == len(self.actions):
if keys[pygame.K_LEFT]: return
self.rotate('left')
if keys[pygame.K_RIGHT]: action = self.actions[self.action_index]
self.rotate('right') match (action):
if keys[pygame.K_UP]: case ('move'):
self.move() self.move()
if keys[pygame.K_r]: case ('left'):
self.water = 50 self.rotate('left')
print(f"💧 replenished water level: {self.water} litres\n") case ('right'):
self.rotate('right')
self.action_index += 1
return
def log_info(self): 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(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 # 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'] 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)}") 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 print() # empty line at end of log statement
@ -87,7 +103,18 @@ class Tractor(pygame.sprite.Sprite):
return current_tile 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 x = self.rect.x // TILE_SIZE
y = self.rect.y // TILE_SIZE y = self.rect.y // TILE_SIZE
neighbors = [] neighbors = []
@ -140,70 +167,114 @@ class Tractor(pygame.sprite.Sprite):
self.image = pygame.transform.scale(self.image, (TILE_SIZE, TILE_SIZE)) self.image = pygame.transform.scale(self.image, (TILE_SIZE, TILE_SIZE))
self.direction = 'north' 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): # https://www.redblobgames.com/pathfinding/a-star/implementation.html
directions = ["east", "south", "west", "north"] def a_star(self):
current_index = directions.index(direction) fringe: list[tuple[int, tuple[int, int]]] = []
new_index = (current_index - 1) % 4 heapq.heappush(fringe, (0, self.start))
return directions[new_index] 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: while fringe:
if (fringe[0])[0] == end[0] and (fringe[0])[1] == end[1]: current: tuple[int, int] = heapq.heappop(fringe)[1]
return path
successors = self.generate_succesors(fringe[0]) if current == self.final:
print(fringe[0]) break
print(successors, '<-----tutaj następniki')
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