Merge branch 'dungeon_master' of https://git.wmi.amu.edu.pl/s474137/automatyczny_kelner into dungeon_master

This commit is contained in:
Mirrowel 2023-05-18 19:17:44 +02:00
commit 67c7f159b1
27 changed files with 494 additions and 219 deletions

View File

@ -17,4 +17,32 @@
## RUN INSTRUCTIONS ## RUN INSTRUCTIONS
pipenv run python main.py pipenv run python agent.py
## TODO
---
- [x] **Planowanie ruchu: Wymagania dot. pierwszego przyrostu**
- [x] Agent powinien dysponować co najmniej następującymi akcjami: ruch do przodu, obrót w lewo, obrót w prawo
- [x] Należy wykorzystać „Schemat procedury przeszukiwania grafu stanów“.
- [x] Należy zaimplementować strategię Breadth-First Search.
---
- [x] **Planowanie ruchu: Wymagania dot. drugiego przyrostu**
- [x] Należy wykorzystać „Schemat procedury przeszukiwania grafu stanów z uwzględnieniem kosztu“
- [x] Należy zaimplementować strategię A\*, czyli zdefiniować funkcję wyznaczającą priorytet następników uwzględniającą zarówno koszt jak i odpowiednią heurystykę.
- [x] Agent powinien dysponować co najmniej następującymi akcjami: ruch do przodu, obrót w lewo, obrót w prawo.
- [x] Koszt wjazdu na pola poszczególnych typów powinien być zróżnicowany.
> _Przykład: Koszt wjazdu traktora na pole marchewek to 10 a koszt wjazdu na pole puste to 1._
---
- [ ] **Drzewa decyzyjne: wymagania dot. trzeciego przyrostu**
- [ ] Należy wykorzystać algorytm ID3 (tj. schemat indukcyjnego uczenia drzewa decyzyjnego oraz procedurę wyboru atrybutu o największym przyroście informacji) lub któreś z jego uogólnień.
- [ ] Należy przygotować zbiór uczący złożony z co najmniej 200 przykładów.
- [ ] Decyzja stanowiąca cel uczenia powinna zostać opisana przynajmniej ośmioma atrybutami.
- [ ] Powinna pojawić się opcja podglądu wyuczonego drzewa (np. w logach lub w pliku z graficzną reprezentacją drzewa).

39
agent.py Normal file
View File

@ -0,0 +1,39 @@
import random
from src.Engine import Engine
from src.obj.Waiter import Waiter
from src.obj.Block import Block
from src.obj.Kitchen import Kitchen
from src.obj.Table import Table
from src.UserController import UserController
from src.StateController import StateController
SCREEN_SIZE = (800, 800)
SQUARE_SIZE = 40
waiter = Waiter([0, 0], 0, SQUARE_SIZE, SCREEN_SIZE)
objects = [
Kitchen([0, 0], 0, SQUARE_SIZE, SCREEN_SIZE)
]
for i in range(150):
pos = [0, 0]
while any([o.compare_pos(pos) for o in objects]):
pos = [random.randint(1, SCREEN_SIZE[0]/SQUARE_SIZE),
random.randint(1, SCREEN_SIZE[0]/SQUARE_SIZE)]
if (random.randint(0, 1)):
objects.append(Block(pos, 0, SQUARE_SIZE, SCREEN_SIZE))
else:
objects.append(Table(pos, 0, SQUARE_SIZE,
SCREEN_SIZE, random.randint(0, 3)))
user = UserController(waiter)
state = StateController(waiter)
engine = Engine(SCREEN_SIZE, SQUARE_SIZE, user, state)
for o in objects:
engine.subscribe(o)
engine.loop()

76
main.py
View File

