427 lines
16 KiB
Python
427 lines
16 KiB
Python
import pygame
|
|
import os
|
|
from kb import ile_podlac, multi_sasiedzi
|
|
from tile import Tile
|
|
from config import TILE_SIZE, FINAL_X, FINAL_Y, START_X, START_Y, STARTING_DIRECTION
|
|
from collections import deque
|
|
import heapq
|
|
import random
|
|
import pandas as pd
|
|
from sklearn.tree import DecisionTreeClassifier
|
|
from sklearn.preprocessing import LabelEncoder
|
|
from sklearn.tree import export_text
|
|
|
|
class Tractor(pygame.sprite.Sprite):
|
|
def __init__(self, field):
|
|
super().__init__
|
|
self.field = field
|
|
|
|
self.image = pygame.image.load('images/tractor/east.png').convert_alpha()
|
|
self.image = pygame.transform.scale(self.image, (TILE_SIZE, TILE_SIZE))
|
|
self.rect = self.image.get_rect()
|
|
|
|
self.direction = STARTING_DIRECTION
|
|
# TODO: enable tractor to start on other tile than (0,0)
|
|
self.start = (START_X, START_Y)
|
|
self.final = (FINAL_X, FINAL_Y)
|
|
print('destination @', self.final[0], self.final[1])
|
|
self.rect.topleft = (self.start[0] * TILE_SIZE, self.start[1] * TILE_SIZE)
|
|
|
|
self.water = 50
|
|
|
|
# A-STAR
|
|
# came_from, total_cost = self.a_star()
|
|
# path = self.reconstruct_path(came_from)
|
|
# self.actions = self.recreate_actions(path)
|
|
# self.action_index = 0
|
|
|
|
# DECISION TREE:
|
|
self.label_encoders = {}
|
|
self.load_decision_tree_model()
|
|
|
|
def load_decision_tree_model(self):
|
|
data = pd.read_csv('tree.csv')
|
|
|
|
# Konwersja danych kategorycznych na liczbowe
|
|
for column in data.columns:
|
|
self.label_encoders[column] = LabelEncoder()
|
|
data[column] = self.label_encoders[column].fit_transform(data[column])
|
|
|
|
# Podział danych na atrybuty (X) i etykiety (y)
|
|
X = data.drop(columns=['action'])
|
|
y = data['action']
|
|
|
|
# Inicjalizacja i dopasowanie modelu drzewa decyzyjnego
|
|
self.decision_tree_model = DecisionTreeClassifier(random_state=42)
|
|
self.decision_tree_model.fit(X.values, y.values)
|
|
tree_rules = export_text(self.decision_tree_model, feature_names=X.columns.tolist())
|
|
print(tree_rules)
|
|
|
|
def make_decision(self):
|
|
neighbors = self.get_neighbors_types()
|
|
if len(neighbors) < 4:
|
|
for _ in range(len(neighbors), 4):
|
|
neighbors.append('grass')
|
|
|
|
input_data = {
|
|
'tile_type': self.get_current_tile().type,
|
|
'water_level': self.get_current_tile().water_level,
|
|
"plant_stage": self.get_current_tile().stage,
|
|
"neighbor_N": neighbors[0],
|
|
"neighbor_E": neighbors[1],
|
|
"neighbor_W": neighbors[3],
|
|
"neighbor_S": neighbors[2]
|
|
}
|
|
|
|
input_values = []
|
|
for column, encoder in self.label_encoders.items():
|
|
if column == 'action': continue
|
|
input_value = input_data.get(column)
|
|
input_values.append(encoder.transform([input_value])[0])
|
|
|
|
# Przewidywanie akcji za pomocą modelu
|
|
predicted_action_index = self.decision_tree_model.predict([input_values])[0]
|
|
|
|
# Odwrotna transformacja na prawdziwą nazwę akcji
|
|
action = self.label_encoders["action"].inverse_transform([predicted_action_index])[0]
|
|
return action
|
|
|
|
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()
|
|
elif self.direction == "south" and self.rect.y < 15 * TILE_SIZE:
|
|
self.rect.y += TILE_SIZE
|
|
# self.log_info()
|
|
elif self.direction == "west" and self.rect.x > 0:
|
|
self.rect.x -= TILE_SIZE
|
|
# self.log_info()
|
|
elif self.direction == "east" and self.rect.x < 15 * TILE_SIZE:
|
|
self.rect.x += TILE_SIZE
|
|
# self.log_info()
|
|
|
|
def move_rotating(self):
|
|
if self.direction == "north":
|
|
if self.rect.y > 0:
|
|
self.rect.y -= TILE_SIZE
|
|
else:
|
|
self.rotate('left')
|
|
self.rect.x -= TILE_SIZE
|
|
elif self.direction == "south":
|
|
if self.rect.y < 15 * TILE_SIZE:
|
|
self.rect.y += TILE_SIZE
|
|
else:
|
|
self.rotate('left')
|
|
self.rect.x += TILE_SIZE
|
|
elif self.direction == "west":
|
|
if self.rect.x > 0:
|
|
self.rect.x -= TILE_SIZE
|
|
else:
|
|
self.rotate('left')
|
|
self.rect.y += TILE_SIZE
|
|
elif self.direction == "east":
|
|
if self.rect.x < 15 * TILE_SIZE:
|
|
self.rect.x += TILE_SIZE
|
|
else:
|
|
self.rotate('left')
|
|
self.rect.y -= TILE_SIZE
|
|
|
|
def update(self):
|
|
# A STAR:
|
|
# 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')
|
|
|
|
# DECISION TREE:
|
|
action = self.make_decision()
|
|
self.prev_action = action
|
|
if self.prev_action is not None and self.prev_action != 'move':
|
|
self.move_rotating()
|
|
self.prev_action = 'move'
|
|
match (action):
|
|
case ('move'):
|
|
self.move_rotating()
|
|
case ('harvest'):
|
|
self.get_current_tile().set_type('grass')
|
|
case ('water'):
|
|
self.get_current_tile().water_level += 10
|
|
case ('plant(bób)'):
|
|
self.get_current_tile().set_type('marchew')
|
|
case ('plant(brokuł)'):
|
|
self.get_current_tile().set_type('brokuł')
|
|
case ('plant(brukselka)'):
|
|
self.get_current_tile().set_type('brukselka')
|
|
case ('plant(burak)'):
|
|
self.get_current_tile().set_type('burak')
|
|
case ('plant(cebula)'):
|
|
self.get_current_tile().set_type('marchew')
|
|
case ('plant(cukinia)'):
|
|
self.get_current_tile().set_type('cukinia')
|
|
case ('plant(dynia)'):
|
|
self.get_current_tile().set_type('fasola')
|
|
case ('plant(fasola)'):
|
|
self.get_current_tile().set_type('fasola')
|
|
case ('plant(groch)'):
|
|
self.get_current_tile().set_type('groch')
|
|
case ('plant(jarmuż)'):
|
|
self.get_current_tile().set_type('jarmuż')
|
|
case ('plant(kalafior)'):
|
|
self.get_current_tile().set_type('kalafior')
|
|
case ('plant(kalarepa)'):
|
|
self.get_current_tile().set_type('kalarepa')
|
|
case ('plant(kapusta)'):
|
|
self.get_current_tile().set_type('kapusta')
|
|
case ('plant(marchew)'):
|
|
self.get_current_tile().set_type('marchew')
|
|
case ('plant(ogórek)'):
|
|
self.get_current_tile().set_type('ogórek')
|
|
case ('plant(papryka)'):
|
|
self.get_current_tile().set_type('papryka')
|
|
case ('plant(pietruszka)'):
|
|
self.get_current_tile().set_type('marchew')
|
|
case ('plant(pomidor)'):
|
|
self.get_current_tile().set_type('pomidor')
|
|
case ('plant(por)'):
|
|
self.get_current_tile().set_type('por')
|
|
case ('plant(rzepa)'):
|
|
self.get_current_tile().set_type('rzepa')
|
|
case ('plant(rzodkiewka)'):
|
|
self.get_current_tile().set_type('rzodkiewka')
|
|
case ('plant(sałata)'):
|
|
self.get_current_tile().set_type('sałata')
|
|
case ('plant(seler)'):
|
|
self.get_current_tile().set_type('seler')
|
|
case ('plant(szpinak)'):
|
|
self.get_current_tile().set_type('szpinak')
|
|
case ('plant(ziemniak)'):
|
|
self.get_current_tile().set_type('ziemniak')
|
|
#self.action_index += 1
|
|
print(action)
|
|
return
|
|
|
|
def log_info(self):
|
|
# print on what tile type the tractor is on
|
|
x = self.rect.x // TILE_SIZE
|
|
y = self.rect.y // TILE_SIZE
|
|
tile_type = self.field.tiles.sprites()[y * 16 + x].type
|
|
print(f"🧭 the tractor is on a {tile_type} tile")
|
|
|
|
# print if the tile can be watered
|
|
current_tile = self.get_current_tile()
|
|
water_needed = ile_podlac(current_tile.type, current_tile.faza)[0]['Woda']
|
|
if self.water >= water_needed:
|
|
print(f"💦 watered {current_tile.type} with {water_needed} liters")
|
|
self.water -= water_needed
|
|
else:
|
|
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_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)}")
|
|
print() # empty line at end of log statement
|
|
|
|
|
|
def get_current_tile(self):
|
|
x = self.rect.x // TILE_SIZE
|
|
y = self.rect.y // TILE_SIZE
|
|
current_tile = self.field.tiles.sprites()[y * 16 + x]
|
|
return current_tile
|
|
|
|
|
|
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 ('water'):
|
|
cost = 1000
|
|
case _:
|
|
cost = 100
|
|
return cost
|
|
|
|
|
|
def get_neighbors_types(self) -> list:
|
|
x = self.rect.x // TILE_SIZE
|
|
y = self.rect.y // TILE_SIZE
|
|
neighbors = []
|
|
self.field.tiles.sprites()[y * 16 + x].type
|
|
|
|
if x > 0:
|
|
neighbors.append(self.field.tiles.sprites()[y * 16 + x - 1].type)
|
|
if x < 15:
|
|
neighbors.append(self.field.tiles.sprites()[y * 16 + x + 1].type)
|
|
if y > 0:
|
|
neighbors.append(self.field.tiles.sprites()[(y - 1) * 16 + x].type)
|
|
if y < 15:
|
|
neighbors.append(self.field.tiles.sprites()[(y + 1) * 16 + x].type)
|
|
|
|
return neighbors
|
|
|
|
|
|
def rotate(self, rotate):
|
|
match (self.direction, rotate):
|
|
case ("north", "left"):
|
|
self.image = pygame.image.load('images/tractor/west.png').convert_alpha()
|
|
self.image = pygame.transform.scale(self.image, (TILE_SIZE, TILE_SIZE))
|
|
self.direction = 'west'
|
|
case ("north", "right"):
|
|
self.image = pygame.image.load('images/tractor/east.png').convert_alpha()
|
|
self.image = pygame.transform.scale(self.image, (TILE_SIZE, TILE_SIZE))
|
|
self.direction = 'east'
|
|
case ("east", "right"):
|
|
self.image = pygame.image.load('images/tractor/south.png').convert_alpha()
|
|
self.image = pygame.transform.scale(self.image, (TILE_SIZE, TILE_SIZE))
|
|
self.direction = 'south'
|
|
case ("east", "left"):
|
|
self.image = pygame.image.load('images/tractor/north.png').convert_alpha()
|
|
self.image = pygame.transform.scale(self.image, (TILE_SIZE, TILE_SIZE))
|
|
self.direction = 'north'
|
|
case ("south", "right"):
|
|
self.image = pygame.image.load('images/tractor/west.png').convert_alpha()
|
|
self.image = pygame.transform.scale(self.image, (TILE_SIZE, TILE_SIZE))
|
|
self.direction = 'west'
|
|
case ("south", "left"):
|
|
self.image = pygame.image.load('images/tractor/east.png').convert_alpha()
|
|
self.image = pygame.transform.scale(self.image, (TILE_SIZE, TILE_SIZE))
|
|
self.direction = 'east'
|
|
case ("west", "left"):
|
|
self.image = pygame.image.load('images/tractor/south.png').convert_alpha()
|
|
self.image = pygame.transform.scale(self.image, (TILE_SIZE, TILE_SIZE))
|
|
self.direction = 'south'
|
|
case ("west", "right"):
|
|
self.image = pygame.image.load('images/tractor/north.png').convert_alpha()
|
|
self.image = pygame.transform.scale(self.image, (TILE_SIZE, TILE_SIZE))
|
|
self.direction = 'north'
|
|
|
|
|
|
# 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
|
|
|
|
while fringe:
|
|
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
|