feat: automate tractor to find path using A* while avoiding vegetable tiles
This commit is contained in:
parent
ac6a8771e6
commit
79ce3cc3ec
@ -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
BIN
src/images/question.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.7 KiB |
23
src/tile.py
23
src/tile.py
@ -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))
|
||||||
|
235
src/tractor.py
235
src/tractor.py
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user