@ -1,76 +0,0 @@
import pygame
import random
from models.Kitchen import Kitchen
from models.Table import Table
from models.Waiter import Waiter
pygame.init()
screen_size = (600, 600)
screen = pygame.display.set_mode(screen_size)
square_size = 50
num_squares = screen_size[0] // square_size
squares = []
for i in range(num_squares):
row = []
for j in range(num_squares):
square_rect = pygame.Rect(
j * square_size, i * square_size, square_size, square_size)
row.append(square_rect)
squares.append(row)
tables = [
Table(square_size, screen_size, 2, 4),
Table(square_size, screen_size, 6, 5),
Table(square_size, screen_size, 4, 2),
Table(square_size, screen_size, 5, 6),
Table(square_size, screen_size, 4, 4),
]
kitchen = Kitchen(square_size, screen_size, 0, 0)
waiter = Waiter(square_size, screen_size, 0, 0)
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_UP:
waiter.up()
elif event.key == pygame.K_DOWN:
waiter.down()
elif event.key == pygame.K_LEFT:
waiter.left()
elif event.key == pygame.K_RIGHT:
waiter.right()
elif event.key == pygame.K_ESCAPE:
for table in tables:
table.new_order()
screen.fill((255, 255, 255))
for row in squares:
for square_rect in row:
pygame.draw.rect(screen, (0, 0, 0), square_rect, 1)
for table in tables:
table.blit(screen)
kitchen.blit(screen)
waiter.blit(screen)
for table in tables:
if table.collision(waiter):
waiter.do_smth(table)
if kitchen.collision(waiter):
kitchen.take_orders(waiter)
pygame.display.flip()

View File

@ -1,18 +0,0 @@
import pygame
from models.Object import Object
from models.Waiter import Waiter
class Kitchen(Object):
def __init__(self, square_size, screen_size, left_square, top_square):
super().__init__(
'kitchen',
square_size,
screen_size,
left_square,
top_square
)
def take_orders(self, waiter: Waiter):
for table in waiter.get_order_list():
table.done_order()

View File

@ -1,39 +0,0 @@
import pygame
class Object:
def __init__(self, role, square_size, screen_size, left_square, top_square):
self.role = role
self.image = pygame.transform.scale(pygame.image.load(
'images/{0}.png'.format(role)), (square_size, square_size))
self.square_size = square_size
self.screen_size = screen_size
left = left_square * square_size
top = top_square * square_size
self.rect = pygame.Rect(left, top, square_size, square_size)
def up(self):
if self.rect.top > 0:
self.rect.top -= self.square_size
def down(self):
if self.rect.bottom < self.screen_size[1]:
self.rect.top += self.square_size
def left(self):
if self.rect.left > 0:
self.rect.left -= self.square_size
def right(self):
if self.rect.right < self.screen_size[0]:
self.rect.left += self.square_size
def blit(self, screen):
screen.blit(self.image, self.rect)
def collision(self, obj):
x = self.rect.left == obj.rect.left
y = self.rect.top == obj.rect.top
return x and y

View File

@ -1,41 +0,0 @@
import pygame
from models.Object import Object
class Table(Object):
def __init__(self, square_size, screen_size, left_square, top_square):
super().__init__(
'table',
square_size,
screen_size,
left_square,
top_square
)
self.state = 'table'
def new_order(self):
self.state = 'order'
self.update_pic()
def wait_order(self):
self.state = 'wait'
self.update_pic()
def done_order(self):
self.state = 'done'
self.update_pic()
def reset_order(self):
self.state = 'table'
self.update_pic()
def is_order(self) -> bool:
return self.state == 'order'
def is_done(self) -> bool:
return self.state == 'done'
def update_pic(self):
self.image = pygame.transform.scale(pygame.image.load(
'images/{0}.png'.format(self.state)), (self.square_size, self.square_size))

View File

@ -1,44 +0,0 @@
import pygame
from models.Object import Object
from models.Table import Table
class Waiter(Object):
def __init__(self, square_size, screen_size, left_square, top_square):
super().__init__(
'waiter',
square_size,
screen_size,
left_square,
top_square
)
self.orders_limit = 3
self.orders_list = []
def do_smth(self, table: Table):
if table.is_order():
self.take_order(table)
elif table.is_done():
self.deliver_order(table)
def take_order(self, table: Table):
if self.orders_limit <= 0:
return
if not table.is_order():
return
self.orders_limit -= 1
self.orders_list.append(table)
table.wait_order()
def deliver_order(self, table: Table):
if table.is_done() and table in self.orders_list:
self.orders_limit += 1
self.orders_list.remove(table)
table.reset_order()
def get_order_list(self) -> list[Table]:
return self.orders_list

