Skip to content
Snippets Groups Projects
Commit ad544b0a authored by Florian Schröder's avatar Florian Schröder
Browse files

PlateReturn is now PlateDispenser (just a Dispenser),

Changed path concatenation,
Created dataclass for item information,
pass item config path via argument of the environment,
no tomato class anymore,
no pot class anymore,
no soup and pan class,
now everything is either a meal, cuttableitem, item, cookingequipment or plate. maybe do the plate as a cooking equipment next.
parent 39b7967b
No related branches found
No related tags found
1 merge request!1340-visualisierungsregeln
Pipeline #41999 failed
...@@ -12,11 +12,9 @@ from overcooked_simulator.game_items import ( ...@@ -12,11 +12,9 @@ from overcooked_simulator.game_items import (
CuttableItem, CuttableItem,
Item, Item,
Plate, Plate,
Pot,
CookingEquipment, CookingEquipment,
Soup, Meal,
) )
from overcooked_simulator.game_items import item_loopkup
class Counter: class Counter:
...@@ -128,7 +126,7 @@ class ServingWindow(Counter): ...@@ -128,7 +126,7 @@ class ServingWindow(Counter):
def can_score(self, item): def can_score(self, item):
if isinstance(item, Plate) and item.holds is not None: if isinstance(item, Plate) and item.holds is not None:
if isinstance(item.holds, Soup): if isinstance(item.holds, Meal):
return item.holds.finished return item.holds.finished
elif item.holds.name == "Salad": elif item.holds.name == "Salad":
return True return True
...@@ -141,79 +139,31 @@ class ServingWindow(Counter): ...@@ -141,79 +139,31 @@ class ServingWindow(Counter):
pass pass
class PlateReturn(Counter):
def __init__(self, pos):
super().__init__(pos)
self.occupied_by = [Plate()]
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.
"""
give_player = self.occupied_by.pop()
if not self.occupied_by:
self.occupied_by.append(Plate())
return give_player
def drop_off(self, item: Item) -> Item | None:
"""Takes the ingredient dropped of by the player.
Args:
item: The ingredient to be placed on the counter.
"""
if isinstance(item, Plate):
if self.occupied_by[-1].holds:
return item
self.occupied_by.append(item)
return None
if self.occupied_by[-1].can_combine(item):
return self.occupied_by[-1].combine(item)
return item
def can_drop_off(self, item: Item) -> bool:
"""Checks whether an ingredient by the player can be dropped of.
Args:
item: The ingredient for which to check, if it can be placed on the counter.
Returns: True if the ingredient can be placed on the counter, False if not.
"""
# possibility to drop off empty plate on empty plate return
return (
isinstance(self.occupied_by[-1], Plate) and isinstance(item, Plate)
) or self.occupied_by[-1].can_combine(item)
class Dispenser(Counter): class Dispenser(Counter):
def __init__(self, pos, dispensing): def __init__(self, pos, dispensing):
self.dispensing = dispensing self.dispensing = dispensing
super().__init__( super().__init__(
pos, pos,
CuttableItem( self.dispensing.create_item(),
name=self.dispensing,
finished_name=item_loopkup[self.dispensing]["finished_name"],
),
) )
def pick_up(self, on_hands: bool = True): def pick_up(self, on_hands: bool = True):
returned = CuttableItem( new_dispensing = self.dispensing.create_item()
name=self.dispensing, if self.occupied_by != new_dispensing:
finished_name=item_loopkup[self.dispensing]["finished_name"], old_dispensing = self.occupied_by
) self.occupied_by = new_dispensing
print(self.occupied_by) return old_dispensing
return returned return new_dispensing
def drop_off(self, item: Item) -> Item | None: def drop_off(self, item: Item) -> Item | None:
return None if self.occupied_by.can_combine(item):
return self.occupied_by.combine(item)
def can_drop_off(self, item: Item) -> bool: def can_drop_off(self, item: Item) -> bool:
return False return self.occupied_by.can_combine(item)
def __repr__(self): def __repr__(self):
return f"{self.dispensing}Dispenser" return f"{self.dispensing.name}Dispenser"
class Trash(Counter): class Trash(Counter):
...@@ -234,11 +184,6 @@ class Trash(Counter): ...@@ -234,11 +184,6 @@ class Trash(Counter):
class Stove(Counter): 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): def progress(self):
"""Called by environment step function for time progression""" """Called by environment step function for time progression"""
if ( if (
......
Tomato: Tomato:
type: Cuttable type: Ingredient
needs: Tomato needs: Tomato
finished_name: ChoppedTomato is_cuttable: True
steps_needed: 500
Lettuce: Lettuce:
type: Cuttable type: Ingredient
needs: Lettuce needs: Lettuce
finished_name: ChoppedLettuce is_cuttable: True
steps_needed: 500
RawPatty: RawPatty:
type: Cuttable type: Ingredient
needs: RawSteak needs: RawSteak
Burger: Burger:
...@@ -19,8 +21,17 @@ Burger: ...@@ -19,8 +21,17 @@ Burger:
Salad: Salad:
type: Meal type: Meal
needs: [ Lettuce, Tomato ] needs: [ Lettuce, Tomato ]
equipment: Plate
TomatoSoup: TomatoSoup:
type: Soup type: Meal
finished_progress_name: TomatoSoup
steps_needed: 500
needs: [ Tomato, Tomato, Tomato ] needs: [ Tomato, Tomato, Tomato ]
equipment: Pot
Plate:
type: Equipment
Pot:
type: Equipment
from __future__ import annotations from __future__ import annotations
import yaml import dataclasses
from enum import Enum
from overcooked_simulator import ROOT_DIR
with open(ROOT_DIR / "game_content/item_combinations.yaml", "r") as file: class ItemType(Enum):
item_loopkup = yaml.safe_load(file) Ingredient = "Ingredient"
Meal = "Meal"
Equipment = "Equipment"
@dataclasses.dataclass
class ItemInfo:
type: ItemType = dataclasses.field(compare=False)
name: str = dataclasses.field(compare=True)
is_cuttable: bool = dataclasses.field(compare=False, default=False)
steps_needed: int = dataclasses.field(compare=False, default=0)
finished_progress_name: str = dataclasses.field(compare=False, default="Chopped*")
needs: list[ItemInfo] = dataclasses.field(compare=False, default_factory=list)
equipment: ItemInfo | None = dataclasses.field(compare=False, default=None)
_start_items: list[ItemInfo] = dataclasses.field(
compare=False, default_factory=list
)
def __post_init__(self):
self.type = ItemType(self.type)
def get_finished_name(self):
if "*" in self.finished_progress_name:
return self.finished_progress_name.replace("*", self.name)
return self.name
def create_item(self) -> Item:
match self.type:
case ItemType.Ingredient:
if self.is_cuttable:
return CuttableItem(
name=self.name,
finished=False,
steps_needed=self.steps_needed,
finished_name=self.get_finished_name(),
item_info=self,
)
return Item(name=self.name, item_info=self)
case ItemType.Equipment:
if self.name == "Plate":
return Plate(item_info=self)
if self.name == "Pot":
return CookingEquipment(name=self.name, item_info=self)
case ItemType.Meal:
return Meal(
name=self.name,
finished=False,
steps_needed=self.steps_needed,
finished_name=self.get_finished_name(),
item_info=self,
)
def add_start_item_to_equipment(self, start_item: ItemInfo):
self._start_items.append(start_item)
def can_start_meal(self, start_item: Item):
# TODO check specific order / only specific start items
return any(
[start_item.name == s for meal in self._start_items for s in meal.needs]
)
combinables = {} def start_meal(self, start_item: Item) -> Item:
for key, item in item_loopkup.items(): for meal in self._start_items:
if item["type"] in ["Meal", "Soup"]: for s in meal.needs:
combinables[key] = item["needs"] if s == start_item.name:
print(combinables) return meal.create_item()
class Item: class Item:
"""Base class for game items which can be held by a player.""" """Base class for game items which can be held by a player."""
def __init__(self, name: str = None, *args, **kwargs): def __init__(self, name: str, item_info: ItemInfo, *args, **kwargs):
super().__init__(*args, **kwargs)
self.name = self.__class__.__name__ if name is None else name self.name = self.__class__.__name__ if name is None else name
self.item_info = item_info
def can_combine(self, other): def can_combine(self, other):
return False return False
...@@ -30,14 +90,17 @@ class Item: ...@@ -30,14 +90,17 @@ class Item:
def __repr__(self): def __repr__(self):
return f"{self.name}({self.extra_repr})" return f"{self.name}({self.extra_repr})"
def __eq__(self, other):
return other and self.name == other.name
@property @property
def extra_repr(self): def extra_repr(self):
return "" return ""
class Plate(Item): class Plate(Item):
def __init__(self, holds: Item = None): def __init__(self, holds: Item = None, *args, **kwargs):
super().__init__() super().__init__(*args, name="Plate", **kwargs)
self.clean = True self.clean = True
self.holds = holds self.holds = holds
...@@ -54,6 +117,14 @@ class Plate(Item): ...@@ -54,6 +117,14 @@ class Plate(Item):
return other return other
self.holds = other self.holds = other
def __eq__(self, other):
return (
other
and self.name == other.name
and self.clean == other.clean
and self.holds == other.holds
)
@property @property
def extra_repr(self): def extra_repr(self):
return self.holds return self.holds
...@@ -106,18 +177,6 @@ class ProgressibleItem(Item): ...@@ -106,18 +177,6 @@ class ProgressibleItem(Item):
class CuttableItem(ProgressibleItem): class CuttableItem(ProgressibleItem):
"""Class of item which can be processed by the cutting board.""" """Class of item which can be processed by the cutting board."""
pass
class Tomato(CuttableItem, Item):
"""Item class representing a tomato. Can be cut on the cutting board"""
def can_combine(self, other):
return False
def __init__(self):
super().__init__(steps_needed=500)
class CookingEquipment(Item): class CookingEquipment(Item):
def __init__(self, content: Meal = None, *args, **kwargs): def __init__(self, content: Meal = None, *args, **kwargs):
...@@ -127,13 +186,13 @@ class CookingEquipment(Item): ...@@ -127,13 +186,13 @@ class CookingEquipment(Item):
def can_combine(self, other): def can_combine(self, other):
if self.content is None: if self.content is None:
# TODO check other is start of a meal, create meal # TODO check other is start of a meal, create meal
return True return self.item_info.can_start_meal(other)
return self.content.can_combine(other) return self.content.can_combine(other)
def combine(self, other): def combine(self, other):
if not self.content: if not self.content:
# find starting meal for other # find starting meal for other
self.content = Soup() self.content = self.item_info.start_meal(other)
self.content.combine(other) self.content.combine(other)
def can_progress(self, counter_type="Stove") -> bool: def can_progress(self, counter_type="Stove") -> bool:
...@@ -163,46 +222,36 @@ class CookingEquipment(Item): ...@@ -163,46 +222,36 @@ class CookingEquipment(Item):
return self.content return self.content
class Pot(CookingEquipment): class Meal(ProgressibleItem):
def __init__(self, holds: Meal = None):
super().__init__()
class Meal(Item):
def __init__(self, parts=None, *args, **kwargs): def __init__(self, parts=None, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.parts = [] if parts is None else parts self.parts = [] if parts is None else parts
# self.rules ... # self.rules ...
def can_combine(self, other) -> bool: def can_combine(self, other) -> bool:
return ( if other and not self.finished:
isinstance(other, Tomato) satisfied = [False for _ in range(len(self.parts))]
and all([isinstance(o, other.__class__) for o in self.parts]) for n in self.item_info.needs:
and len(self.parts) < 3 for i, p in enumerate(self.parts):
) # rules if not satisfied[i] and p.name == n:
satisfied[i] = True
break
else:
if n == other.name:
return True
return False
def combine(self, other): def combine(self, other):
self.parts.append(other) self.parts.append(other)
@property
def extra_repr(self):
return self.parts
class Soup(ProgressibleItem, Meal):
def __init__(self):
super().__init__(finished_name="CookedSoup")
def can_progress(self) -> bool: def can_progress(self) -> bool:
return len(self.parts) == 3 return self.item_info.steps_needed and len(self.item_info.needs) == len(
self.parts
)
class Pan(CookingEquipment):
def __init__(self):
super().__init__(steps_needed=500)
def can_combine(self, other): def finished_call(self):
return False super().finished_call()
def combine(self, other): @property
pass def extra_repr(self):
return self.parts
import sys import sys
from pathlib import Path
import numpy as np import numpy as np
import pygame import pygame
...@@ -11,7 +10,7 @@ from overcooked_simulator.simulation_runner import Simulator ...@@ -11,7 +10,7 @@ from overcooked_simulator.simulation_runner import Simulator
def main(): def main():
simulator = Simulator(Path(ROOT_DIR, "game_content/layouts", "basic.layout"), 600) simulator = Simulator(ROOT_DIR / "game_content" / "layouts" / "basic.layout", 600)
player_one_name = "p1" player_one_name = "p1"
player_two_name = "p2" player_two_name = "p2"
simulator.register_player(Player(player_one_name, np.array([350.0, 200.0]))) simulator.register_player(Player(player_one_name, np.array([350.0, 200.0])))
......
...@@ -2,6 +2,10 @@ from __future__ import annotations ...@@ -2,6 +2,10 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import yaml
from overcooked_simulator.game_items import ItemInfo
if TYPE_CHECKING: if TYPE_CHECKING:
from overcooked_simulator.player import Player from overcooked_simulator.player import Player
from pathlib import Path from pathlib import Path
...@@ -14,7 +18,6 @@ from overcooked_simulator.counters import ( ...@@ -14,7 +18,6 @@ from overcooked_simulator.counters import (
Trash, Trash,
Dispenser, Dispenser,
ServingWindow, ServingWindow,
PlateReturn,
Stove, Stove,
) )
...@@ -55,10 +58,12 @@ class Environment: ...@@ -55,10 +58,12 @@ class Environment:
# TODO Abstract base class for different environments # TODO Abstract base class for different environments
""" """
def __init__(self, layout_path): def __init__(self, layout_path, item_info_path):
self.players: dict[str, Player] = {} self.players: dict[str, Player] = {}
self.counter_side_length: int = 40 self.counter_side_length: int = 40
self.layout_path: Path = layout_path self.layout_path: Path = layout_path
self.item_info_path: Path = item_info_path
self.item_info = self.load_item_info()
self.game_score = GameScore() self.game_score = GameScore()
self.SYMBOL_TO_CHARACTER_MAP = { self.SYMBOL_TO_CHARACTER_MAP = {
...@@ -66,11 +71,14 @@ class Environment: ...@@ -66,11 +71,14 @@ class Environment:
"B": CuttingBoard, "B": CuttingBoard,
"X": Trash, "X": Trash,
"W": lambda pos: ServingWindow(pos, self.game_score), "W": lambda pos: ServingWindow(pos, self.game_score),
"T": lambda pos: Dispenser(pos, "Tomato"), "T": lambda pos: Dispenser(pos, self.item_info["Tomato"]),
"L": lambda pos: Dispenser(pos, "Lettuce"), "L": lambda pos: Dispenser(pos, self.item_info["Lettuce"]),
"P": PlateReturn, "P": lambda pos: Dispenser(pos, self.item_info["Plate"]),
"E": None, "E": None,
"U": Stove, # Stove with pot: U because it looks like a pot "U": lambda pos: Stove(
pos,
self.item_info["Pot"].create_item(),
), # Stove with pot: U because it looks like a pot
} }
self.counters: list[Counter] = self.create_counters(self.layout_path) self.counters: list[Counter] = self.create_counters(self.layout_path)
...@@ -78,6 +86,18 @@ class Environment: ...@@ -78,6 +86,18 @@ class Environment:
self.world_width: int = 800 self.world_width: int = 800
self.world_height: int = 600 self.world_height: int = 600
def load_item_info(self) -> dict[str, ItemInfo]:
with open(self.item_info_path, "r") as file:
item_lookup = yaml.safe_load(file)
for item_name in item_lookup:
item_lookup[item_name] = ItemInfo(name=item_name, **item_lookup[item_name])
for item_name, item_info in item_lookup.items():
if item_info.equipment:
item_info.equipment = item_lookup[item_info.equipment]
item_info.equipment.add_start_item_to_equipment(item_info)
return item_lookup
def create_counters(self, layout_file: Path): def create_counters(self, layout_file: Path):
"""Creates layout of kitchen counters in the environment based on layout file. """Creates layout of kitchen counters in the environment based on layout file.
Counters are arranged in a fixed size grid starting at [0,0]. The center of the first counter is at Counters are arranged in a fixed size grid starting at [0,0]. The center of the first counter is at
......
import math import math
from pathlib import Path
import numpy as np import numpy as np
import numpy.typing as npt import numpy.typing as npt
...@@ -13,7 +12,6 @@ from overcooked_simulator.game_items import ( ...@@ -13,7 +12,6 @@ from overcooked_simulator.game_items import (
Item, Item,
CookingEquipment, CookingEquipment,
Meal, Meal,
Soup,
) )
from overcooked_simulator.overcooked_environment import Action from overcooked_simulator.overcooked_environment import Action
from overcooked_simulator.simulation_runner import Simulator from overcooked_simulator.simulation_runner import Simulator
...@@ -84,10 +82,10 @@ class PyGameGUI: ...@@ -84,10 +82,10 @@ class PyGameGUI:
for player_name, keys in zip(self.player_names, self.player_keys) for player_name, keys in zip(self.player_names, self.player_keys)
] ]
with open(ROOT_DIR / "pygame_gui/visualization.yaml", "r") as file: with open(ROOT_DIR / "pygame_gui" / "visualization.yaml", "r") as file:
self.visualization_config = yaml.safe_load(file) self.visualization_config = yaml.safe_load(file)
self.images_path = Path(ROOT_DIR, "pygame_gui", "images") self.images_path = ROOT_DIR / "pygame_gui" / "images"
def send_action(self, action: Action): def send_action(self, action: Action):
"""Sends an action to the game environment. """Sends an action to the game environment.
...@@ -179,7 +177,7 @@ class PyGameGUI: ...@@ -179,7 +177,7 @@ class PyGameGUI:
else: else:
img_path = self.visualization_config["Cook"]["parts"][0]["path"] img_path = self.visualization_config["Cook"]["parts"][0]["path"]
image = pygame.image.load( image = pygame.image.load(
ROOT_DIR / Path("pygame_gui") / img_path ROOT_DIR / "pygame_gui" / img_path
).convert_alpha() ).convert_alpha()
rel_x, rel_y = player.facing_direction rel_x, rel_y = player.facing_direction
angle = -np.rad2deg(math.atan2(rel_y, rel_x)) + 90 angle = -np.rad2deg(math.atan2(rel_y, rel_x)) + 90
...@@ -206,7 +204,7 @@ class PyGameGUI: ...@@ -206,7 +204,7 @@ class PyGameGUI:
part_type = part["type"] part_type = part["type"]
if part_type == "image": if part_type == "image":
image = pygame.image.load( image = pygame.image.load(
ROOT_DIR / Path("pygame_gui") / parts[0]["path"] ROOT_DIR / "pygame_gui" / parts[0]["path"]
).convert_alpha() ).convert_alpha()
size = parts[0]["size"] size = parts[0]["size"]
...@@ -267,25 +265,13 @@ class PyGameGUI: ...@@ -267,25 +265,13 @@ class PyGameGUI:
if isinstance(item, CookingEquipment) and item.content: if isinstance(item, CookingEquipment) and item.content:
self.draw_item(pos, item.content) self.draw_item(pos, item.content)
if isinstance(item, Meal) and item.parts and not isinstance(item, Soup): if isinstance(item, Meal):
for i, o in enumerate(item.parts): if "Soup" in item.name:
self.draw_item(np.abs([pos[0], pos[1] - (i * 5)]), o) if item.finished:
if isinstance(item, Soup) and item.parts:
if item.parts:
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:
if item.name in self.visualization_config: if item.name in self.visualization_config:
image = pygame.image.load( image = pygame.image.load(
ROOT_DIR ROOT_DIR
/ Path("pygame_gui") / "pygame_gui"
/ self.visualization_config[item.name]["parts"][0]["path"] / self.visualization_config[item.name]["parts"][0]["path"]
).convert_alpha() ).convert_alpha()
...@@ -295,6 +281,17 @@ class PyGameGUI: ...@@ -295,6 +281,17 @@ class PyGameGUI:
rect = image.get_rect() rect = image.get_rect()
rect.center = pos rect.center = pos
self.screen.blit(image, rect) self.screen.blit(image, rect)
elif item.parts:
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:
for i, o in enumerate(item.parts):
self.draw_item(np.abs([pos[0], pos[1] - (i * 5)]), o)
def draw_progress_bar(self, pos, current, needed): def draw_progress_bar(self, pos, current, needed):
"""Visualize progress of progressing item as a green bar under the item.""" """Visualize progress of progressing item as a green bar under the item."""
......
...@@ -17,7 +17,7 @@ CuttingBoard: ...@@ -17,7 +17,7 @@ CuttingBoard:
center_offset: [ +6, -8 ] center_offset: [ +6, -8 ]
color: [ 120, 120, 120 ] color: [ 120, 120, 120 ]
PlateReturn: PlateDispenser:
parts: parts:
- type: "rect" - type: "rect"
height: 38 height: 38
...@@ -129,7 +129,7 @@ TomatoSoup3of3: ...@@ -129,7 +129,7 @@ TomatoSoup3of3:
radius: 12 radius: 12
color: [ 255, 0, 0 ] color: [ 255, 0, 0 ]
CookedSoup: TomatoSoup:
parts: parts:
- type: "image" - type: "image"
path: "images/tomato_soup.png" path: "images/tomato_soup.png"
......
import time import time
from threading import Thread from threading import Thread
from overcooked_simulator import ROOT_DIR
from overcooked_simulator.overcooked_environment import Environment, Action from overcooked_simulator.overcooked_environment import Environment, Action
from overcooked_simulator.player import Player from overcooked_simulator.player import Player
...@@ -19,12 +20,17 @@ class Simulator(Thread): ...@@ -19,12 +20,17 @@ class Simulator(Thread):
``` ```
""" """
def __init__(self, env_layout_path, frequency: int): def __init__(
self,
env_layout_path,
frequency: int,
item_info_path=ROOT_DIR / "game_content" / "item_info.yaml",
):
self.finished: bool = False self.finished: bool = False
self.step_frequency: int = frequency self.step_frequency: int = frequency
self.preferred_sleep_time_ns: float = 1e9 / self.step_frequency self.preferred_sleep_time_ns: float = 1e9 / self.step_frequency
self.env: Environment = Environment(env_layout_path) self.env: Environment = Environment(env_layout_path, item_info_path)
super().__init__() super().__init__()
......
import time import time
from pathlib import Path
import numpy as np import numpy as np
import pytest import pytest
...@@ -11,7 +10,7 @@ from overcooked_simulator.overcooked_environment import Action ...@@ -11,7 +10,7 @@ from overcooked_simulator.overcooked_environment import Action
from overcooked_simulator.player import Player from overcooked_simulator.player import Player
from overcooked_simulator.simulation_runner import Simulator from overcooked_simulator.simulation_runner import Simulator
layouts_folder = Path(ROOT_DIR / "game_content/layouts") layouts_folder = ROOT_DIR / "game_content" / "layouts"
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment