inteligenty-traktor/src/tractor.py

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