89
src/Engine.py Normal file
View File

@ -0,0 +1,89 @@
import time
import pygame
from .obj.Object import Object
from .UserController import UserController
from .StateController import StateController
class Engine:
def __init__(self, screen_size, square_size, user: UserController, state: StateController):
pygame.display.set_caption('Waiter Agent')
self.user = user
self.state = state
self.screen_size = screen_size
self.screen = pygame.display.set_mode(self.screen_size)
self.square_size = square_size
self.num_squares = self.screen_size[0] // self.square_size
self.squares = self.__init_squares_field__(
self.num_squares, self.square_size)
self.objects: list[Object] = []
self.goals: list = []
self.runnin = False
def __init_squares_field__(self, num_squares, square_size):
squares = []
for i in range(num_squares):
row = []
for j in range(num_squares):
square_rect = pygame.Rect(
j * square_size, i * square_size,
square_size, square_size)
row.append(square_rect)
squares.append(row)
return squares
def subscribe(self, object: Object):
self.objects.append(object)
def loop(self):
self.running = True
while self.running:
self.action()
self.redraw()
def quit(self):
self.running = False
def action(self):
if not self.state.path:
if self.goals:
self.state.graphsearch(self)
self.user.handler(self)
else:
state = self.user.obj.changeState(self.state.path.pop())
print("Action:\t{0}\tCost:\t{1}\tCost so far: {2}".format(
state.agent_role,
state.cost,
state.cost_so_far)
)
time.sleep(0.5)
def redraw(self):
self.screen.fill((255, 255, 255))
for row in self.squares:
for square_rect in row:
pygame.draw.rect(self.screen, (0, 0, 0), square_rect, 1)
for o in self.objects:
o.blit(self.screen)
self.user.obj.blit(self.screen)
for f in self.state.fringe.queue:
f.blit(self.screen)
for s in self.state.path:
s.blit(self.screen)
pygame.display.flip()
def appendGoalPosition(self, position):
self.goals.append(position)

92
src/StateController.py Normal file
View File

@ -0,0 +1,92 @@
from .obj.TemporaryState import TemporaryState
from queue import PriorityQueue
class StateController:
def __init__(self, istate):
self.path = []
self.explored = []
self.fringe = PriorityQueue()
self.istate = istate
self.goal = istate.position
def reset(self):
self.path.clear()
self.explored.clear()
self.fringe = PriorityQueue()
def build_path(self, goal_state):
total_cost = goal_state.cost
self.path.append(goal_state)
while self.path[-1].parent.agent_role != "blank":
self.path.append(self.path[-1].parent)
total_cost += self.path[-1].cost
print("Total path cost:\t{0}".format(total_cost))
return self.path
def graphsearch(self, engine): # A*
print("Search path")
self.goal = list(engine.goals.pop())
self.reset()
start = TemporaryState(self.istate, 0)
self.fringe.put(start)
while self.fringe and not self.path:
self.explored.append(self.fringe.get())
if self.explored[-1].position == self.goal:
goal_state = self.explored[-1]
self.reset()
return self.build_path(goal_state)
self.succ(self.explored[-1].front(), engine)
self.succ(self.explored[-1].left(), engine)
self.succ(self.explored[-1].right(), engine)
engine.redraw()
self.reset()
print("Not found")
return False
def succ(self, state, engine):
if state.collide_test():
return
elif any(e.compare(state) for e in self.explored):
return
elif any([o.collide_test(state) for o in engine.objects]):
return
for o in engine.objects:
if state.cost != 1:
break
if o.position == state.position:
state.change_cost(o)
state.cost_so_far = self.explored[-1].cost_so_far + state.cost
in_explored = any([state.compare(s) for s in self.explored]
)
in_frige = any([state.compare(f) for f in self.fringe.queue])
if not in_explored and not in_frige:
state.heuristic(self.goal)
self.fringe.put(state)
elif in_frige:
fringe = state
for f in self.fringe.queue:
if state.compare(f):
fringe = f
break
if state.cost_so_far < fringe.cost_so_far:
fringe.replace(state)

