diff --git a/overcooked_simulator/counters.py b/overcooked_simulator/counters.py index 9fe8eee9fd3d50108ea1dfc9b2c15635e556434d..41bd67eeb5c10e23ea15538dd90fe9ca1afce515 100644 --- a/overcooked_simulator/counters.py +++ b/overcooked_simulator/counters.py @@ -3,13 +3,11 @@ from __future__ import annotations import logging from collections import deque from datetime import datetime, timedelta -from typing import TYPE_CHECKING, Optional - -from overcooked_simulator.utils import create_init_env_time +from typing import TYPE_CHECKING, Optional, Callable if TYPE_CHECKING: from overcooked_simulator.overcooked_environment import ( - GameScore, + OrderAndScoreManager, ) import numpy as np @@ -144,23 +142,24 @@ class ServingWindow(Counter): def __init__( self, pos, - game_score: GameScore, + order_and_score: OrderAndScoreManager, meals: set[str], + env_time_func: Callable[[], datetime], plate_dispenser: PlateDispenser = None, ): - self.game_score = game_score + self.order_and_score = order_and_score self.plate_dispenser = plate_dispenser self.meals = meals + self.env_time_func = env_time_func super().__init__(pos) def drop_off(self, item) -> Item | None: - reward = 5 - log.debug(f"Drop off item {item}") - # TODO define rewards - self.game_score.increment_score(reward) - if self.plate_dispenser is not None: - self.plate_dispenser.update_plate_out_of_kitchen() - return None + env_time = self.env_time_func() + if self.order_and_score.serve_meal(item=item, env_time=env_time): + if self.plate_dispenser is not None: + self.plate_dispenser.update_plate_out_of_kitchen(env_time=env_time) + return None + return item def can_drop_off(self, item: Item) -> bool: return isinstance(item, CookingEquipment) and ( @@ -217,7 +216,6 @@ class PlateDispenser(Counter): self.plate_config = {"plate_delay": [5, 10]} self.plate_config.update(plate_config) self.next_plate_time = datetime.max - self.env_time = create_init_env_time() # is overwritten in progress anyway self.plate_transitions = plate_transitions self.setup_plates() @@ -246,10 +244,10 @@ class PlateDispenser(Counter): def add_dirty_plate(self): self.occupied_by.appendleft(self.create_item()) - def update_plate_out_of_kitchen(self): + def update_plate_out_of_kitchen(self, env_time: datetime): """Is called from the serving window to add a plate out of kitchen.""" # not perfect identical to datetime.now but based on framerate enough. - time_plate_to_add = self.env_time + timedelta( + time_plate_to_add = env_time + timedelta( seconds=np.random.uniform( low=self.plate_config["plate_delay"][0], high=self.plate_config["plate_delay"][1], @@ -276,7 +274,6 @@ class PlateDispenser(Counter): def progress(self, passed_time: timedelta, now: datetime): """Check if plates arrive from outside the kitchen and add a dirty plate accordingly""" - self.env_time = now if self.next_plate_time < now: idx_delete = [] for i, times in enumerate(self.out_of_kitchen_timer): @@ -368,6 +365,7 @@ class Sink(Counter): "result" ] plate = self.occupied_by.pop() + plate.clean = True self.sink_addon.add_clean_plate(plate) def start_progress(self): diff --git a/overcooked_simulator/game_content/environment_config.yaml b/overcooked_simulator/game_content/environment_config.yaml index a5b77f8b13f33361009a5aedab5d6c91da98c907..b6b7c579ec950edd189c41e2ec09a5cbda2a239f 100644 --- a/overcooked_simulator/game_content/environment_config.yaml +++ b/overcooked_simulator/game_content/environment_config.yaml @@ -1,7 +1,63 @@ plates: - clean_plates: 3 + clean_plates: 1 dirty_plates: 2 plate_delay: [ 5, 10 ] + # seconds until the dirty plate arrives. game: time_limit_seconds: 180 + +meals: + all: false + # if all: false -> only orders for these meals are generated + # TODO: what if this list is empty? + list: + - TomatoSoup + - OnionSoup + - Salad + +orders: + kwargs: + duration_sample: + # how long should the orders be alive + # 'random' library call with getattr, kwargs are passed to the function + func: uniform + kwargs: + a: 40 + b: 60 + max_orders: 6 + # maximum number of active orders at the same time + num_start_meals: 3 + # number of orders generated at the start of the environment + sample_on_dur: true + # if true, the next order is generated based on the sample_on_dur_func method in seconds + # if sample_on_serving is also true, the value is sampled after a meal was served, otherwise it is sampled directly after an order generation. + sample_on_dur_func: + # 'random' library call with getattr, kwargs are passed to the function + func: uniform + kwargs: + a: 10 + b: 20 + sample_on_serving: false + # The sample time for a new incoming order is only generated after a meal was served. + score_calc_gen_func: !!python/name:overcooked_simulator.order.simple_score_calc_gen_func '' + score_calc_gen_kwargs: + # the kwargs for the score_calc_gen_func + other: 0 + scores: + Burger: 15 + OnionSoup: 10 + Salad: 5 + TomatoSoup: 10 + expired_penalty_func: !!python/name:overcooked_simulator.order.simple_expired_penalty '' + expired_penalty_kwargs: + default: -5 + serving_not_ordered_meals: null + # a func that calcs a store for not ordered but served meals. Input: meal + order_gen_class: !!python/name:overcooked_simulator.order.RandomOrderGeneration '' + # the class to that receives the kwargs. Should be a child class of OrderGeneration in order.py + +player_config: + radius: 0.4 + move_dist: 0.15 + interaction_range: 1.6 \ No newline at end of file diff --git a/overcooked_simulator/game_content/item_info.yaml b/overcooked_simulator/game_content/item_info.yaml index f87d661701f6d6e2a6465addaf395b3722f3f1ca..777e5f28e106587285ecf8eb6d1e77e23911914e 100644 --- a/overcooked_simulator/game_content/item_info.yaml +++ b/overcooked_simulator/game_content/item_info.yaml @@ -44,30 +44,30 @@ Bun: ChoppedTomato: type: Ingredient needs: [ Tomato ] - seconds: 0.1 + seconds: 4.0 equipment: CuttingBoard ChoppedLettuce: type: Ingredient needs: [ Lettuce ] - seconds: 0.1 + seconds: 3.0 equipment: CuttingBoard ChoppedOnion: type: Ingredient needs: [ Onion ] - seconds: 0.1 + seconds: 5.0 equipment: CuttingBoard ChoppedMeat: type: Ingredient needs: [ Meat ] - seconds: 0.1 + seconds: 4.0 equipment: CuttingBoard CookedPatty: type: Ingredient - seconds: 2.0 + seconds: 5.0 needs: [ ChoppedMeat ] equipment: Pan @@ -86,11 +86,11 @@ Salad: TomatoSoup: type: Meal needs: [ ChoppedTomato, ChoppedTomato, ChoppedTomato ] - seconds: 3.0 + seconds: 6.0 equipment: Pot OnionSoup: type: Meal needs: [ ChoppedOnion, ChoppedOnion, ChoppedOnion ] - seconds: 3.0 + seconds: 6.0 equipment: Pot diff --git a/overcooked_simulator/game_content/item_info_debug.yaml b/overcooked_simulator/game_content/item_info_debug.yaml new file mode 100644 index 0000000000000000000000000000000000000000..32c8696610ab0e1b3f059071da01d7c182e6b80f --- /dev/null +++ b/overcooked_simulator/game_content/item_info_debug.yaml @@ -0,0 +1,96 @@ +CuttingBoard: + type: Equipment + +Sink: + type: Equipment + +Stove: + type: Equipment + +Pot: + type: Equipment + equipment: Stove + +Pan: + type: Equipment + equipment: Stove + +DirtyPlate: + type: Equipment + +Plate: + type: Equipment + needs: [ DirtyPlate ] + seconds: 1.0 + equipment: Sink + +# -------------------------------------------------------------------------------- + +Tomato: + type: Ingredient + +Lettuce: + type: Ingredient + +Onion: + type: Ingredient + +Meat: + type: Ingredient + +Bun: + type: Ingredient + +ChoppedTomato: + type: Ingredient + needs: [ Tomato ] + seconds: 0.1 + equipment: CuttingBoard + +ChoppedLettuce: + type: Ingredient + needs: [ Lettuce ] + seconds: 0.1 + equipment: CuttingBoard + +ChoppedOnion: + type: Ingredient + needs: [ Onion ] + seconds: 0.1 + equipment: CuttingBoard + +ChoppedMeat: + type: Ingredient + needs: [ Meat ] + seconds: 0.1 + equipment: CuttingBoard + +CookedPatty: + type: Ingredient + seconds: 2.0 + needs: [ ChoppedMeat ] + equipment: Pan + +# -------------------------------------------------------------------------------- + +Burger: + type: Meal + needs: [ Bun, ChoppedLettuce, ChoppedTomato, CookedPatty ] + equipment: ~ + +Salad: + type: Meal + needs: [ ChoppedLettuce, ChoppedTomato ] + equipment: ~ + +TomatoSoup: + type: Meal + needs: [ ChoppedTomato, ChoppedTomato, ChoppedTomato ] + seconds: 3.0 + equipment: Pot + +OnionSoup: + type: Meal + needs: [ ChoppedOnion, ChoppedOnion, ChoppedOnion ] + seconds: 3.0 + equipment: Pot diff --git a/overcooked_simulator/game_content/player_config.yaml b/overcooked_simulator/game_content/player_config.yaml deleted file mode 100644 index b684cabe240ed9db7ae5f835a329a4ef0a9ccd3a..0000000000000000000000000000000000000000 --- a/overcooked_simulator/game_content/player_config.yaml +++ /dev/null @@ -1,3 +0,0 @@ -radius: 0.4 -move_dist: 0.15 -interaction_range: 1.6 \ No newline at end of file diff --git a/overcooked_simulator/game_items.py b/overcooked_simulator/game_items.py index cb674e41e05470e04fdd2ea38736fe50f9ed35a4..4592e68bbe4057719b3618fbf7b34513fb4d232b 100644 --- a/overcooked_simulator/game_items.py +++ b/overcooked_simulator/game_items.py @@ -129,6 +129,7 @@ class CookingEquipment(Item): ingredients = collections.Counter( item.name for item in self.content_list + other ) + print(ingredients) return any( ingredients <= recipe["recipe"] for recipe in self.transitions.values() ) @@ -196,12 +197,25 @@ class CookingEquipment(Item): super().reset() self.active_transition = None + def get_potential_meal(self) -> Item | None: + if self.content_ready: + return self.content_ready + if len(self.content_list) == 1: + return self.content_list[0] + return None + class Plate(CookingEquipment): def __init__(self, transitions, clean, *args, **kwargs): self.clean = clean + self.meals = set(transitions.keys()) super().__init__( - name=self.create_name(), transitions=transitions, *args, **kwargs + name=self.create_name(), + transitions={ + k: v for k, v in transitions.items() if not v["info"].equipment + }, + *args, + **kwargs, ) def finished_call(self): @@ -222,7 +236,8 @@ class Plate(CookingEquipment): and not self.content_list and self.clean ): - return other.content_list[0].name in self.transitions + return other.content_list[0].name in self.meals return False - else: + elif self.clean: return True + return False diff --git a/overcooked_simulator/gui_2d_vis/gui_theme.json b/overcooked_simulator/gui_2d_vis/gui_theme.json index 4683e4e3ec100dd15772c1a1ff2bc58f75260e08..862d3d963c63ae0abe29c2c37516be2bc750b1cf 100644 --- a/overcooked_simulator/gui_2d_vis/gui_theme.json +++ b/overcooked_simulator/gui_2d_vis/gui_theme.json @@ -56,7 +56,7 @@ }, "font": { "size": 20, - "bold": 1 + "bold": 0 } }, "#score_label": { @@ -68,6 +68,15 @@ "bold": 1 } }, + "#orders_label": { + "colours": { + "normal_text": "#000000" + }, + "font": { + "size": 20, + "bold": 0 + } + }, "#quit_button": { "colours": { "normal_bg": "#f71b29", diff --git a/overcooked_simulator/gui_2d_vis/images/burger.png b/overcooked_simulator/gui_2d_vis/images/burger.png new file mode 100644 index 0000000000000000000000000000000000000000..df84a65fcca2ff5edcc9e258fe80a4005dc0716e Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/burger.png differ diff --git a/overcooked_simulator/gui_2d_vis/images/salad.png b/overcooked_simulator/gui_2d_vis/images/salad.png new file mode 100644 index 0000000000000000000000000000000000000000..e227f75660c7c89e4ed7604dc38983bfc084f769 Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/salad.png differ diff --git a/overcooked_simulator/gui_2d_vis/images/trash3.png b/overcooked_simulator/gui_2d_vis/images/trash3.png new file mode 100644 index 0000000000000000000000000000000000000000..5a391dc446197664636119fd360eeace6cb17b3c Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/trash3.png differ diff --git a/overcooked_simulator/gui_2d_vis/overcooked_gui.py b/overcooked_simulator/gui_2d_vis/overcooked_gui.py index 6686fef3779a22611a20107720d41fb6e9aec38e..0e7db7ccea58611bfa917562639d4b9dfcae5c68 100644 --- a/overcooked_simulator/gui_2d_vis/overcooked_gui.py +++ b/overcooked_simulator/gui_2d_vis/overcooked_gui.py @@ -22,6 +22,7 @@ from overcooked_simulator.game_items import ( ) from overcooked_simulator.gui_2d_vis.game_colors import BLUE from overcooked_simulator.gui_2d_vis.game_colors import colors, Color +from overcooked_simulator.order import Order from overcooked_simulator.overcooked_environment import Action from overcooked_simulator.simulation_runner import Simulator @@ -119,6 +120,10 @@ class PyGameGUI: self.buttons_width = self.visualization_config["GameWindow"]["buttons_width"] self.buttons_height = self.visualization_config["GameWindow"]["buttons_height"] + self.order_bar_height = self.visualization_config["GameWindow"][ + "order_bar_height" + ] + self.window_width = self.min_width self.window_height = self.min_height @@ -259,7 +264,9 @@ class PyGameGUI: 1, ) - def draw_image(self, img_path, size, pos, rot_angle=0): + def draw_image( + self, img_path, size, pos, rot_angle=0, screen: pygame.Surface = None + ): cache_entry = f"{img_path}" if cache_entry in self.image_cache_dict.keys(): image = self.image_cache_dict[cache_entry] @@ -274,7 +281,11 @@ class PyGameGUI: image = pygame.transform.rotate(image, rot_angle) rect = image.get_rect() rect.center = pos - self.game_screen.blit(image, rect) + + if screen is None: + self.game_screen.blit(image, rect) + else: + screen.blit(image, rect) def draw_players(self, state): """Visualizes the players as circles with a triangle for the facing direction. @@ -362,7 +373,11 @@ class PyGameGUI: ) def draw_thing( - self, pos: npt.NDArray[float], parts: list[dict[str]], scale: float = 1.0 + self, + pos: npt.NDArray[float], + parts: list[dict[str]], + scale: float = 1.0, + screen: pygame.Surface = None, ): """Draws an item, based on its visual parts specified in the visualization config. @@ -372,6 +387,9 @@ class PyGameGUI: scale: Rescale the item by this factor. """ + if screen is None: + screen = self.game_screen + for part in parts: part_type = part["type"] match part_type: @@ -381,9 +399,10 @@ class PyGameGUI: pos += d self.draw_image( - parts[0]["path"], - parts[0]["size"] * scale * self.grid_size, + part["path"], + part["size"] * scale * self.grid_size, pos, + screen=screen, ) case "rect": height = part["height"] * self.grid_size @@ -392,7 +411,7 @@ class PyGameGUI: if "center_offset" in part: dx, dy = np.array(part["center_offset"]) * self.grid_size rect = pygame.Rect(pos[0] + dx, pos[1] + dy, height, width) - pygame.draw.rect(self.game_screen, color, rect) + pygame.draw.rect(screen, color, rect) else: rect = pygame.Rect( pos[0] - (height / 2), @@ -400,7 +419,7 @@ class PyGameGUI: height, width, ) - pygame.draw.rect(self.game_screen, color, rect) + pygame.draw.rect(screen, color, rect) case "circle": radius = part["radius"] * self.grid_size color = colors[part["color"]] @@ -412,10 +431,15 @@ class PyGameGUI: radius, ) else: - pygame.draw.circle(self.game_screen, color, pos, radius) + pygame.draw.circle(screen, color, pos, radius) def draw_item( - self, pos: npt.NDArray[float], item: Item, scale: float = 1.0, plate=False + self, + pos: npt.NDArray[float], + item: Item, + scale: float = 1.0, + plate=False, + screen=None, ): """Visualization of an item at the specified position. On a counter or in the hands of the player. The visual composition of the item is read in from visualization.yaml file, where it is specified as @@ -425,6 +449,8 @@ class PyGameGUI: pos: The position of the item to draw. item: The item do be drawn in the game. scale: Rescale the item by this factor. + screen: the pygame screen to draw on. + plate: item is on a plate (soup are is different on a plate and pot) """ if not isinstance(item, list): @@ -433,14 +459,26 @@ class PyGameGUI: if "Soup" in item.name and plate: item_key += "Plate" self.draw_thing( - pos, self.visualization_config[item_key]["parts"], scale=scale + pos, + self.visualization_config[item_key]["parts"], + scale=scale, + screen=screen, ) if isinstance(item, (Item, Plate)) and item.progress_percentage > 0.0: self.draw_progress_bar(pos, item.progress_percentage) if isinstance(item, CookingEquipment) and item.content_list: - if isinstance(item.content_list, list): + if ( + item.content_ready + and item.content_ready.name in self.visualization_config + ): + self.draw_thing( + pos, + self.visualization_config[item.content_ready.name]["parts"], + screen=screen, + ) + else: triangle_offsets = create_polygon(len(item.content_list), length=10) scale = 1 if len(item.content_list) == 1 else 0.6 for idx, o in enumerate(item.content_list): @@ -449,9 +487,8 @@ class PyGameGUI: o, scale=scale, plate=isinstance(item, Plate), + screen=screen, ) - elif item.name in self.visualization_config: - self.draw_thing(pos, self.visualization_config[item.name]["parts"]) # if isinstance(item, Meal): # if item.finished: @@ -462,7 +499,7 @@ class PyGameGUI: # triangle_offsets = create_polygon(len(item.parts), length=10) # self.draw_item(pos + triangle_offsets[idx], o, scale=0.6) - def draw_progress_bar(self, pos, percent): + def draw_progress_bar(self, pos, percent, screen=None): """Visualize progress of progressing item as a green bar under the item.""" bar_height = self.grid_size * 0.2 progress_width = percent * self.grid_size @@ -472,7 +509,10 @@ class PyGameGUI: progress_width, bar_height, ) - pygame.draw.rect(self.game_screen, colors["green1"], progress_bar) + if screen is None: + pygame.draw.rect(self.game_screen, colors["green1"], progress_bar) + else: + pygame.draw.rect(screen, colors["green1"], progress_bar) def draw_counter(self, counter): """Visualization of a counter at its position. If it is occupied by an item, it is also shown. @@ -484,7 +524,6 @@ class PyGameGUI: """ pos = counter.pos * self.grid_size - self.draw_thing(pos, self.visualization_config["Counter"]["parts"]) if str(counter) in self.visualization_config: self.draw_thing(pos, self.visualization_config[str(counter)]["parts"]) @@ -517,7 +556,11 @@ class PyGameGUI: def update_score_label(self, state): score = state["score"] - self.score_label.set_text(f"Your score is {score}") + self.score_label.set_text(f"Score {score}") + + def update_conclusion_label(self, state): + score = state["score"] + self.conclusion_label.set_text(f"Your final score is {score}. Hurray!") def update_remaining_time(self, remaining_time: timedelta): hours, rem = divmod(remaining_time.seconds, 3600) @@ -525,6 +568,64 @@ class PyGameGUI: display_time = f"{minutes}:{'%02d' % seconds}" self.timer_label.set_text(f"Time remaining: {display_time}") + def draw_orders(self, state): + orders_width = self.game_width - 100 + orders_height = self.screen_margin + + order_screen = pygame.Surface( + (orders_width, orders_height), + ) + + bg_color = colors[self.visualization_config["GameWindow"]["background_color"]] + pygame.draw.rect(order_screen, bg_color, order_screen.get_rect()) + + order_rects_start = (orders_height // 2) - (self.grid_size // 2) + with self.simulator.env.lock: + for idx, order in enumerate(state["orders"]): + order: Order + order_upper_left = [ + order_rects_start + idx * self.grid_size * 1.2, + order_rects_start, + ] + pygame.draw.rect( + order_screen, + colors["red"], + pygame.Rect( + order_upper_left[0], + order_upper_left[1], + self.grid_size, + self.grid_size, + ), + width=2, + ) + center = np.array(order_upper_left) + np.array( + [self.grid_size / 2, self.grid_size / 2] + ) + self.draw_thing( + center, + self.visualization_config["Plate"]["parts"], + screen=order_screen, + ) + self.draw_item( + center, + order.meal, + plate=True, + screen=order_screen, + ) + order_done_seconds = ( + (order.start_time + order.max_duration) - state["env_time"] + ).total_seconds() + + percentage = order_done_seconds / order.max_duration.total_seconds() + self.draw_progress_bar(center, percentage, screen=order_screen) + + orders_rect = order_screen.get_rect() + orders_rect.center = [ + self.screen_margin + (orders_width // 2), + orders_height // 2, + ] + self.main_window.blit(order_screen, orders_rect) + def draw(self, state): """Main visualization function. @@ -539,6 +640,9 @@ class PyGameGUI: self.manager.draw_ui(self.main_window) self.update_remaining_time(state["remaining_time"]) + self.draw_orders(state) + self.update_score_label(state) + def init_ui_elements(self): self.manager = pygame_gui.UIManager((self.window_width, self.window_height)) self.manager.get_theme().load_theme(ROOT_DIR / "gui_2d_vis" / "gui_theme.json") @@ -578,7 +682,7 @@ class PyGameGUI: ), (self.buttons_width, self.buttons_height), ), - text="End screen", + text="Finish round", manager=self.manager, ) self.finished_button.can_hover() @@ -596,17 +700,15 @@ class PyGameGUI: ) self.back_button.can_hover() - self.score_rect = pygame.Rect( - ( - (self.window_width // 2) - self.buttons_width, - (self.window_height // 2) - self.buttons_height // 2, - ), - (self.buttons_width * 2, self.buttons_height), - ) - self.score_label = pygame_gui.elements.UILabel( - text=f"Your score: _", - relative_rect=self.score_rect, + text=f"Score: _", + relative_rect=pygame.Rect( + ( + (0), + self.window_height - self.screen_margin, + ), + (self.screen_margin * 2, self.screen_margin), + ), manager=self.manager, object_id="#score_label", ) @@ -629,17 +731,30 @@ class PyGameGUI: options_list=layout_file_paths, starting_option=layout_file_paths[-1], ) - self.timer_label = pygame_gui.elements.UILabel( text="GAMETIME", relative_rect=pygame.Rect( - (0, 0), - (self.buttons_width * 1.5, self.buttons_height), + (self.screen_margin, self.window_height - self.screen_margin), + (self.game_width, self.screen_margin), ), manager=self.manager, object_id="#timer_label", ) + self.orders_label = pygame_gui.elements.UILabel( + text="Orders:", + relative_rect=pygame.Rect(0, 0, self.screen_margin, self.screen_margin), + manager=self.manager, + object_id="#orders_label", + ) + + self.conclusion_label = pygame_gui.elements.UILabel( + text="Your final score was _", + relative_rect=pygame.Rect(0, 0, self.window_width, self.window_height), + manager=self.manager, + object_id="#score_label", + ) + def set_window_size(self, window_width, window_height, game_width, game_height): self.game_screen = pygame.Surface( ( @@ -688,20 +803,27 @@ class PyGameGUI: self.finished_button.hide() self.layout_selection.show() self.timer_label.hide() + self.orders_label.hide() + self.conclusion_label.hide() case MenuStates.Game: self.start_button.hide() - self.back_button.show() - self.score_label.hide() + self.back_button.hide() + self.score_label.show() + self.score_label.show() self.finished_button.show() self.layout_selection.hide() self.timer_label.show() + self.orders_label.show() + self.conclusion_label.hide() case MenuStates.End: self.start_button.hide() self.back_button.show() - self.score_label.show() + self.score_label.hide() self.finished_button.hide() self.layout_selection.hide() self.timer_label.hide() + self.orders_label.hide() + self.conclusion_label.show() def start_button_press(self): self.menu_state = MenuStates.Game @@ -748,6 +870,7 @@ class PyGameGUI: clock = pygame.time.Clock() + self.reset_window_size() self.init_ui_elements() self.manage_button_visibility() @@ -787,12 +910,15 @@ class PyGameGUI: state = self.simulator.get_state() - self.main_window.fill(colors["lemonchiffon1"]) + self.main_window.fill( + colors[self.visualization_config["GameWindow"]["background_color"]] + ) self.manager.draw_ui(self.main_window) match self.menu_state: case MenuStates.Start: pass + case MenuStates.Game: self.draw_background() @@ -801,19 +927,20 @@ class PyGameGUI: if state["ended"]: self.finished_button_press() self.manage_button_visibility() + else: + self.draw(state) - self.draw(state) - - game_screen_rect = self.game_screen.get_rect() - game_screen_rect.center = [ - self.window_width // 2, - self.window_height // 2, - ] + game_screen_rect = self.game_screen.get_rect() + game_screen_rect.center = [ + self.window_width // 2, + self.window_height // 2, + ] - self.main_window.blit(self.game_screen, game_screen_rect) + self.main_window.blit(self.game_screen, game_screen_rect) case MenuStates.End: - self.update_score_label(state) + self.update_conclusion_label(state) + self.manager.update(time_delta) pygame.display.flip() diff --git a/overcooked_simulator/gui_2d_vis/visualization.yaml b/overcooked_simulator/gui_2d_vis/visualization.yaml index 0bec00301f5212459e5c34d4b29048b02e813234..8d4d52deb8f111a56dda65c44501aca3414fe979 100644 --- a/overcooked_simulator/gui_2d_vis/visualization.yaml +++ b/overcooked_simulator/gui_2d_vis/visualization.yaml @@ -9,6 +9,10 @@ GameWindow: buttons_width: 180 buttons_height: 60 + order_bar_height: 100 + order_size: 50 + + background_color: lemonchiffon1 Kitchen: ground_tiles_color: sgigray76 @@ -38,9 +42,9 @@ PlateDispenser: Trash: parts: - type: image - path: images/trash2.png - size: 1.5 - center_offset: [ 0, 0.01 ] + path: images/trash3.png + size: 0.9 + center_offset: [ 0, 0 ] TomatoDispenser: parts: @@ -183,6 +187,17 @@ CookedPatty: path: images/cooked_patty.png size: 0.9 +Burger: + parts: + - type: image + path: images/burger.png + size: 0.8 + +Salad: + parts: + - type: image + path: images/salad.png + size: 0.8 TomatoSoup: parts: diff --git a/overcooked_simulator/order.py b/overcooked_simulator/order.py new file mode 100644 index 0000000000000000000000000000000000000000..5d7abbf72cdbbd1a6d2fe110934b8561d342ca44 --- /dev/null +++ b/overcooked_simulator/order.py @@ -0,0 +1,353 @@ +import dataclasses +import logging +import random +from abc import abstractmethod +from collections import deque +from datetime import datetime, timedelta +from typing import Callable, Tuple, Any, Deque + +from overcooked_simulator.game_items import Item, Plate, ItemInfo + +log = logging.getLogger(__name__) + + +@dataclasses.dataclass +class Order: + meal: ItemInfo + start_time: datetime + max_duration: timedelta + score_calc: Callable[[timedelta, ...], float] + timed_penalties: list[ + Tuple[timedelta, float] | Tuple[timedelta, float, int, timedelta] + ] + expired_penalty: float + + finished_info: dict[str, Any] = dataclasses.field(default_factory=dict) + _timed_penalties: list[Tuple[datetime, float]] = dataclasses.field( + default_factory=list + ) + + def order_time(self, env_time: datetime) -> timedelta: + return self.start_time - env_time + + def create_penalties(self, env_time: datetime): + for penalty_info in self.timed_penalties: + match penalty_info: + case (offset, penalty): + self._timed_penalties.append((env_time + offset, penalty)) + case (duration, penalty, number_repeat, offset): + self._timed_penalties.extend( + [ + (env_time + offset + (duration * i), penalty) + for i in range(number_repeat) + ] + ) + + +class OrderGeneration: + def __init__(self, available_meals: dict[str, ItemInfo], **kwargs): + self.available_meals: list[ItemInfo] = list(available_meals.values()) + + @abstractmethod + def init_orders(self, now) -> list[Order]: + ... + + @abstractmethod + def get_orders( + self, + passed_time: timedelta, + now: datetime, + new_finished_orders: list[Order], + expired_orders: list[Order], + ) -> list[Order]: + ... + + +def zero(item: ItemInfo, **kwargs) -> float: + return 0.0 + + +@dataclasses.dataclass +class RandomOrderKwarg: + num_start_meals: int + sample_on_serving: bool + sample_on_dur: bool + sample_on_dur_func: dict + max_orders: int + duration_sample: dict + score_calc_gen_func: Callable[ + [ItemInfo, timedelta, datetime, Any], Callable[[timedelta, Order], float] + ] + score_calc_gen_kwargs: dict + expired_penalty_func: Callable[[ItemInfo], float] = zero + expired_penalty_kwargs: dict = dataclasses.field(default_factory=dict) + + +class RandomOrderGeneration(OrderGeneration): + def __init__(self, available_meals: dict[str, ItemInfo], **kwargs): + super().__init__(available_meals, **kwargs) + self.kwargs: RandomOrderKwarg = RandomOrderKwarg(**kwargs["kwargs"]) + self.next_order_time: datetime | None = datetime.max + self.number_cur_orders = 0 + self.needed_orders: int = 0 + """For the sample on dur but when it was restricted due to max order number.""" + + def init_orders(self, now) -> list[Order]: + self.number_cur_orders = self.kwargs.num_start_meals + if self.kwargs.sample_on_dur: + self.create_random_next_time_delta(now) + return self.create_orders_for_meals( + random.choices(self.available_meals, k=self.kwargs.num_start_meals), + now, + self.kwargs.sample_on_serving, + ) + + def get_orders( + self, + passed_time: timedelta, + now: datetime, + new_finished_orders: list[Order], + expired_orders: list[Order], + ) -> list[Order]: + self.number_cur_orders -= len(new_finished_orders) + self.number_cur_orders -= len(expired_orders) + if self.kwargs.sample_on_serving: + if new_finished_orders: + self.create_random_next_time_delta(now) + return [] + if self.needed_orders: + self.needed_orders -= len(new_finished_orders) + self.needed_orders = max(self.needed_orders, 0) + self.number_cur_orders += len(new_finished_orders) + return self.create_orders_for_meals( + random.choices(self.available_meals, k=len(new_finished_orders)), + now, + ) + if self.next_order_time <= now: + if self.number_cur_orders >= self.kwargs.max_orders: + self.needed_orders += 1 + else: + if self.kwargs.sample_on_dur: + self.create_random_next_time_delta(now) + else: + self.next_order_time = datetime.max + self.number_cur_orders += 1 + return self.create_orders_for_meals( + [random.choice(self.available_meals)], + now, + ) + return [] + + def create_orders_for_meals( + self, meals: list[ItemInfo], now: datetime, no_time_limit: bool = False + ) -> list[Order]: + orders = [] + for meal in meals: + if no_time_limit: + duration = datetime.max - now + else: + duration = timedelta( + seconds=getattr(random, self.kwargs.duration_sample["func"])( + **self.kwargs.duration_sample["kwargs"] + ) + ) + log.info(f"Create order for meal {meal} with duration {duration}") + orders.append( + Order( + meal=meal, + start_time=now, + max_duration=duration, + score_calc=self.kwargs.score_calc_gen_func( + meal=meal, + duration=duration, + now=now, + kwargs=self.kwargs.score_calc_gen_kwargs, + ), + timed_penalties=[], + expired_penalty=self.kwargs.expired_penalty_func( + meal, **self.kwargs.expired_penalty_kwargs + ), + ) + ) + + return orders + + def create_random_next_time_delta(self, now: datetime): + self.next_order_time = now + timedelta( + seconds=getattr(random, self.kwargs.sample_on_dur_func["func"])( + **self.kwargs.sample_on_dur_func["kwargs"] + ) + ) + log.info(f"Next order in {self.next_order_time}") + + +def simple_score_calc_gen_func( + meal: Item, duration: timedelta, now: datetime, kwargs: dict, **other_kwargs +) -> Callable: + scores = kwargs["scores"] + other = kwargs["other"] + + def score_calc(relative_order_time: timedelta, order: Order) -> float: + if order.meal.name in scores: + return scores[order.meal.name] + return other + + return score_calc + + +def simple_expired_penalty(item: ItemInfo, default: float, **kwargs) -> float: + return default + + +class OrderAndScoreManager: + def __init__(self, order_config, available_meals: dict[str, ItemInfo]): + self.score = 0 + self.order_gen: OrderGeneration = order_config["order_gen_class"]( + available_meals=available_meals, kwargs=order_config["kwargs"] + ) + self.kwargs_for_func = order_config["kwargs"] + self.serving_not_ordered_meals = order_config["serving_not_ordered_meals"] + self.available_meals = available_meals + self.open_orders: Deque[Order] = deque() + + # for logs or history in the future + # TODO log who / which player served which meal -> for split scores + self.served_meals: list[Tuple[Item, datetime]] = [] + self.last_finished = [] + self.next_relevant_time = datetime.max + self.last_expired = [] + + def update_next_relevant_time(self): + next_relevant_time = datetime.max + for order in self.open_orders: + next_relevant_time = min( + next_relevant_time, order.start_time + order.max_duration + ) + for penalty in order._timed_penalties: + next_relevant_time = min(next_relevant_time, penalty[0]) + self.next_relevant_time = next_relevant_time + + def serve_meal(self, item: Item, env_time: datetime) -> bool: + if isinstance(item, Plate): + meal = item.get_potential_meal() + if meal is not None: + if meal.name in self.available_meals: + order = self.find_order_for_meal(meal) + if order is None: + if self.serving_not_ordered_meals: + accept, score = self.serving_not_ordered_meals(meal) + if accept: + log.info( + f"Serving meal without order {meal.name} with score {score}" + ) + self.score += score + self.served_meals.append((meal, env_time)) + return accept + log.info( + f"Do not serve meal {meal.name} because it is not ordered" + ) + return False + order, index = order + score = order.score_calc( + relative_order_time=env_time - order.start_time, + order=order, + ) + self.score += score + order.finished_info = { + "end_time": env_time, + "score": score, + } + log.info(f"Serving meal {meal.name} with order with score {score}") + self.last_finished.append(order) + del self.open_orders[index] + self.served_meals.append((meal, env_time)) + return True + log.info(f"Do not serve item {item}") + return False + + def increment_score(self, score: int): + self.score += score + log.debug(f"Score: {self.score}") + + def create_init_orders(self, env_time): + init_orders = self.order_gen.init_orders(env_time) + self.open_orders.extend(init_orders) + + def progress(self, passed_time: timedelta, now: datetime): + new_orders = self.order_gen.get_orders( + passed_time=passed_time, + now=now, + new_finished_orders=self.last_finished, + expired_orders=self.last_expired, + ) + self.open_orders.extend(new_orders) + self.last_finished = [] + self.last_expired = [] + if new_orders or self.next_relevant_time <= now: + remove_orders = [] + for index, order in enumerate(self.open_orders): + if now >= order.start_time + order.max_duration: + self.score += order.expired_penalty + remove_orders.append(index) + remove_penalties = [] + for i, (penalty_time, penalty) in enumerate(order.timed_penalties): + if penalty_time < now: + self.score -= penalty + remove_penalties.append(i) + + for i in reversed(remove_penalties): + # or del order.timed_penalties[index] + order.timed_penalties.pop(i) + expired_orders = [] + for remove_order in reversed(remove_orders): + expired_orders.append(self.open_orders[remove_order]) + del self.open_orders[remove_order] + self.last_expired = expired_orders + + self.update_next_relevant_time() + + def find_order_for_meal(self, meal) -> Tuple[Order, int] | None: + for index, order in enumerate(self.open_orders): + if order.meal.name == meal.name: + return order, index + + def setup_penalties(self, new_orders: list[Order], env_time: datetime): + for order in new_orders: + order.create_penalties(env_time) + + +if __name__ == "__main__": + import yaml + + order_config = yaml.safe_load( + """orders: + kwargs: + duration_sample: + func: uniform + kwargs: + a: 30 + b: 50 + max_orders: 5 + num_start_meals: 3 + sample_on_dur: false + sample_on_dur_func: + func: uniform + kwargs: + a: 30 + b: 50 + sample_on_serving: true + score_calc_gen_func: null + score_calc_gen_kwargs: + other: 0 + scores: + Burger: 15 + OnionSoup: 10 + Salad: 5 + TomatoSoup: 10 + score_calc_gen_func: ~'' + order_gen_class: ~ + serving_not_ordered_meals: null""" + ) + order_config["orders"]["order_gen_class"] = RandomOrderGeneration + order_config["orders"]["kwargs"]["score_calc_gen_func"] = simple_score_calc_gen_func + print(yaml.dump(order_config)) diff --git a/overcooked_simulator/overcooked_environment.py b/overcooked_simulator/overcooked_environment.py index e36429fe4850674f570277218f45eb6cd0b005aa..3eaf84e14abfee0b6bc2931b00c54a1b0a5c6be5 100644 --- a/overcooked_simulator/overcooked_environment.py +++ b/overcooked_simulator/overcooked_environment.py @@ -22,25 +22,18 @@ from overcooked_simulator.counters import ( PlateDispenser, SinkAddon, ) -from overcooked_simulator.game_items import ItemInfo, ItemType, CookingEquipment +from overcooked_simulator.game_items import ( + ItemInfo, + ItemType, + CookingEquipment, +) +from overcooked_simulator.order import OrderAndScoreManager from overcooked_simulator.player import Player from overcooked_simulator.utils import create_init_env_time log = logging.getLogger(__name__) -class GameScore: - def __init__(self): - self.score = 0 - - def increment_score(self, score: int): - self.score += score - log.debug(f"Score: {self.score}") - - def read_score(self): - return self.score - - class Action: """Action class, specifies player, action type and action itself.""" @@ -70,14 +63,32 @@ class Environment: self.players: dict[str, Player] = {} with open(env_config_path, "r") as file: - environment_config = yaml.safe_load(file) + self.environment_config = yaml.load(file, Loader=yaml.Loader) self.layout_path: Path = layout_path # self.counter_side_length = 1 # -> this changed! is 1 now self.item_info_path: Path = item_info_path self.item_info = self.load_item_info() self.validate_item_info() - self.game_score = GameScore() + if self.environment_config["meals"]["all"]: + self.allowed_meal_names = set( + [ + item + for item, info in self.item_info.items() + if info.type == ItemType.Meal + ] + ) + else: + self.allowed_meal_names = set(self.environment_config["meals"]["list"]) + + self.order_and_score = OrderAndScoreManager( + order_config=self.environment_config["orders"], + available_meals={ + item: info + for item, info in self.item_info.items() + if info.type == ItemType.Meal and item in self.allowed_meal_names + }, + ) plate_transitions = { item: { "seconds": info.seconds, @@ -102,12 +113,9 @@ class Environment: "X": Trash, "W": lambda pos: ServingWindow( pos, - self.game_score, - meals={ - item - for item, info in self.item_info.items() - if info.type == ItemType.Meal - }, + self.order_and_score, + meals=self.allowed_meal_names, + env_time_func=self.get_env_time, ), "T": lambda pos: Dispenser(pos, self.item_info["Tomato"]), "L": lambda pos: Dispenser(pos, self.item_info["Lettuce"]), @@ -115,8 +123,8 @@ class Environment: plate_transitions=plate_transitions, pos=pos, dispensing=self.item_info["Plate"], - plate_config=environment_config["plates"] - if "plates" in environment_config + plate_config=self.environment_config["plates"] + if "plates" in self.environment_config else {}, ), "N": lambda pos: Dispenser(pos, self.item_info["Onion"]), # N for oNioN @@ -179,12 +187,16 @@ class Environment: self.init_counters() self.env_time = create_init_env_time() + self.order_and_score.create_init_orders(self.env_time) self.beginning_time = self.env_time self.env_time_end = self.env_time + timedelta( - seconds=environment_config["game"]["time_limit_seconds"] + seconds=self.environment_config["game"]["time_limit_seconds"] ) log.debug(f"End time: {self.env_time_end}") + def get_env_time(self): + return self.env_time + @property def game_ended(self) -> bool: return self.env_time >= self.env_time_end @@ -315,18 +327,19 @@ class Environment: Args: action: The action to be performed """ - assert action.player in self.players.keys(), "Unknown player." player = self.players[action.player] if action.act_type == "movement": - self.perform_movement(player, action.action) + with self.lock: + self.perform_movement(player, action.action) else: counter = self.get_facing_counter(player) if player.can_reach(counter): if action.act_type == "pickup": - player.pick_action(counter) + with self.lock: + player.pick_action(counter) elif action.act_type == "interact": if action.action == "keydown": @@ -334,7 +347,10 @@ class Environment: player.last_interacted_counter = counter if action.action == "keyup": if player.last_interacted_counter: - player.perform_interact_hold_stop(player.last_interacted_counter) + with self.lock: + player.perform_interact_hold_stop( + player.last_interacted_counter + ) def get_closest_counter(self, point: np.ndarray): """Determines the closest counter for a given 2d-coordinate point in the env. @@ -513,7 +529,9 @@ class Environment: def add_player(self, player_name: str, pos: npt.NDArray = None): log.debug(f"Add player {player_name} to the game") - player = Player(player_name, pos) + player = Player( + player_name, player_config=self.environment_config["player_config"], pos=pos + ) self.players[player.name] = player if player.pos is None: if len(self.designated_player_positions) > 0: @@ -552,6 +570,7 @@ class Environment: for counter in self.counters: if isinstance(counter, (CuttingBoard, Stove, Sink, PlateDispenser)): counter.progress(passed_time=passed_time, now=self.env_time) + self.order_and_score.progress(passed_time=passed_time, now=self.env_time) def get_state(self): """Get the current state of the game environment. The state here is accessible by the current python objects. @@ -562,8 +581,10 @@ class Environment: return { "players": self.players, "counters": self.counters, - "score": self.game_score.read_score(), + "score": self.order_and_score.score, + "orders": self.order_and_score.open_orders, "ended": self.game_ended, + "env_time": self.env_time, "remaining_time": max(self.env_time_end - self.env_time, timedelta(0)), } diff --git a/overcooked_simulator/player.py b/overcooked_simulator/player.py index 4f85046ff9a100c8b0b95a43246a9acccaff4b04..63f803c86a1c2575c2cf2a8b8248e8c0a62d116a 100644 --- a/overcooked_simulator/player.py +++ b/overcooked_simulator/player.py @@ -1,15 +1,12 @@ import logging from collections import deque -from pathlib import Path -from typing import Optional +from typing import Optional, Any import numpy as np import numpy.typing as npt -import yaml -from overcooked_simulator import ROOT_DIR from overcooked_simulator.counters import Counter -from overcooked_simulator.game_items import Item +from overcooked_simulator.game_items import Item, Plate log = logging.getLogger(__name__) @@ -24,9 +21,11 @@ class Player: def __init__( self, name: str, + player_config: dict[str, Any], pos: Optional[npt.NDArray[float]] = None, ): self.name: str = name + self.player_config = player_config if pos is not None: self.pos: npt.NDArray[float] = np.array(pos, dtype=float) else: @@ -34,12 +33,6 @@ class Player: self.holding: Optional[Item] = None - self.player_config_path: Path = Path( - ROOT_DIR / "game_content" / "player_config.yaml" - ) - with open(self.player_config_path, "r") as file: - self.player_config = yaml.safe_load(file) - self.radius: float = self.player_config["radius"] self.move_dist: int = self.player_config["move_dist"] self.interaction_range: int = self.player_config["interaction_range"] @@ -120,6 +113,8 @@ class Player: log.debug( f"Self: {self.holding}, {counter.__class__.__name__}: {counter.occupied_by}" ) + if isinstance(self.holding, Plate): + log.debug(self.holding.clean) def perform_interact_hold_start(self, counter: Counter): """Starts an interaction with the counter. Should be called for a diff --git a/setup.py b/setup.py index b3ceeba705d5d329be64f2dc239e4de19dbc5056..f50b81aaa0c54c9e70528b6b2aaf230355244185 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ setup( license="MIT license", long_description=readme + "\n\n" + history, include_package_data=True, - keywords=["aaambos", "overcooked_simulator"], + keywords=["overcooked_simulator"], name="overcooked_simulator", packages=find_packages(include=["overcooked_simulator", "overcooked_simulator.*"]), test_suite="tests",