diff --git a/overcooked_simulator/counters.py b/overcooked_simulator/counters.py index 1151b960feb694bc6eae22ec462bc4fa6379df80..d228137b74097c23197769c8bbffee8260025c5d 100644 --- a/overcooked_simulator/counters.py +++ b/overcooked_simulator/counters.py @@ -10,32 +10,42 @@ import numpy.typing as npt from overcooked_simulator.game_items import ( CuttableItem, - HoldableItem, + Item, ProgressibleItem, Plate, Tomato, + Pot, + CookingEquipment, ) class Counter: """Simple class for a counter at a specified position (center of counter). Can hold things on top.""" - def __init__(self, pos: npt.NDArray[float]): + def __init__(self, pos: npt.NDArray[float], occupied_by: Optional[Item] = None): self.pos: npt.NDArray[float] = pos - self.occupied_by: Optional[HoldableItem] = None + self.occupied_by: Optional[Item] = occupied_by - def pick_up(self): + def pick_up(self, on_hands: bool = True): """Gets called upon a player performing the pickup action. If the counter can give something to the player, it does so. In the standard counter this is when an item is on the counter. Returns: The item which the counter is occupied by. None if nothing is there. """ - give_player = self.occupied_by + if on_hands: + if self.occupied_by: + occupied_by = self.occupied_by + self.occupied_by = None + return occupied_by + return None + if self.occupied_by and isinstance(self.occupied_by, CookingEquipment): + return self.occupied_by.release() + occupied_by = self.occupied_by self.occupied_by = None - return give_player + return occupied_by - def can_drop_off(self, item: HoldableItem) -> bool: + def can_drop_off(self, item: Item) -> bool: """Checks whether an item by the player can be dropped of. More relevant for example with ingredient dispensers, which should always be occupied and cannot take an item. @@ -47,7 +57,7 @@ class Counter: """ return self.occupied_by is None or self.occupied_by.can_combine(item) - def drop_off(self, item: HoldableItem) -> HoldableItem | None: + def drop_off(self, item: Item) -> Item | None: """Takes the thing dropped of by the player. Args: @@ -59,7 +69,7 @@ class Counter: if self.occupied_by is None: self.occupied_by = item elif self.occupied_by.can_combine(item): - self.occupied_by.combine(item) + return self.occupied_by.combine(item) return None def interact_start(self): @@ -71,7 +81,9 @@ class Counter: pass def __repr__(self): - return f"Counter(pos:{str(self.pos)},holds:{self.occupied_by})" + return ( + f"{self.__class__.__name__}(pos={self.pos},occupied_by={self.occupied_by})" + ) class CuttingBoard(Counter): @@ -101,33 +113,31 @@ class CuttingBoard(Counter): """Handles player interaction, stopping to hold key down.""" self.pause_progress() - def __repr__(self): - return f"CuttingBoard({self.occupied_by})" - class ServingWindow(Counter): def __init__(self, pos, game_score: GameScore): self.game_score = game_score super().__init__(pos) - def drop_off(self, item) -> HoldableItem | None: + def drop_off(self, item) -> Item | None: reward = 5 + print(item) # TODO define rewards self.game_score.increment_score(reward) return None def can_score(self, item): - if isinstance(item, Plate) and isinstance(item.holds, ProgressibleItem): - return item.holds.finished + if isinstance(item, Plate): + if isinstance(item.holds, ProgressibleItem): + return item.holds.finished + else: + return bool(item.holds) - def can_drop_off(self, item: HoldableItem) -> bool: + def can_drop_off(self, item: Item) -> bool: return self.can_score(item) - def pick_up(self): - return None - - def __repr__(self): - return "ServingWindow" + def pick_up(self, on_hands: bool = True): + pass class PlateReturn(Counter): @@ -135,7 +145,7 @@ class PlateReturn(Counter): super().__init__(pos) self.occupied_by = [Plate()] - def pick_up(self): + def pick_up(self, on_hands: bool = True): """Gets called upon a player performing the pickup action. Gives back a plate (possibly with ingredient. Returns: A plate possibly with an ingredient on it. @@ -146,7 +156,7 @@ class PlateReturn(Counter): self.occupied_by.append(Plate()) return give_player - def drop_off(self, item: HoldableItem) -> HoldableItem | None: + def drop_off(self, item: Item) -> Item | None: """Takes the ingredient dropped of by the player. Args: @@ -158,11 +168,10 @@ class PlateReturn(Counter): self.occupied_by.append(item) return None if self.occupied_by[-1].can_combine(item): - self.occupied_by[-1].combine(item) - return None + return self.occupied_by[-1].combine(item) return item - def can_drop_off(self, item: HoldableItem) -> bool: + def can_drop_off(self, item: Item) -> bool: """Checks whether an ingredient by the player can be dropped of. Args: @@ -176,39 +185,49 @@ class PlateReturn(Counter): isinstance(self.occupied_by[-1], Plate) and isinstance(item, Plate) ) or self.occupied_by[-1].can_combine(item) - def __repr__(self): - return "PlateReturn" - class TomatoDispenser(Counter): def __init__(self, pos): super().__init__(pos) - def pick_up(self): + def pick_up(self, on_hands: bool = True): return Tomato() - def drop_off(self, item: HoldableItem) -> HoldableItem | None: + def drop_off(self, item: Item) -> Item | None: return None - def can_drop_off(self, item: HoldableItem) -> bool: + def can_drop_off(self, item: Item) -> bool: return False - def __repr__(self): - return f"{self.occupied_by}Dispenser" - class Trash(Counter): - def pick_up(self): + def pick_up(self, on_hands: bool = True): pass - def drop_off(self, item: HoldableItem) -> HoldableItem | None: + def drop_off(self, item: Item) -> Item | None: if isinstance(item, Plate): item.holds = None return item + if isinstance(item, CookingEquipment): + item.content = None + return item return None - def can_drop_off(self, item: HoldableItem) -> bool: + def can_drop_off(self, item: Item) -> bool: return True - def __repr__(self): - return "Trash" + +class Stove(Counter): + def __init__(self, pos: npt.NDArray[float], occupied_by: Optional[Item] = ...): + if occupied_by is ...: + occupied_by = Pot() + super().__init__(pos, occupied_by) + + def progress(self): + """Called by environment step function for time progression""" + if ( + self.occupied_by + and isinstance(self.occupied_by, CookingEquipment) + and self.occupied_by.can_progress() + ): + self.occupied_by.progress() diff --git a/overcooked_simulator/game_items.py b/overcooked_simulator/game_items.py index 986692a6724cae29c3d29260a4541c434f763cc9..1d5098de7b8923d316851859430221a53554abea 100644 --- a/overcooked_simulator/game_items.py +++ b/overcooked_simulator/game_items.py @@ -1,52 +1,93 @@ -class HoldableItem: +from __future__ import annotations + + +class Item: """Base class for game items which can be held by a player.""" + def __init__(self, name: str = None, *args, **kwargs): + super().__init__(*args, **kwargs) + self.name = self.__class__.__name__ if name is None else name + def can_combine(self, other): return False - def combine(self, other): + def combine(self, other) -> Item | None: pass + def __repr__(self): + return f"{self.name}({self.extra_repr})" -class Plate(HoldableItem): - def __init__(self, holds: HoldableItem = None): - self.clean = True - self.holds = holds + @property + def extra_repr(self): + return "" + +class Plate(Item): + def __init__(self, holds: Item = None): super().__init__() + self.clean = True + self.holds = holds - def can_combine(self, other: HoldableItem): - return self.holds is None and not isinstance(other, Plate) + def can_combine(self, other: Item) -> bool: + if self.holds is None: + if isinstance(other, CookingEquipment): + return other.can_release_content() + return not isinstance(other, Plate) + return False def combine(self, other): + if isinstance(other, CookingEquipment): + self.holds = other.release() + return other self.holds = other - def __repr__(self): - return f"Plate({self.holds})" + @property + def extra_repr(self): + return self.holds -class ProgressibleItem(HoldableItem): +class ProgressibleItem: """Class for items which need to be processed (cut, cooked, ...)""" - def __init__(self, steps_needed): - self.progressed_steps = 0 + def __init__( + self, + finished: bool = False, + steps_needed: int = 1500, + finished_name: str = None, + *args, + **kwargs, + ): + super().__init__(*args, **kwargs) + self.progressed_steps = steps_needed if finished else 0 self.steps_needed = steps_needed - self.finished = False - super().__init__() + self.finished = finished + self.finished_name = ( + f"Cutted{self.name}" if finished_name is None else finished_name + ) def progress(self): """Progresses the item process as long as it is not finished.""" if self.progressed_steps >= self.steps_needed: self.finished = True - self.progressed_steps = 0 + self.finished_call() if not self.finished: self.progressed_steps += 1 + def can_progress(self) -> bool: + return True + + def finished_call(self): + self.name = self.finished_name + + def reset(self): + self.finished = False + self.progressed_steps = 0 + def __repr__(self): if self.finished: - return "CutTomato" + return f"{self.name}({self.extra_repr})" else: - return f"{self.__class__.__name__}({int(self.progressed_steps / self.steps_needed * 100)}%)" + return f"{self.name}(progress={int(self.progressed_steps / self.steps_needed * 100)}%,{self.extra_repr})" class CuttableItem(ProgressibleItem): @@ -55,7 +96,7 @@ class CuttableItem(ProgressibleItem): pass -class Tomato(CuttableItem): +class Tomato(CuttableItem, Item): """Item class representing a tomato. Can be cut on the cutting board""" def can_combine(self, other): @@ -63,3 +104,89 @@ class Tomato(CuttableItem): def __init__(self): super().__init__(steps_needed=1500) + + +class CookingEquipment(Item): + def __init__(self, content: Meal = None, *args, **kwargs): + super().__init__(*args, **kwargs) + self.content = content + + def can_combine(self, other): + if self.content is None: + # TODO check other is start of a meal, create meal + return True + return self.content.can_combine(other) + + def combine(self, other): + if not self.content: + # find starting meal for other + self.content = Soup() + self.content.combine(other) + + def can_progress(self, counter_type="Stove") -> bool: + return ( + self.content + and isinstance(self.content, ProgressibleItem) + and self.content.can_progress() + ) + + def progress(self): + self.content.progress() + + def can_release_content(self) -> bool: + return ( + self.content + and isinstance(self.content, ProgressibleItem) + and self.content.finished + ) + + def release(self): + content = self.content + self.content = None + return content + + @property + def extra_repr(self): + return self.content + + +class Pot(CookingEquipment): + def __init__(self, holds: Meal = None): + super().__init__() + + +class Meal(Item): + def __init__(self, parts=None, *args, **kwargs): + super().__init__(*args, **kwargs) + self.parts = [] if parts is None else parts + # self.rules ... + + def can_combine(self, other) -> bool: + return ( + isinstance(other, Tomato) + and all([isinstance(o, other.__class__) for o in self.parts]) + and len(self.parts) < 3 + ) # rules + + def combine(self, other): + self.parts.append(other) + + @property + def extra_repr(self): + return self.parts + + +class Soup(ProgressibleItem, Meal): + def can_progress(self) -> bool: + return len(self.parts) == 3 + + +class Pan(CookingEquipment): + def __init__(self): + super().__init__(steps_needed=1500) + + def can_combine(self, other): + return False + + def combine(self, other): + pass diff --git a/overcooked_simulator/layouts/basic.layout b/overcooked_simulator/layouts/basic.layout index d4d883fc230af82331dec09ce59a95c7d205da68..f077e3ee582829f40eac41f2e2ae1173ea5c2d5e 100644 --- a/overcooked_simulator/layouts/basic.layout +++ b/overcooked_simulator/layouts/basic.layout @@ -1,5 +1,5 @@ EEEEEEEEEEE -ECCCCTCCCCE +ECCUCTCCCCE ECEEEEEEECE ECEEEEEEECE EWEEEEEEEEE diff --git a/overcooked_simulator/overcooked_environment.py b/overcooked_simulator/overcooked_environment.py index 93a9bd93b8f6b054dc662e8e7736ea43d88ad534..8d1650e650c5a6f8f79cd489bd52f620a7de4a4b 100644 --- a/overcooked_simulator/overcooked_environment.py +++ b/overcooked_simulator/overcooked_environment.py @@ -6,7 +6,6 @@ if TYPE_CHECKING: from overcooked_simulator.player import Player from pathlib import Path import numpy as np -import numpy.typing as npt from scipy.spatial import distance_matrix from overcooked_simulator.counters import ( @@ -16,6 +15,7 @@ from overcooked_simulator.counters import ( TomatoDispenser, ServingWindow, PlateReturn, + Stove, ) @@ -69,6 +69,7 @@ class Environment: "T": TomatoDispenser, "P": PlateReturn, "E": None, + "U": Stove, # Stove with pot: U because it looks like a pot } self.counters: list[Counter] = self.create_counters(self.layout_path) @@ -192,7 +193,7 @@ class Environment: old_pos_other = collided_player.pos.copy() collided_player.move(pushing_vector * (collided_player.move_dist / 2)) if self.detect_collision_counters( - collided_player + collided_player ) or self.detect_collision_world_bounds(player): collided_player.move_abs(old_pos_other) player.move_abs(old_pos) @@ -226,9 +227,9 @@ class Environment: Returns: True if the player is intersecting with any object in the environment. """ return ( - len(self.get_collided_players(player)) != 0 - or self.detect_collision_counters(player) - or self.detect_collision_world_bounds(player) + len(self.get_collided_players(player)) != 0 + or self.detect_collision_counters(player) + or self.detect_collision_world_bounds(player) ) def get_collided_players(self, player: Player) -> list[Player]: @@ -323,7 +324,7 @@ class Environment: and time limits. """ for counter in self.counters: - if isinstance(counter, CuttingBoard): + if isinstance(counter, (CuttingBoard, Stove)): counter.progress() def get_state(self): diff --git a/overcooked_simulator/player.py b/overcooked_simulator/player.py index ab028d7197c576ecf6d388255d0e929295bd8e24..8b1260705f93833455ae758600d94bd34b791cfd 100644 --- a/overcooked_simulator/player.py +++ b/overcooked_simulator/player.py @@ -4,7 +4,7 @@ import numpy as np import numpy.typing as npt from overcooked_simulator.counters import Counter -from overcooked_simulator.game_items import HoldableItem +from overcooked_simulator.game_items import Item class Player: @@ -17,7 +17,7 @@ class Player: def __init__(self, name: str, pos: npt.NDArray[float]): self.name: str = name self.pos: npt.NDArray[float] = np.array(pos, dtype=float) - self.holding: Optional[HoldableItem] = None + self.holding: Optional[Item] = None self.radius: int = 18 self.move_dist: int = 5 @@ -81,7 +81,7 @@ class Player: self.holding = counter.drop_off(self.holding) elif self.holding.can_combine(counter.occupied_by): - returned_by_counter = counter.pick_up() + returned_by_counter = counter.pick_up(on_hands=False) self.holding.combine(returned_by_counter) def perform_interact_hold_start(self, counter: Counter): diff --git a/overcooked_simulator/pygame_gui/images/tomato_soup.png b/overcooked_simulator/pygame_gui/images/tomato_soup.png new file mode 100644 index 0000000000000000000000000000000000000000..2a0fbf6568ae68871136acb6c78fa734f0dc14ce Binary files /dev/null and b/overcooked_simulator/pygame_gui/images/tomato_soup.png differ diff --git a/overcooked_simulator/pygame_gui/pygame_gui.py b/overcooked_simulator/pygame_gui/pygame_gui.py index a708b0889ff0210166b1601c310a0d54155d9287..7488799fc767716bb4f6e4ec994545bbee71e9de 100644 --- a/overcooked_simulator/pygame_gui/pygame_gui.py +++ b/overcooked_simulator/pygame_gui/pygame_gui.py @@ -11,8 +11,15 @@ from overcooked_simulator.counters import ( TomatoDispenser, PlateReturn, ServingWindow, + Stove, +) +from overcooked_simulator.game_items import ( + ProgressibleItem, + Plate, + Item, + Pot, + Soup, ) -from overcooked_simulator.game_items import ProgressibleItem, Plate, HoldableItem from overcooked_simulator.game_items import Tomato from overcooked_simulator.overcooked_environment import Action from overcooked_simulator.simulation_runner import Simulator @@ -31,8 +38,9 @@ BACKGROUND_LINES_COLOR = (200, 200, 200) KNIFE_COLOR = (120, 120, 120) PLATE_RETURN_COLOR = (170, 170, 240) BOARD_COLOR = (239, 193, 151) +POT_COLOR = (130, 130, 130) -USE_COOK_SPRITE = False +USE_COOK_SPRITE = True class PlayerKeySet: @@ -194,7 +202,7 @@ class PyGameGUI: holding_item_pos = player.pos + (20 * player.facing_direction) self.draw_item(holding_item_pos, player.holding) - def draw_item(self, pos, item: HoldableItem): + def draw_item(self, pos, item: Item): """Visualisation of an item at the specified position. On a counter or in the hands of the player.""" if isinstance(item, Tomato): if item.finished: @@ -218,6 +226,31 @@ class PyGameGUI: if item.holds is not None: self.draw_item(pos, item.holds) + if isinstance(item, Pot): + pot_size = 15 + pygame.draw.circle(self.screen, GREY, pos, pot_size) + pygame.draw.circle(self.screen, POT_COLOR, pos, pot_size, width=2) + if item.content: + self.draw_item(pos, item.content) + if isinstance(item, Soup): + if not item.finished: + match len(item.parts): + case 1: + pygame.draw.circle(self.screen, RED, pos, 4) + case 2: + pygame.draw.circle(self.screen, RED, pos, 8) + case 3: + pygame.draw.circle(self.screen, RED, pos, 12) + else: + """https://www.readersdigest.ca/wp-content/uploads/2020/11/The-Best-Ever-Tomato-Soup_EXPS_THSO18_222724_D03_06_5b-4.jpg""" + image = pygame.image.load( + self.images_path / "tomato_soup.png" + ).convert_alpha() + image = pygame.transform.scale(image, (24, 24)) + rect = image.get_rect() + rect.center = pos + self.screen.blit(image, rect) + if isinstance(item, ProgressibleItem) and not item.finished: self.draw_progress_bar(pos, item.progressed_steps, item.steps_needed) @@ -289,6 +322,7 @@ class PyGameGUI: ) pygame.draw.rect(self.screen, RED, board_rect) self.draw_item(counter.pos, Tomato()) + if isinstance(counter, ServingWindow): board_size = 33 board_rect = pygame.Rect( @@ -299,6 +333,17 @@ class PyGameGUI: ) pygame.draw.rect(self.screen, YELLOW, board_rect) + if isinstance(counter, Stove): + stove_width = 35 + stove_height = 25 + stove_rect = pygame.Rect( + counter.pos[0] - (stove_width / 2), + counter.pos[1] - (stove_height / 2), + stove_width, + stove_height, + ) + pygame.draw.rect(self.screen, BLACK, stove_rect) + pygame.draw.circle(self.screen, RED, center=counter.pos, radius=10) if counter.occupied_by is not None: if isinstance(counter.occupied_by, list): for i, o in enumerate(counter.occupied_by):