16
src/UserController.py Normal file
View File

@ -0,0 +1,16 @@
import pygame
class UserController:
def __init__(self, usrObj):
self.obj = usrObj
def handler(self, engine):
for event in pygame.event.get():
if event.type == pygame.QUIT:
engine.quit()
elif event.type == pygame.MOUSEBUTTONDOWN:
pos = pygame.mouse.get_pos()
pos = (pos[0] // engine.square_size,
pos[1] // engine.square_size)
engine.appendGoalPosition(pos)

BIN
src/img/blank.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 B

BIN
src/img/block.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

BIN
src/img/front.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

BIN
src/img/left.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
src/img/right.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

9
src/obj/Block.py Normal file
View File

@ -0,0 +1,9 @@
from src.obj.Object import Object
class Block(Object):
def __init__(self, position, orientation, square_size, screen_size):
super().__init__("block", position, orientation, square_size, screen_size)
def collide_test(self, waiter: Object) -> bool:
return waiter.position == self.position

6
src/obj/Kitchen.py Normal file
View File

@ -0,0 +1,6 @@
from src.obj.Object import Object
class Kitchen(Object):
def __init__(self, position, orientation, square_size, screen_size):
super().__init__("kitchen", position, orientation, square_size, screen_size)

48
src/obj/Object.py Normal file
View File

@ -0,0 +1,48 @@
import pygame
class Object:
def __init__(self, agent_role, position, orientation, square_size, screen_size):
self.agent_role = agent_role
self.position = position
self.orientation = orientation % 4
self.square_size = square_size
self.screen_size = screen_size
self.square_count = screen_size[0] // square_size
self.image = pygame.image.load(
'src/img/{0}.png'.format(self.agent_role))
self.image = pygame.transform.scale(
self.image, (self.square_size, self.square_size))
self.rect = pygame.Rect(position[0] * square_size,
position[1] * square_size,
square_size, square_size)
def change_role(self, new_role):
self.agent_role = new_role
self.image = pygame.image.load('src/img/{0}.png'.format(new_role))
self.image = pygame.transform.scale(
self.image, (self.square_size, self.square_size))
def get_angle(self):
'''
orientation = 0 -> up\n
orientation = 1 -> left\n
orientation = 2 -> down\n
orientation = 3 -> right\n
'''
return self.orientation * 90
def collide_test(self, obj) -> bool:
return False
def blit(self, screen: pygame.Surface):
image = pygame.transform.rotate(self.image, self.get_angle())
self.rect.x = self.position[0] * self.square_size
self.rect.y = self.position[1] * self.square_size
screen.blit(image, self.rect)
def compare_pos(self, pos) -> bool:
return self.position == pos

14
src/obj/Table.py Normal file
View File

@ -0,0 +1,14 @@
from src.obj.Object import Object
class Table(Object):
def __init__(self, position, orientation, square_size, screen_size, current_role=0):
super().__init__("table", position, orientation, square_size, screen_size)
self.roles = ["table", "order", "wait", "done"]
self.current_role = current_role
self.change_role(self.roles[self.current_role])
def next_role(self, waiter):
if waiter.agent_role == "waiter":
self.current_role = (self.current_role + 1) % 4
self.change_role(self.roles[self.current_role])

102
src/obj/TemporaryState.py Normal file
View File

@ -0,0 +1,102 @@
from src.obj.Waiter import Waiter
import copy
class TemporaryState(Waiter):
def __init__(self, parent, cost_so_far, action="blank", cost=0, h=0):
super().__init__(copy.deepcopy(parent.position),
copy.copy(parent.orientation),
copy.copy(parent.square_size),
copy.copy(parent.screen_size),
copy.copy(parent.basket))
self.agent_role = action
self.parent = parent
self.change_role(action)
self.apply_transformation()
self.cost = cost
self.cost_so_far = cost_so_far
self.h = h
def replace(self, repl):
self.cost_so_far = copy.copy(repl.cost_so_far)
self.basket = copy.copy(repl.basket)
self.parent = repl.parent
def apply_transformation(self):
if self.agent_role == "left":
self.orientation = (self.orientation + 1) % 4
elif self.agent_role == "right":
self.orientation = (self.orientation - 1) % 4
elif self.agent_role == "front":
if self.orientation % 2: # x (1 or 3)
self.position[0] += self.orientation - 2 # x (-1 or +1)
else: # y (0 or 2)
self.position[1] += self.orientation - 1 # y (-1 or +1)
def left(self):
return TemporaryState(self, self.cost_so_far, "left", 1)
def right(self):
return TemporaryState(self, self.cost_so_far, "right", 1)
def front(self):
return TemporaryState(self, self.cost_so_far, "front", 1)
def collide_test(self) -> bool:
out_of_range = [
self.position[0] >= self.square_count,
self.position[1] >= self.square_count,
self.position[0] < 0,
self.position[1] < 0
]
return any(out_of_range)
def compare(self, state) -> bool:
conditions = [
self.position == state.position,
self.orientation == state.orientation
]
return all(conditions)
def change_cost(self, obj):
self.cost = 1 # default cost
if self.agent_role == "front":
costs = {
"kitchen": 5,
"table": 5,
"order": 20,
"wait": 10,
"done": 15,
}
if obj.agent_role in costs.keys():
self.cost = costs[obj.agent_role]
def heuristic(self, goal):
x = abs(self.position[0] - goal[0])
y = abs(self.position[1] - goal[1])
self.h = x + y
return self.h
def current_cost(self):
return self.cost_so_far + self.h
def __eq__(self, __value) -> bool:
return self.current_cost() == __value.current_cost()
def __lt__(self, __value) -> bool:
return self.current_cost() < __value.current_cost()
def __le__(self, __value) -> bool:
return self.current_cost() <= __value.current_cost()
def __gt__(self, __value) -> bool:
return self.current_cost() > __value.current_cost()
def __ge__(self, __value) -> bool:
return self.current_cost() >= __value.current_cost()

50
src/obj/Waiter.py Normal file
View File

@ -0,0 +1,50 @@
import copy
from src.obj.Object import Object
class Waiter(Object):
def __init__(self, position, orientation, square_size, screen_size, basket=[]):
super().__init__("waiter", position, orientation, square_size, screen_size)
self.basket_size = 2
self.basket = basket
self.prev_position = copy.deepcopy(self.position)
self.prev_orientation = copy.copy(self.orientation)
def changeState(self, state):
self.position = copy.deepcopy(state.position)
self.orientation = copy.copy(state.orientation)
self.basket = copy.copy(state.basket)
return state
def dampState(self):
self.prev_position = copy.deepcopy(self.position)
self.prev_orientation = copy.copy(self.orientation)
def rollbackState(self):
self.position = copy.deepcopy(self.prev_position)
self.orientation = copy.copy(self.prev_orientation)
def orders_in_basket(self) -> bool:
return self.basket
def left(self):
self.orientation = (self.orientation + 1) % 4
def right(self):
self.orientation = (self.orientation - 1) % 4
def front(self):
if self.orientation % 2: # x (1 or 3)
self.position[0] += self.orientation - 2 # x (-1 or +1)
else: # y (0 or 2)
self.position[1] += self.orientation - 1 # y (-1 or +1)
def collide_test(self, obj) -> bool:
out_of_range = [
self.position[0] >= self.square_count,
self.position[1] >= self.square_count,
self.position[0] < 0,
self.position[1] < 0
]
return any(out_of_range)