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:
|
||||
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)
|
||||
|
||||
|
||||
|
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):
|
||||
|
||||
def __init__(self, id, field, type):
|
||||
def __init__(self, id, field):
|
||||
super().__init__()
|
||||
self.id = id
|
||||
x = id%16
|
||||
@ -15,9 +15,13 @@ class Tile(pygame.sprite.Sprite):
|
||||
|
||||
self.field = field
|
||||
|
||||
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)
|
||||
# temporary solution to have vegetables act as obstacles
|
||||
if random.randint(1, 10) % 3 == 0:
|
||||
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'
|
||||
|
||||
@ -30,9 +34,12 @@ class Tile(pygame.sprite.Sprite):
|
||||
|
||||
def set_type(self, type):
|
||||
self.type = type
|
||||
image_path = f"images/vegetables/{self.type}.png"
|
||||
if os.path.exists(image_path):
|
||||
self.image = pygame.image.load(image_path).convert()
|
||||
if self.type == 'grass':
|
||||
image_path = "images/grass.png"
|
||||
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))
|
||||
|
235
src/tractor.py
235
src/tractor.py
@ -4,6 +4,8 @@ from kb import ile_podlac, multi_sasiedzi
|
||||
from tile import Tile
|
||||
from config import TILE_SIZE
|
||||
from collections import deque
|
||||
import heapq
|
||||
import random
|
||||
|
||||
class Tractor(pygame.sprite.Sprite):
|
||||
def __init__(self, field):
|
||||
@ -15,43 +17,60 @@ class Tractor(pygame.sprite.Sprite):
|
||||
self.rect = self.image.get_rect()
|
||||
|
||||
self.direction = 'east'
|
||||
|
||||
x, y = 0, 0
|
||||
self.rect.topleft = (x, y)
|
||||
# TODO: enable tractor to start on other tile than (0,0)
|
||||
self.start = (0, 0)
|
||||
self.final = (8, 14)
|
||||
print('final target @', self.final[0], self.final[1])
|
||||
self.rect.topleft = (0, 0)
|
||||
|
||||
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):
|
||||
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):
|
||||
if self.direction == "north" and self.rect.y > 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
|
||||
|
Loading…
Reference in New Issue
Block a user