diff --git a/README.MD b/README.MD index 9f7123a..9d16b79 100644 --- a/README.MD +++ b/README.MD @@ -17,4 +17,26 @@ ## 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. + +--- + +- [ ] Planowanie ruchu: Wymagania dot. drugiego przyrostu + - [ ] Należy wykorzystać „Schemat procedury przeszukiwania grafu stanów z uwzględnieniem kosztu“ + - [ ] 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. + - [ ] 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._ + +--- diff --git a/agent.py b/agent.py new file mode 100644 index 0000000..d41e7e5 --- /dev/null +++ b/agent.py @@ -0,0 +1,27 @@ +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 + +waiter = Waiter([0, 0], 0, 50, 450//50) +objects = [ + Kitchen([0, 0], 0, 50, 450//50), + Table([3, 6], 0, 50, 450//50), + Table([2, 4], 0, 50, 450//50), + Table([1, 5], 0, 50, 450//50), + Block([3, 5], 0, 50, 450//50), + Block([1, 4], 0, 50, 450//50), + Block([2, 5], 0, 50, 450//50) +] + +user = UserController(waiter) +state = StateController(waiter) +engine = Engine((450, 450), 50, user, state) + +for o in objects: + engine.subscribe(o) + +engine.loop() diff --git a/main.py b/main.py deleted file mode 100644 index 1f8aa39..0000000 --- a/main.py +++ /dev/null @@ -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() diff --git a/models/Kitchen.py b/models/Kitchen.py deleted file mode 100644 index 1035a16..0000000 --- a/models/Kitchen.py +++ /dev/null @@ -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() diff --git a/models/Object.py b/models/Object.py deleted file mode 100644 index c2b30e4..0000000 --- a/models/Object.py +++ /dev/null @@ -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 diff --git a/models/Table.py b/models/Table.py deleted file mode 100644 index 9e7ba05..0000000 --- a/models/Table.py +++ /dev/null @@ -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)) diff --git a/models/Waiter.py b/models/Waiter.py deleted file mode 100644 index 652a1fc..0000000 --- a/models/Waiter.py +++ /dev/null @@ -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 diff --git a/src/Engine.py b/src/Engine.py new file mode 100644 index 0000000..bbaa2b5 --- /dev/null +++ b/src/Engine.py @@ -0,0 +1,80 @@ +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): + 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.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): + self.user.handler(self) + + conditionals = [ + not self.user.obj.collide_test(self.user.obj), + all([not o.collide_test(self.user.obj) for o in self.objects]) + ] + + if all(conditionals): + self.user.obj.dampState() + else: + self.user.obj.rollbackState() + + self.user.obj.goal_test(self) + + 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 s in self.state.path: + s.blit(self.screen) + + pygame.display.flip() diff --git a/src/StateController.py b/src/StateController.py new file mode 100644 index 0000000..88b0c7d --- /dev/null +++ b/src/StateController.py @@ -0,0 +1,60 @@ +from .obj.TemporaryState import TemporaryState + + +class StateController: + def __init__(self, istate): + self.path = [] + self.explored = [] + self.fringe = [] + self.istate = istate + + def reset(self): + self.path.clear() + self.explored.clear() + self.fringe.clear() + + def build_path(self, goal_state): + self.path.append(goal_state) + while self.path[-1].agent_role != "blank": + self.path.append(self.path[-1].parent) + + return self.path + + def graphsearch(self, engine): # BFS + print("Search path") + + self.reset() + + self.fringe.append(TemporaryState(self.istate)) + + while self.fringe: + self.explored.append(self.fringe.pop(0)) + + if self.explored[-1].goal_test(engine): + print("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) + + self.path = self.fringe + 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 + + self.fringe.append(state) diff --git a/src/UserController.py b/src/UserController.py new file mode 100644 index 0000000..5dbe326 --- /dev/null +++ b/src/UserController.py @@ -0,0 +1,20 @@ +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.KEYDOWN: + if event.key == pygame.K_UP: + self.obj.front() + elif event.key == pygame.K_LEFT: + self.obj.left() + elif event.key == pygame.K_RIGHT: + self.obj.right() + elif event.key == pygame.K_SPACE: + engine.state.graphsearch(engine) diff --git a/src/img/blank.png b/src/img/blank.png new file mode 100644 index 0000000..0cb6d41 Binary files /dev/null and b/src/img/blank.png differ diff --git a/src/img/block.png b/src/img/block.png new file mode 100644 index 0000000..6012ad1 Binary files /dev/null and b/src/img/block.png differ diff --git a/images/done.png b/src/img/done.png similarity index 100% rename from images/done.png rename to src/img/done.png diff --git a/src/img/front.png b/src/img/front.png new file mode 100644 index 0000000..2ba38d9 Binary files /dev/null and b/src/img/front.png differ diff --git a/images/kitchen.png b/src/img/kitchen.png similarity index 100% rename from images/kitchen.png rename to src/img/kitchen.png diff --git a/src/img/left.png b/src/img/left.png new file mode 100644 index 0000000..e1b0873 Binary files /dev/null and b/src/img/left.png differ diff --git a/images/order.png b/src/img/order.png similarity index 100% rename from images/order.png rename to src/img/order.png diff --git a/src/img/right.png b/src/img/right.png new file mode 100644 index 0000000..b270933 Binary files /dev/null and b/src/img/right.png differ diff --git a/images/table.png b/src/img/table.png similarity index 100% rename from images/table.png rename to src/img/table.png diff --git a/images/wait.png b/src/img/wait.png similarity index 100% rename from images/wait.png rename to src/img/wait.png diff --git a/images/waiter.png b/src/img/waiter.png similarity index 100% rename from images/waiter.png rename to src/img/waiter.png diff --git a/src/obj/Block.py b/src/obj/Block.py new file mode 100644 index 0000000..51df401 --- /dev/null +++ b/src/obj/Block.py @@ -0,0 +1,9 @@ +from src.obj.Object import Object + + +class Block(Object): + def __init__(self, position, orientation, square_size, square_count): + super().__init__("block", position, orientation, square_size, square_count) + + def collide_test(self, waiter: Object) -> bool: + return waiter.position == self.position diff --git a/src/obj/Kitchen.py b/src/obj/Kitchen.py new file mode 100644 index 0000000..a8063ad --- /dev/null +++ b/src/obj/Kitchen.py @@ -0,0 +1,20 @@ +from src.obj.Object import Object + + +class Kitchen(Object): + def __init__(self, position, orientation, square_size, square_count): + super().__init__("kitchen", position, orientation, square_size, square_count) + + def goal_test(self, waiter) -> bool: + conditions = [ + waiter.orders_in_basket(), + self.position == waiter.position + ] + + if all(conditions): + for table in waiter.basket: + if table.agent_role == "wait": + table.next_role(waiter) + return True + + return False diff --git a/src/obj/Object.py b/src/obj/Object.py new file mode 100644 index 0000000..958f70f --- /dev/null +++ b/src/obj/Object.py @@ -0,0 +1,47 @@ +import pygame + + +class Object: + def __init__(self, agent_role, position, orientation, square_size, square_count): + self.agent_role = agent_role + self.position = position + self.orientation = orientation % 4 + self.square_size = square_size + self.square_count = square_count + + 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 goal_test(self, waiter) -> 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) diff --git a/src/obj/Table.py b/src/obj/Table.py new file mode 100644 index 0000000..6d21f19 --- /dev/null +++ b/src/obj/Table.py @@ -0,0 +1,24 @@ +from src.obj.Object import Object + + +class Table(Object): + def __init__(self, position, orientation, square_size, square_count, current_role=1): + super().__init__("table", position, orientation, square_size, square_count) + 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]) + + def goal_test(self, waiter) -> bool: + if self.position == waiter.position: + + if self.agent_role == "order": + return waiter.take_order(self) + elif self.agent_role == "done": + return waiter.drop_order(self) + + return False diff --git a/src/obj/TemporaryState.py b/src/obj/TemporaryState.py new file mode 100644 index 0000000..0dc666b --- /dev/null +++ b/src/obj/TemporaryState.py @@ -0,0 +1,51 @@ +from src.obj.Waiter import Waiter +import copy + + +class TemporaryState(Waiter): + def __init__(self, parent, action="blank"): + super().__init__(copy.deepcopy(parent.position), + copy.copy(parent.orientation), + copy.copy(parent.square_size), + copy.copy(parent.square_count), + copy.copy(parent.basket)) + self.parent = parent + self.change_role(action) + self.apply_transformation() + + 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, "left") + + def right(self): + return TemporaryState(self, "right") + + def front(self): + return TemporaryState(self, "front") + + 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) diff --git a/src/obj/Waiter.py b/src/obj/Waiter.py new file mode 100644 index 0000000..c266f76 --- /dev/null +++ b/src/obj/Waiter.py @@ -0,0 +1,62 @@ +import copy +from src.obj.Object import Object + + +class Waiter(Object): + def __init__(self, position, orientation, square_size, square_count, basket=[]): + super().__init__("waiter", position, orientation, square_size, square_count) + self.basket_size = 2 + self.basket = basket + self.prev_position = copy.deepcopy(self.position) + self.prev_orientation = copy.copy(self.orientation) + + 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 take_order(self, table) -> bool: + if table.agent_role == "order": + if len(self.basket) < self.basket_size: + table.next_role(self) + self.basket.append(table) + return True + return False + + def drop_order(self, table) -> bool: + if table.agent_role == "done": + self.basket.remove(table) + table.next_role(self) + return True + return False + + 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) + + def goal_test(self, engine): + return any([o.goal_test(self) for o in engine.objects])