diff --git a/overcooked_simulator/counters.py b/overcooked_simulator/counters.py index 9694c824906af718f34d321a807cd9b06fef5c45..616ad6949e266bf9a00207525ab8334054751c95 100644 --- a/overcooked_simulator/counters.py +++ b/overcooked_simulator/counters.py @@ -5,6 +5,8 @@ from collections import deque from datetime import datetime, timedelta from typing import TYPE_CHECKING, Optional +from overcooked_simulator.utils import create_init_env_time + if TYPE_CHECKING: from overcooked_simulator.overcooked_environment import ( GameScore, @@ -97,7 +99,7 @@ class CuttingBoard(Counter): self.progressing = False super().__init__(pos) - def progress(self): + def progress(self, passed_time: timedelta, now: datetime): """Called by environment step function for time progression""" if self.progressing: if isinstance(self.occupied_by, CuttableItem): @@ -194,6 +196,7 @@ 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.setup_plates() def pick_up(self, on_hands: bool = True): @@ -223,7 +226,8 @@ class PlateDispenser(Counter): def update_plate_out_of_kitchen(self): """Is called from the serving window to add a plate out of kitchen.""" - time_plate_to_add = datetime.now() + timedelta( + # not perfect identical to datetime.now but based on framerate enough. + time_plate_to_add = self.env_time + timedelta( seconds=np.random.uniform( low=self.plate_config["plate_delay"][0], high=self.plate_config["plate_delay"][1], @@ -251,9 +255,9 @@ class PlateDispenser(Counter): ] ) - def progress(self): + def progress(self, passed_time: timedelta, now: datetime): """Check if plates arrive from outside the kitchen and add a dirty plate accordingly""" - now = datetime.now() + self.env_time = now if self.next_plate_time < now: idx_delete = [] for i, times in enumerate(self.out_of_kitchen_timer): @@ -294,7 +298,7 @@ class Stove(Counter): else: return self.occupied_by.can_combine(item) - def progress(self): + def progress(self, passed_time: timedelta, now: datetime): """Called by environment step function for time progression""" if ( self.occupied_by @@ -311,7 +315,7 @@ class Sink(Counter): self.sink_addon: SinkAddon = sink_addon self.occupied_by = deque() - def progress(self): + def progress(self, passed_time: timedelta, now: datetime): """Called by environment step function for time progression""" if self.progressing: if self.occupied_by: diff --git a/overcooked_simulator/game_content/environment_config.yaml b/overcooked_simulator/game_content/environment_config.yaml index 073f4c70f4a8ee88d4071313b116f3135bfb9e83..158a22290759fa0415806ee59ce968972e07029c 100644 --- a/overcooked_simulator/game_content/environment_config.yaml +++ b/overcooked_simulator/game_content/environment_config.yaml @@ -1,7 +1,4 @@ -counter_side_length: 70 -world_width: 1000 -world_height: 800 plates: - clean_plates: 0 - dirty_plates: 10 + clean_plates: 3 + dirty_plates: 2 plate_delay: [ 5, 10 ] \ No newline at end of file diff --git a/overcooked_simulator/game_content/layouts/basic.layout b/overcooked_simulator/game_content/layouts/basic.layout index 8b46829b755b1963fde61a396d395618070b3a21..2e7395aa296304bae3d354eea5613affcc0ee6b0 100644 --- a/overcooked_simulator/game_content/layouts/basic.layout +++ b/overcooked_simulator/game_content/layouts/basic.layout @@ -1,11 +1,9 @@ -_________________ -_#QU#TNLB#_______ -_#_______M_______ -_#_______#_______ -_W_______________ -_#__A__A_________ -_C_______________ -_C_______#_______ -_#_______X_______ -_#P#S+#S+#_______ -_________________ +#QU#T###NLB# +#__________M +#__________# +W___________ +#__A_____A__ +C___________ +C__________# +#__________X +#P#S+####S+# \ No newline at end of file diff --git a/overcooked_simulator/game_content/layouts/empty.layout b/overcooked_simulator/game_content/layouts/empty.layout index de0f1b9eb58638cd6a2b94b6ae4591bc72e6144d..2fa1dd8c29c076e9d94e9a305843ff87bebb29f1 100644 --- a/overcooked_simulator/game_content/layouts/empty.layout +++ b/overcooked_simulator/game_content/layouts/empty.layout @@ -1,3 +1,7 @@ -_____ -_____ -____P \ No newline at end of file +______ +______ +______ +______ +______ +______ +_____P \ No newline at end of file diff --git a/overcooked_simulator/game_content/player_config.yaml b/overcooked_simulator/game_content/player_config.yaml index 43cd56a89b915e37ae4eda4bb005c87bcf716cb1..7387659aa3919ca3315269cbe0ca97c3460aac89 100644 --- a/overcooked_simulator/game_content/player_config.yaml +++ b/overcooked_simulator/game_content/player_config.yaml @@ -1,3 +1,3 @@ radius: 0.4 -move_dist: 7 +move_dist: 0.12 interaction_range: 1.6 \ No newline at end of file diff --git a/overcooked_simulator/pygame_gui/__init__.py b/overcooked_simulator/gui_2d_vis/__init__.py similarity index 100% rename from overcooked_simulator/pygame_gui/__init__.py rename to overcooked_simulator/gui_2d_vis/__init__.py diff --git a/overcooked_simulator/pygame_gui/game_colors.py b/overcooked_simulator/gui_2d_vis/game_colors.py similarity index 100% rename from overcooked_simulator/pygame_gui/game_colors.py rename to overcooked_simulator/gui_2d_vis/game_colors.py diff --git a/overcooked_simulator/gui_2d_vis/gui_theme.json b/overcooked_simulator/gui_2d_vis/gui_theme.json new file mode 100644 index 0000000000000000000000000000000000000000..3ca11b55479e8d69bbbf8ab0e3fc4b46cad713e7 --- /dev/null +++ b/overcooked_simulator/gui_2d_vis/gui_theme.json @@ -0,0 +1,49 @@ +{ + "defaults": { + "colours": { + "normal_bg": "#45494e", + "hovered_bg": "#35393e", + "disabled_bg": "#25292e", + "selected_bg": "#193754", + "dark_bg": "#15191e", + "normal_text": "#c5cbd8", + "hovered_text": "#FFFFFF", + "selected_text": "#FFFFFF", + "disabled_text": "#6d736f", + "link_text": "#0000EE", + "link_hover": "#2020FF", + "link_selected": "#551A8B", + "text_shadow": "#777777", + "normal_border": "#DDDDDD", + "hovered_border": "#B0B0B0", + "disabled_border": "#808080", + "selected_border": "#8080B0", + "active_border": "#8080B0", + "filled_bar": "#f4251b", + "unfilled_bar": "#CCCCCC" + } + }, + "button": { + "colours": { + "normal_bg": "#45494e", + "hovered_bg": "#35393e", + "disabled_bg": "#25292e", + "selected_bg": "#193754", + "active_bg": "#193754", + "dark_bg": "#15191e", + "normal_text": "#c5cbd8", + "hovered_text": "#FFFFFF", + "selected_text": "#FFFFFF", + "disabled_text": "#6d736f", + "active_text": "#FFFFFF", + "normal_border": "#DDDDDD", + "hovered_border": "#B0B0B0", + "disabled_border": "#808080", + "selected_border": "#8080B0", + "active_border": "#8080B0" + }, + "misc": { + "tool_tip_delay": "1.5" + } + } +} \ No newline at end of file diff --git a/overcooked_simulator/pygame_gui/images/pixel_cook.png b/overcooked_simulator/gui_2d_vis/images/pixel_cook.png similarity index 100% rename from overcooked_simulator/pygame_gui/images/pixel_cook.png rename to overcooked_simulator/gui_2d_vis/images/pixel_cook.png diff --git a/overcooked_simulator/pygame_gui/images/plate.png b/overcooked_simulator/gui_2d_vis/images/plate.png similarity index 100% rename from overcooked_simulator/pygame_gui/images/plate.png rename to overcooked_simulator/gui_2d_vis/images/plate.png diff --git a/overcooked_simulator/pygame_gui/images/plate_clean.png b/overcooked_simulator/gui_2d_vis/images/plate_clean.png similarity index 100% rename from overcooked_simulator/pygame_gui/images/plate_clean.png rename to overcooked_simulator/gui_2d_vis/images/plate_clean.png diff --git a/overcooked_simulator/pygame_gui/images/plate_dirty.png b/overcooked_simulator/gui_2d_vis/images/plate_dirty.png similarity index 100% rename from overcooked_simulator/pygame_gui/images/plate_dirty.png rename to overcooked_simulator/gui_2d_vis/images/plate_dirty.png diff --git a/overcooked_simulator/pygame_gui/images/pot.png b/overcooked_simulator/gui_2d_vis/images/pot.png similarity index 100% rename from overcooked_simulator/pygame_gui/images/pot.png rename to overcooked_simulator/gui_2d_vis/images/pot.png diff --git a/overcooked_simulator/gui_2d_vis/images/tomato.png b/overcooked_simulator/gui_2d_vis/images/tomato.png new file mode 100644 index 0000000000000000000000000000000000000000..84e063976d58e8a4aa194124409223e3c67c6834 Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/tomato.png differ diff --git a/overcooked_simulator/gui_2d_vis/images/tomato_cut.png b/overcooked_simulator/gui_2d_vis/images/tomato_cut.png new file mode 100644 index 0000000000000000000000000000000000000000..18a12e7f7681856c15bc85bc3a7b2c9a6a42d71f Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/tomato_cut.png differ diff --git a/overcooked_simulator/gui_2d_vis/images/tomato_soup.png b/overcooked_simulator/gui_2d_vis/images/tomato_soup.png new file mode 100644 index 0000000000000000000000000000000000000000..2a0fbf6568ae68871136acb6c78fa734f0dc14ce Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/tomato_soup.png differ diff --git a/overcooked_simulator/gui_2d_vis/overcooked_gui.py b/overcooked_simulator/gui_2d_vis/overcooked_gui.py new file mode 100644 index 0000000000000000000000000000000000000000..7c910c07f9b6fadfd571a64eaf6d4f606fee333f --- /dev/null +++ b/overcooked_simulator/gui_2d_vis/overcooked_gui.py @@ -0,0 +1,755 @@ +import colorsys +import logging +import math +import sys +from collections import deque +from enum import Enum + +import numpy as np +import numpy.typing as npt +import pygame +import pygame_gui +import yaml +from scipy.spatial import KDTree + +from overcooked_simulator import ROOT_DIR +from overcooked_simulator.counters import Counter +from overcooked_simulator.game_items import ( + ProgressibleItem, + Item, + CookingEquipment, + Meal, + Plate, +) +from overcooked_simulator.gui_2d_vis.game_colors import BLUE +from overcooked_simulator.gui_2d_vis.game_colors import colors, Color +from overcooked_simulator.overcooked_environment import Action +from overcooked_simulator.simulation_runner import Simulator + +USE_PLAYER_COOK_SPRITES = True +SHOW_INTERACTION_RANGE = False +SHOW_COUNTER_CENTERS = False + + +class MenuStates(Enum): + Start = "Start" + Game = "Game" + End = "End" + + +def create_polygon(n, length): + if n == 0: + return np.array([0, 0]) + + vector = np.array([length, 0]) + angle = (2 * np.pi) / n + + rot_matrix = np.array( + [[np.cos(angle), -np.sin(angle)], [np.sin(angle), np.cos(angle)]] + ) + + vecs = [vector] + for i in range(n - 1): + vector = np.dot(rot_matrix, vector) + vecs.append(vector) + + return vecs + + +log = logging.getLogger(__name__) + + +class PlayerKeySet: + """Set of keyboard keys for controlling a player. + First four keys are for movement. Order: Down, Up, Left, Right. + 5th key is for interacting with counters. + 6th key ist for picking up things or dropping them. + + """ + + def __init__(self, player_name: str, keys: list[pygame.key]): + """Creates a player key set which contains information about which keyboard keys control the player. + Movement keys in the following order: Down, Up, Left, Right + + Args: + player_name: The name of the player to control. + keys: The keys which control this player in the following order: Down, Up, Left, Right, Interact, Pickup. + """ + self.name = player_name + self.player_keys = keys + self.move_vectors = [[-1, 0], [1, 0], [0, -1], [0, 1]] + self.key_to_movement = { + key: vec for (key, vec) in zip(self.player_keys[:-2], self.move_vectors) + } + self.interact_key = self.player_keys[-2] + self.pickup_key = self.player_keys[-1] + + +class PyGameGUI: + """Visualisation of the overcooked environment and reading keyboard inputs using pygame.""" + + def __init__( + self, + simulator: Simulator, + player_names: list[str], + player_keys: list[pygame.key], + ): + self.game_screen = None + self.FPS = 60 + self.simulator: Simulator = simulator + self.running = True + + self.player_names = player_names + self.player_keys = player_keys + + self.player_key_sets: list[PlayerKeySet] = [ + PlayerKeySet(player_name, keys) + for player_name, keys in zip( + self.player_names, self.player_keys[: len(self.player_names)] + ) + ] + + # TODO cache loaded images? + with open(ROOT_DIR / "gui_2d_vis" / "visualization.yaml", "r") as file: + self.visualization_config = yaml.safe_load(file) + + self.screen_margin = self.visualization_config["GameWindow"]["screen_margin"] + self.window_width = self.visualization_config["GameWindow"]["start_width"] + self.window_height = self.visualization_config["GameWindow"]["start_height"] + + self.main_window = pygame.display.set_mode( + ( + self.window_width, + self.window_height, + ) + ) + + self.game_width, self.game_height = 0, 0 + + self.images_path = ROOT_DIR / "pygame_gui" / "images" + + self.image_cache_dict = {} + + self.menu_state = MenuStates.Start + self.manager: pygame_gui.UIManager + + def init_window_sizes(self): + if self.visualization_config["GameWindow"]["WhatIsFixed"] == "window_width": + game_width = self.visualization_config["GameWindow"]["size"] + kitchen_aspect_ratio = ( + self.simulator.env.kitchen_height / self.simulator.env.kitchen_width + ) + game_height = int(game_width * kitchen_aspect_ratio) + grid_size = int(game_width / self.simulator.env.kitchen_width) + elif self.visualization_config["GameWindow"]["WhatIsFixed"] == "grid": + grid_size = self.visualization_config["GameWindow"]["size"] + game_width, game_height = ( + self.simulator.env.kitchen_width * grid_size, + self.simulator.env.kitchen_height * grid_size, + ) + else: + game_width, game_height = 0, 0 + grid_size = 0 + + window_width, window_height = ( + game_width + (2 * self.screen_margin), + game_height + (2 * self.screen_margin), + ) + + return window_width, window_height, game_width, game_height, grid_size + + def create_player_colors(self) -> list[Color]: + number_player = len(self.simulator.env.players) + hue_values = np.linspace(0, 1, number_player + 1) + + colors_vec = np.array([col for col in colors.values()]) + + tree = KDTree(colors_vec) + + color_names = list(colors.keys()) + + player_colors = [] + for hue in hue_values: + rgb = colorsys.hsv_to_rgb(hue, 1, 1) + query_color = np.array([int(c * 255) for c in rgb]) + _, index = tree.query(query_color, k=1) + player_colors.append(color_names[index]) + + return player_colors + + def send_action(self, action: Action): + """Sends an action to the game environment. + + Args: + action: The action to be sent. Contains the player, action type and move direction if action is a movement. + """ + self.simulator.enter_action(action) + + def handle_keys(self): + """Handles keyboard inputs. Sends action for the respective players. When a key is held down, every frame + an action is sent in this function. + """ + keys = pygame.key.get_pressed() + for player_idx, key_set in enumerate(self.player_key_sets): + relevant_keys = [keys[k] for k in key_set.player_keys] + if any(relevant_keys[:-2]): + move_vec = np.zeros(2) + for idx, pressed in enumerate(relevant_keys[:-2]): + if pressed: + move_vec += key_set.move_vectors[idx] + if np.linalg.norm(move_vec) != 0: + move_vec = move_vec / np.linalg.norm(move_vec) + + action = Action(key_set.name, "movement", move_vec) + self.send_action(action) + + def handle_key_event(self, event): + """Handles key events for the pickup and interaction keys. Pickup is a single action, + for interaction keydown and keyup is necessary, because the player has to be able to hold + the key down. + + Args: + event: Pygame event for extracting the key action. + """ + for key_set in self.player_key_sets: + if event.key == key_set.pickup_key and event.type == pygame.KEYDOWN: + action = Action(key_set.name, "pickup", "pickup") + self.send_action(action) + + if event.key == key_set.interact_key: + if event.type == pygame.KEYDOWN: + action = Action(key_set.name, "interact", "keydown") + self.send_action(action) + elif event.type == pygame.KEYUP: + action = Action(key_set.name, "interact", "keyup") + self.send_action(action) + + def draw_background(self): + """Visualizes a game background.""" + block_size = self.grid_size // 2 # Set the size of the grid block + self.game_screen.fill( + colors[self.visualization_config["Kitchen"]["ground_tiles_color"]] + ) + for x in range(0, self.window_width, block_size): + for y in range(0, self.window_height, block_size): + rect = pygame.Rect(x, y, block_size, block_size) + pygame.draw.rect( + self.game_screen, + self.visualization_config["Kitchen"]["background_lines"], + rect, + 1, + ) + + def draw_image(self, img_path, size, pos, rot_angle=0): + cache_entry = f"{img_path}" + if cache_entry in self.image_cache_dict.keys(): + image = self.image_cache_dict[cache_entry] + else: + image = pygame.image.load( + ROOT_DIR / "gui_2d_vis" / img_path + ).convert_alpha() + self.image_cache_dict[cache_entry] = image + + image = pygame.transform.scale(image, (size, size)) + if rot_angle != 0: + image = pygame.transform.rotate(image, rot_angle) + rect = image.get_rect() + rect.center = pos + self.game_screen.blit(image, rect) + + def draw_players(self, state): + """Visualizes the players as circles with a triangle for the facing direction. + If the player holds something in their hands, it is displayed + + Args: + state: The game state returned by the environment. + """ + for p_idx, player in enumerate(state["players"].values()): + pos = player.pos * self.grid_size + + if USE_PLAYER_COOK_SPRITES: + img_path = self.visualization_config["Cook"]["parts"][0]["path"] + rel_x, rel_y = player.facing_direction + angle = -np.rad2deg(math.atan2(rel_y, rel_x)) + 90 + size = ( + self.visualization_config["Cook"]["parts"][0]["size"] + * self.grid_size + ) + self.draw_image(img_path, size, pos, angle) + + else: + size = player.radius * self.grid_size + color1 = self.player_colors[p_idx] + color2 = colors["white"] + + pygame.draw.circle(self.game_screen, color2, pos, size) + pygame.draw.circle(self.game_screen, BLUE, pos, size, width=1) + pygame.draw.circle(self.game_screen, colors[color1], pos, size // 2) + + facing = player.facing_direction + pygame.draw.polygon( + self.game_screen, + BLUE, + ( + ( + pos[0] + (facing[1] * 0.1 * self.grid_size), + pos[1] - (facing[0] * 0.1 * self.grid_size), + ), + ( + pos[0] - (facing[1] * 0.1 * self.grid_size), + pos[1] + (facing[0] * 0.1 * self.grid_size), + ), + pos + (facing * 0.5 * self.grid_size), + ), + ) + + if SHOW_INTERACTION_RANGE: + pygame.draw.circle( + self.game_screen, + BLUE, + player.facing_point * self.grid_size, + player.interaction_range * self.grid_size, + width=1, + ) + pygame.draw.circle( + self.game_screen, + colors["red1"], + player.facing_point * self.grid_size, + 4, + ) + pygame.draw.circle( + self.game_screen, colors["red1"], player.facing_point, 4 + ) + + if player.holding is not None: + holding_item_pos = (player.pos * self.grid_size) + ( + 20 * player.facing_direction + ) + self.draw_item(holding_item_pos, player.holding) + + if player.current_nearest_counter: + counter: Counter = player.current_nearest_counter + pos = counter.pos * self.grid_size + pygame.draw.rect( + self.game_screen, + colors[self.player_colors[p_idx]], + rect=pygame.Rect( + pos[0] - (self.grid_size // 2), + pos[1] - (self.grid_size // 2), + self.grid_size, + self.grid_size, + ), + width=2, + ) + + def draw_thing( + self, pos: npt.NDArray[float], parts: list[dict[str]], scale: float = 1.0 + ): + """Draws an item, based on its visual parts specified in the visualization config. + + Args: + pos: Where to draw the item parts. + parts: The visual parts to draw. + scale: Rescale the item by this factor. + """ + + for part in parts: + part_type = part["type"] + match part_type: + case "image": + self.draw_image( + parts[0]["path"], + parts[0]["size"] * scale * self.grid_size, + pos, + ) + case "rect": + height = part["height"] * self.grid_size + width = part["width"] * self.grid_size + color = part["color"] + 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) + else: + rect = pygame.Rect( + pos[0] - (height / 2), + pos[1] - (width / 2), + height, + width, + ) + pygame.draw.rect(self.game_screen, color, rect) + case "circle": + radius = part["radius"] * self.grid_size + color = colors[part["color"]] + if "center_offset" in part: + pygame.draw.circle( + self.game_screen, + color, + pos + (np.array(part["center_offset"]) * self.grid_size), + radius, + ) + else: + pygame.draw.circle(self.game_screen, color, pos, radius) + + def draw_item(self, pos: npt.NDArray[float], item: Item, scale: float = 1.0): + """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 + different parts to be drawn. + + Args: + pos: The position of the item to draw. + item: The item do be drawn in the game. + scale: Rescale the item by this factor. + """ + + if not isinstance(item, Meal): + if item.name in self.visualization_config: + self.draw_thing( + pos, self.visualization_config[item.name]["parts"], scale=scale + ) + + if isinstance(item, (ProgressibleItem, Plate)) and not item.finished: + self.draw_progress_bar(pos, item.progressed_steps, item.steps_needed) + + if isinstance(item, CookingEquipment) and item.content: + self.draw_item(pos, item.content) + + if isinstance(item, Meal): + if item.finished: + if item.name in self.visualization_config: + self.draw_thing(pos, self.visualization_config[item.name]["parts"]) + else: + for idx, o in enumerate(item.parts): + 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, current, needed): + """Visualize progress of progressing item as a green bar under the item.""" + if current != 0: + bar_height = self.grid_size * 0.2 + progress_width = (current / needed) * self.grid_size + progress_bar = pygame.Rect( + pos[0] - (self.grid_size / 2), + pos[1] - (self.grid_size / 2) + self.grid_size - bar_height, + progress_width, + bar_height, + ) + pygame.draw.rect(self.game_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. + The visual composition of the counter is read in from visualization.yaml file, where it is specified as + different parts to be drawn. + + Args: + counter: The counter to visualize. + """ + + 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"]) + else: + self.draw_thing( + pos, + self.visualization_config[counter.__class__.__name__]["parts"], + ) + + if counter.occupied_by is not None: + # Multiple plates on plate return: + if isinstance(counter.occupied_by, (list, deque)): + with self.simulator.env.lock: + for i, o in enumerate(counter.occupied_by): + self.draw_item(np.abs([pos[0], pos[1] - (i * 3)]), o) + # All other items: + else: + self.draw_item(pos, counter.occupied_by) + + def draw_counters(self, state): + """Visualizes the counters in the environment. + + Args: + state: The game state returned by the environment. + """ + for counter in state["counters"]: + self.draw_counter(counter) + if SHOW_COUNTER_CENTERS: + pygame.draw.circle(self.game_screen, colors["green1"], counter.pos, 3) + + def draw(self, state): + """Main visualization function. + + Args: + state: The game state returned by the environment. + """ + + self.draw_background() + + self.draw_counters(state) + self.draw_players(state) + self.manager.draw_ui(self.game_screen) + + 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") + + button_width, button_height = 200, 60 + self.start_button = pygame_gui.elements.UIButton( + relative_rect=pygame.Rect( + ( + (self.window_width // 2) - button_width // 2, + (self.window_height / 2) - button_height // 2, + ), + (button_width, button_height), + ), + text="Start Game", + manager=self.manager, + ) + self.start_button.can_hover() + + self.quit_button = pygame_gui.elements.UIButton( + relative_rect=pygame.Rect( + ( + (self.window_width - button_width), + 0, + ), + (button_width, button_height), + ), + text="Quit Game", + manager=self.manager, + ) + self.quit_button.can_hover() + + self.finished_button = pygame_gui.elements.UIButton( + relative_rect=pygame.Rect( + ( + (self.window_width - button_width), + (self.window_height - button_height), + ), + (button_width, button_height), + ), + text="End screen", + manager=self.manager, + ) + self.finished_button.can_hover() + + self.back_button = pygame_gui.elements.UIButton( + relative_rect=pygame.Rect( + ( + (0), + (self.window_height - button_height), + ), + (button_width, button_height), + ), + text="Back to Start", + manager=self.manager, + ) + self.back_button.can_hover() + + self.score_rect = pygame.Rect( + ( + (self.window_width // 2) - button_width // 2, + (self.window_height / 2) - button_height // 2, + ), + (button_width, button_height), + ) + + self.score_label = pygame_gui.elements.UILabel( + text=f"Your score: _", + relative_rect=self.score_rect, + manager=self.manager, + object_id="#score_label", + ) + + layout_file_paths = [ + str(p.name) + for p in (ROOT_DIR / "game_content" / "layouts").glob("*.layout") + ] + assert len(layout_file_paths) != 0, "No layout files." + dropdown_width, dropdown_height = 200, 40 + self.layout_selection = pygame_gui.elements.UIDropDownMenu( + relative_rect=pygame.Rect( + ( + 0, + 0, + ), + (dropdown_width, dropdown_height), + ), + manager=self.manager, + options_list=layout_file_paths, + starting_option=layout_file_paths[-1], + ) + + def setup_windows(self): + ( + self.window_width, + self.window_height, + self.game_width, + self.game_height, + self.grid_size, + ) = self.init_window_sizes() + + self.game_screen = pygame.Surface( + ( + self.game_width, + self.game_height, + ), + ) + + self.main_window = pygame.display.set_mode( + ( + self.window_width, + self.window_height, + ) + ) + self.player_colors = self.create_player_colors() + + def setup_simulation(self, config_path, layout_path): + self.simulator = Simulator(config_path, layout_path, 600) + number_player = len(self.player_names) + for i in range(number_player): + player_name = f"p{i}" + self.simulator.register_player(player_name) + self.simulator.start() + + def manage_button_visibility(self): + match self.menu_state: + case MenuStates.Start: + self.back_button.hide() + self.quit_button.show() + self.start_button.show() + self.score_label.hide() + self.finished_button.hide() + self.layout_selection.show() + case MenuStates.Game: + self.start_button.hide() + self.back_button.show() + self.score_label.hide() + self.finished_button.show() + self.layout_selection.hide() + case MenuStates.End: + self.start_button.hide() + self.back_button.show() + self.score_label.show() + self.score_label.set_text( + f"Your Score is {self.simulator.env.game_score.score}" + ) + self.finished_button.hide() + self.layout_selection.hide() + + def start_button_press(self): + self.menu_state = MenuStates.Game + + layout_path = ( + ROOT_DIR + / "game_content" + / "layouts" + / self.layout_selection.selected_option + ) + config_path = ROOT_DIR / "game_content" / "environment_config.yaml" + + self.setup_simulation(config_path, layout_path) + self.setup_windows() + self.init_ui_elements() + log.debug("Pressed start button") + + def back_button_press(self): + self.simulator.stop() + self.menu_state = MenuStates.Start + log.debug("Pressed back button") + + def quit_button_press(self): + self.simulator.stop() + self.running = False + log.debug("Pressed quit button") + + def finished_button_press(self): + self.simulator.stop() + self.menu_state = MenuStates.End + log.debug("Pressed finished button") + + def start_pygame(self): + """Starts pygame and the gui loop. Each frame the game state is visualized and keyboard inputs are read.""" + log.debug(f"Starting pygame gui at {self.FPS} fps") + pygame.init() + pygame.font.init() + + self.setup_windows() + self.init_ui_elements() + + pygame.display.set_caption("Simple Overcooked Simulator") + + clock = pygame.time.Clock() + + self.manage_button_visibility() + + # Game loop + self.running = True + while self.running: + try: + time_delta = clock.tick(self.FPS) / 1000.0 + + for event in pygame.event.get(): + if event.type == pygame.QUIT: + self.running = False + if event.type == pygame_gui.UI_BUTTON_PRESSED: + match event.ui_element: + case self.start_button: + self.start_button_press() + case self.back_button: + self.back_button_press() + case self.finished_button: + self.finished_button_press() + case self.quit_button: + self.quit_button_press() + + self.manage_button_visibility() + + if ( + event.type in [pygame.KEYDOWN, pygame.KEYUP] + and self.menu_state == MenuStates.Game + ): + self.handle_key_event(event) + + self.manager.process_events(event) + + # drawing: + + self.main_window.fill(colors["lemonchiffon1"]) + self.manager.draw_ui(self.main_window) + + match self.menu_state: + case MenuStates.Start: + pass + case MenuStates.Game: + self.draw_background() + + self.handle_keys() + + state = self.simulator.get_state() + self.draw(state) + + 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) + + case MenuStates.End: + pygame.draw.rect( + self.game_screen, colors["cornsilk1"], self.score_rect + ) + self.manager.update(time_delta) + pygame.display.flip() + + except KeyboardInterrupt: + self.simulator.stop() + pygame.quit() + sys.exit() + + self.simulator.stop() + pygame.quit() + sys.exit() diff --git a/overcooked_simulator/pygame_gui/visualization.yaml b/overcooked_simulator/gui_2d_vis/visualization.yaml similarity index 96% rename from overcooked_simulator/pygame_gui/visualization.yaml rename to overcooked_simulator/gui_2d_vis/visualization.yaml index f9f278ef76e63c7fd2524b5cbf82fb8aa3b029ce..5cef5d26cd8be74c201ed6c220d1e6b87cc4d5a8 100644 --- a/overcooked_simulator/pygame_gui/visualization.yaml +++ b/overcooked_simulator/gui_2d_vis/visualization.yaml @@ -1,5 +1,12 @@ # colors: https://www.webucator.com/article/python-color-constants-module/ +GameWindow: + WhatIsFixed: window_width # entweder grid oder window_width + size: 600 + screen_margin: 100 + start_width: 600 + start_height: 600 + Kitchen: ground_tiles_color: sgigray76 background_lines: gray79 diff --git a/overcooked_simulator/main.py b/overcooked_simulator/main.py index 239779f471d534e3e9e4cb186735b01c24630226..86caba35763a863735582b4f0e0fb32c3402bae7 100644 --- a/overcooked_simulator/main.py +++ b/overcooked_simulator/main.py @@ -7,7 +7,7 @@ from datetime import datetime import pygame from overcooked_simulator import ROOT_DIR -from overcooked_simulator.pygame_gui.pygame_gui import PyGameGUI +from overcooked_simulator.gui_2d_vis.overcooked_gui import PyGameGUI from overcooked_simulator.simulation_runner import Simulator log = logging.getLogger(__name__) @@ -50,12 +50,9 @@ def main(): pygame.K_i, ] keys2 = [pygame.K_a, pygame.K_d, pygame.K_w, pygame.K_s, pygame.K_f, pygame.K_e] - gui = PyGameGUI(simulator, [f"p{i}" for i in range(number_player)], [keys1, keys2]) - simulator.start() + gui = PyGameGUI(simulator, [f"p{i}" for i in range(number_player)], [keys1, keys2]) gui.start_pygame() - - simulator.stop() sys.exit() diff --git a/overcooked_simulator/overcooked_environment.py b/overcooked_simulator/overcooked_environment.py index 8a51ecf4570d6eb912c7be00db3a2560a0ebe759..8f5adf962fed07d207ac034821c4978edb3a3d47 100644 --- a/overcooked_simulator/overcooked_environment.py +++ b/overcooked_simulator/overcooked_environment.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging import random +from datetime import timedelta from pathlib import Path from threading import Lock @@ -23,6 +24,7 @@ from overcooked_simulator.counters import ( ) from overcooked_simulator.game_items import ItemInfo, ItemType from overcooked_simulator.player import Player +from overcooked_simulator.utils import create_init_env_time log = logging.getLogger(__name__) @@ -82,7 +84,7 @@ class Environment: with open(env_config_path, "r") as file: environment_config = yaml.safe_load(file) self.layout_path: Path = layout_path - self.counter_side_length = environment_config["counter_side_length"] + # 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() @@ -118,6 +120,9 @@ class Environment: "+": SinkAddon, } + self.kitchen_height: int = 0 + self.kitchen_width: int = 0 + ( self.counters, self.designated_player_positions, @@ -128,8 +133,7 @@ class Environment: self.score: int = 0 - self.world_width: int = environment_config["world_width"] - self.world_height: int = environment_config["world_height"] + self.env_time = create_init_env_time() def load_item_info(self) -> dict[str, ItemInfo]: with open(self.item_info_path, "r") as file: @@ -156,16 +160,20 @@ class Environment: Args: layout_file: Path to the layout file. """ - current_y: float = self.counter_side_length / 2 + current_y: float = 0.5 counters: list[Counter] = [] designated_player_positions: list[npt.NDArray] = [] free_positions: list[npt.NDArray] = [] + self.kitchen_width = 0 + with open(layout_file, "r") as layout_file: lines = layout_file.readlines() + self.kitchen_height = len(lines) + for line in lines: line = line.replace("\n", "").replace(" ", "") # remove newline char - current_x = self.counter_side_length / 2 + current_x = 0.5 for character in line: character = character.capitalize() pos = np.array([current_x, current_y]) @@ -180,8 +188,12 @@ class Environment: ) elif counter_class == "Free": free_positions.append(np.array([current_x, current_y])) - current_x += self.counter_side_length - current_y += self.counter_side_length + current_x += 1 + if current_x > self.kitchen_width: + self.kitchen_width = current_x + current_y += 1 + + self.kitchen_width -= 0.5 return counters, designated_player_positions, free_positions @@ -331,9 +343,7 @@ class Environment: other_players = filter(lambda p: p.name != player.name, self.players.values()) def collide(p): - return np.linalg.norm(player.pos - p.pos) <= ( - player.radius * self.counter_side_length - ) + (p.radius * self.counter_side_length) + return np.linalg.norm(player.pos - p.pos) <= (player.radius) + (p.radius) return list(filter(collide, other_players)) @@ -351,10 +361,7 @@ class Environment: other_players = filter(lambda p: p.name != player.name, self.players.values()) def collide(p): - return np.linalg.norm(player.pos - p.pos) <= ( - (player.radius * self.counter_side_length) - + (p.radius * self.counter_side_length) - ) + return np.linalg.norm(player.pos - p.pos) <= ((player.radius) + (p.radius)) return any(map(collide, other_players)) @@ -388,16 +395,15 @@ class Environment: Returns: True if player and counter overlap, False if not. """ - size = self.counter_side_length cx, cy = player.pos - dx = max(np.abs(cx - counter.pos[0]) - size / 2, 0) - dy = max(np.abs(cy - counter.pos[1]) - size / 2, 0) + dx = max(np.abs(cx - counter.pos[0]) - 1 / 2, 0) + dy = max(np.abs(cy - counter.pos[1]) - 1 / 2, 0) distance = np.linalg.norm([dx, dy]) - return distance < (player.radius * self.counter_side_length) + return distance < (player.radius) 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, self.counter_side_length, pos) + player = Player(player_name, pos) self.players[player.name] = player if player.pos is None: if len(self.designated_player_positions) > 0: @@ -420,23 +426,21 @@ class Environment: Returns: True if the player touches the world bounds, False if not. """ - collisions_lower = any( - (player.pos - (player.radius * self.counter_side_length)) < 0 - ) + collisions_lower = any((player.pos - (player.radius)) < 0) collisions_upper = any( - (player.pos + (player.radius * self.counter_side_length)) - > [self.world_width, self.world_height] + (player.pos + (player.radius)) > [self.kitchen_width, self.kitchen_height] ) return collisions_lower or collisions_upper - def step(self): + def step(self, passed_time: timedelta): """Performs a step of the environment. Affects time based events such as cooking or cutting things, orders and time limits. """ + self.env_time += passed_time with self.lock: for counter in self.counters: if isinstance(counter, (CuttingBoard, Stove, Sink, PlateDispenser)): - counter.progress() + counter.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. @@ -467,9 +471,7 @@ class Environment: case Sink(pos=pos): assert len(sink_addons) > 0, "No SinkAddon but normal Sink" closest_addon = self.get_closest(pos, sink_addons) - assert self.counter_side_length - ( - self.counter_side_length * 0.05 - ) <= np.linalg.norm( + assert 1 - (1 * 0.05) <= np.linalg.norm( closest_addon.pos - pos ), f"No SinkAddon connected to Sink at pos {pos}" counter.set_addon(closest_addon) @@ -484,3 +486,7 @@ class Environment: return list( filter(lambda counter: isinstance(counter, counter_type), self.counters) ) + + def reset_env_time(self): + self.env_time = create_init_env_time() + log.debug(f"Reset env time to {self.env_time}") diff --git a/overcooked_simulator/player.py b/overcooked_simulator/player.py index c4b5fbaaf3db37ec31c9b18ccc5ced63f93dceb9..4f85046ff9a100c8b0b95a43246a9acccaff4b04 100644 --- a/overcooked_simulator/player.py +++ b/overcooked_simulator/player.py @@ -24,7 +24,6 @@ class Player: def __init__( self, name: str, - grid_size: int, pos: Optional[npt.NDArray[float]] = None, ): self.name: str = name @@ -42,7 +41,6 @@ class Player: self.player_config = yaml.safe_load(file) self.radius: float = self.player_config["radius"] - self.grid_size: int = grid_size self.move_dist: int = self.player_config["move_dist"] self.interaction_range: int = self.player_config["interaction_range"] self.facing_direction: npt.NDArray[float] = np.array([0, 1]) @@ -85,9 +83,7 @@ class Player: self.update_facing_point() def update_facing_point(self): - self.facing_point = self.pos + ( - self.facing_direction * self.radius * self.grid_size * 0.5 - ) + self.facing_point = self.pos + (self.facing_direction * self.radius * 0.5) def can_reach(self, counter: Counter): """Checks whether the player can reach the counter in question. Simple check if the distance is not larger @@ -99,9 +95,7 @@ class Player: Returns: True if the counter is in range of the player, False if not. """ - return np.linalg.norm(counter.pos - self.facing_point) <= ( - self.interaction_range * self.grid_size - ) + return np.linalg.norm(counter.pos - self.facing_point) <= self.interaction_range def pick_action(self, counter: Counter): """Performs the pickup-action with the counter. Handles the logic of what the player is currently holding, diff --git a/overcooked_simulator/pygame_gui/images/bun.png b/overcooked_simulator/pygame_gui/images/bun.png deleted file mode 100644 index 2c2f180610dbfd664acb61d2d733d7a3df75e2e5..0000000000000000000000000000000000000000 Binary files a/overcooked_simulator/pygame_gui/images/bun.png and /dev/null differ diff --git a/overcooked_simulator/pygame_gui/images/cooked_patty.png b/overcooked_simulator/pygame_gui/images/cooked_patty.png deleted file mode 100644 index 3d5b9270e569211eca7e971b21e8e9db944ac9bc..0000000000000000000000000000000000000000 Binary files a/overcooked_simulator/pygame_gui/images/cooked_patty.png and /dev/null differ diff --git a/overcooked_simulator/pygame_gui/images/cutting_board_large.png b/overcooked_simulator/pygame_gui/images/cutting_board_large.png deleted file mode 100644 index 8f50132ce6b8646ec1f35f1599c7210f7d0e1c4f..0000000000000000000000000000000000000000 Binary files a/overcooked_simulator/pygame_gui/images/cutting_board_large.png and /dev/null differ diff --git a/overcooked_simulator/pygame_gui/images/lettuce.png b/overcooked_simulator/pygame_gui/images/lettuce.png deleted file mode 100644 index 937a06b30c0bae8e0c28836f04450b68be21d5ff..0000000000000000000000000000000000000000 Binary files a/overcooked_simulator/pygame_gui/images/lettuce.png and /dev/null differ diff --git a/overcooked_simulator/pygame_gui/images/lettuce_cut.png b/overcooked_simulator/pygame_gui/images/lettuce_cut.png deleted file mode 100644 index 70c5c623d3d09ef84ca41a167ec47b7b1b26690a..0000000000000000000000000000000000000000 Binary files a/overcooked_simulator/pygame_gui/images/lettuce_cut.png and /dev/null differ diff --git a/overcooked_simulator/pygame_gui/images/lettuce_cut_smaller.png b/overcooked_simulator/pygame_gui/images/lettuce_cut_smaller.png deleted file mode 100644 index 0fc897f72a779a998a264fce5ed4363bd8813d32..0000000000000000000000000000000000000000 Binary files a/overcooked_simulator/pygame_gui/images/lettuce_cut_smaller.png and /dev/null differ diff --git a/overcooked_simulator/pygame_gui/images/lettuce_smaller.png b/overcooked_simulator/pygame_gui/images/lettuce_smaller.png deleted file mode 100644 index 6c0abbda27f4ac859d8965d8c0828d0685277c08..0000000000000000000000000000000000000000 Binary files a/overcooked_simulator/pygame_gui/images/lettuce_smaller.png and /dev/null differ diff --git a/overcooked_simulator/pygame_gui/images/meat.png b/overcooked_simulator/pygame_gui/images/meat.png deleted file mode 100644 index ff750e1c4858d0bab5dd2434966ee38c7f88e085..0000000000000000000000000000000000000000 Binary files a/overcooked_simulator/pygame_gui/images/meat.png and /dev/null differ diff --git a/overcooked_simulator/pygame_gui/images/onion_cut.png b/overcooked_simulator/pygame_gui/images/onion_cut.png deleted file mode 100644 index f33ec9ab41c1aea7fbe6553891db86032346d29e..0000000000000000000000000000000000000000 Binary files a/overcooked_simulator/pygame_gui/images/onion_cut.png and /dev/null differ diff --git a/overcooked_simulator/pygame_gui/images/onion_large.png b/overcooked_simulator/pygame_gui/images/onion_large.png deleted file mode 100644 index dd70c1a5dd8a271c353909e4717ebe3629e80d54..0000000000000000000000000000000000000000 Binary files a/overcooked_simulator/pygame_gui/images/onion_large.png and /dev/null differ diff --git a/overcooked_simulator/pygame_gui/images/onion_soup_plate.png b/overcooked_simulator/pygame_gui/images/onion_soup_plate.png deleted file mode 100644 index 89a81f7b27509ce34005d3752949e1c4a1dd3084..0000000000000000000000000000000000000000 Binary files a/overcooked_simulator/pygame_gui/images/onion_soup_plate.png and /dev/null differ diff --git a/overcooked_simulator/pygame_gui/images/onion_soup_pot.png b/overcooked_simulator/pygame_gui/images/onion_soup_pot.png deleted file mode 100644 index 35ee354b302c94fe9c70a3502a1ebbfd2616cb54..0000000000000000000000000000000000000000 Binary files a/overcooked_simulator/pygame_gui/images/onion_soup_pot.png and /dev/null differ diff --git a/overcooked_simulator/pygame_gui/images/pan.png b/overcooked_simulator/pygame_gui/images/pan.png deleted file mode 100644 index afb288156faee0fdbb6917de0b18e0e1b698862d..0000000000000000000000000000000000000000 Binary files a/overcooked_simulator/pygame_gui/images/pan.png and /dev/null differ diff --git a/overcooked_simulator/pygame_gui/images/pot_smaller.png b/overcooked_simulator/pygame_gui/images/pot_smaller.png deleted file mode 100644 index 5f8d29c3f66beba620e518b09e210783ff43ebe1..0000000000000000000000000000000000000000 Binary files a/overcooked_simulator/pygame_gui/images/pot_smaller.png and /dev/null differ diff --git a/overcooked_simulator/pygame_gui/images/raw_fish.png b/overcooked_simulator/pygame_gui/images/raw_fish.png deleted file mode 100644 index a096f867280ade64f925c4fe61d1e445ab961508..0000000000000000000000000000000000000000 Binary files a/overcooked_simulator/pygame_gui/images/raw_fish.png and /dev/null differ diff --git a/overcooked_simulator/pygame_gui/images/raw_patty.png b/overcooked_simulator/pygame_gui/images/raw_patty.png deleted file mode 100644 index 85a42e2a04efbddf49c99e23705f192e7d089e3b..0000000000000000000000000000000000000000 Binary files a/overcooked_simulator/pygame_gui/images/raw_patty.png and /dev/null differ diff --git a/overcooked_simulator/pygame_gui/images/sink.png b/overcooked_simulator/pygame_gui/images/sink.png deleted file mode 100644 index b191df7d898ef980f082e6de1cae5b862c4d6757..0000000000000000000000000000000000000000 Binary files a/overcooked_simulator/pygame_gui/images/sink.png and /dev/null differ diff --git a/overcooked_simulator/pygame_gui/images/sink_large.png b/overcooked_simulator/pygame_gui/images/sink_large.png deleted file mode 100644 index 0758d02bc6f4b2c80cfd4c984f8914b74bb0ff83..0000000000000000000000000000000000000000 Binary files a/overcooked_simulator/pygame_gui/images/sink_large.png and /dev/null differ diff --git a/overcooked_simulator/pygame_gui/pygame_gui.py b/overcooked_simulator/pygame_gui/pygame_gui.py deleted file mode 100644 index 9efaabe7e36f5749f68bf8f6b02675bf8b26f09c..0000000000000000000000000000000000000000 --- a/overcooked_simulator/pygame_gui/pygame_gui.py +++ /dev/null @@ -1,487 +0,0 @@ -import colorsys -import logging -import math -import sys -from collections import deque - -import numpy as np -import numpy.typing as npt -import pygame -import yaml -from scipy.spatial import KDTree - -from overcooked_simulator import ROOT_DIR -from overcooked_simulator.counters import Counter -from overcooked_simulator.game_items import ( - ProgressibleItem, - Item, - CookingEquipment, - Meal, - Plate, -) -from overcooked_simulator.overcooked_environment import Action -from overcooked_simulator.pygame_gui.game_colors import BLUE -from overcooked_simulator.pygame_gui.game_colors import colors, Color -from overcooked_simulator.simulation_runner import Simulator - -USE_PLAYER_COOK_SPRITES = True -SHOW_INTERACTION_RANGE = False -SHOW_COUNTER_CENTERS = False - - -def create_polygon(n, length): - if n == 0: - return np.array([0, 0]) - - vector = np.array([length, 0]) - angle = (2 * np.pi) / n - - rot_matrix = np.array( - [[np.cos(angle), -np.sin(angle)], [np.sin(angle), np.cos(angle)]] - ) - - vecs = [vector] - for i in range(n - 1): - vector = np.dot(rot_matrix, vector) - vecs.append(vector) - - return vecs - - -log = logging.getLogger(__name__) - - -class PlayerKeySet: - """Set of keyboard keys for controlling a player. - First four keys are for movement. Order: Down, Up, Left, Right. - 5th key is for interacting with counters. - 6th key ist for picking up things or dropping them. - - """ - - def __init__(self, player_name: str, keys: list[pygame.key]): - """Creates a player key set which contains information about which keyboard keys control the player. - Movement keys in the following order: Down, Up, Left, Right - - Args: - player_name: The name of the player to control. - keys: The keys which control this player in the following order: Down, Up, Left, Right, Interact, Pickup. - """ - self.name = player_name - self.player_keys = keys - self.move_vectors = [[-1, 0], [1, 0], [0, -1], [0, 1]] - self.key_to_movement = { - key: vec for (key, vec) in zip(self.player_keys[:-2], self.move_vectors) - } - self.interact_key = self.player_keys[-2] - self.pickup_key = self.player_keys[-1] - - -class PyGameGUI: - """Visualisation of the overcooked environment and reading keyboard inputs using pygame.""" - - def __init__( - self, - simulator: Simulator, - player_names: list[str], - player_keys: list[pygame.key], - ): - self.screen = None - self.FPS = 60 - self.simulator = simulator - self.counter_size = self.simulator.env.counter_side_length - self.window_width, self.window_height = ( - simulator.env.world_width, - simulator.env.world_height, - ) - - self.player_names = player_names - self.player_keys = player_keys - - self.player_key_sets: list[PlayerKeySet] = [ - PlayerKeySet(player_name, keys) - for player_name, keys in zip( - self.player_names, self.player_keys[: len(self.player_names)] - ) - ] - - # TODO cache loaded images? - with open(ROOT_DIR / "pygame_gui" / "visualization.yaml", "r") as file: - self.visualization_config = yaml.safe_load(file) - - self.images_path = ROOT_DIR / "pygame_gui" / "images" - - self.player_colors = self.create_player_colors() - - self.image_cache_dict = {} - - def create_player_colors(self) -> list[Color]: - number_player = len(self.simulator.env.players) - hue_values = np.linspace(0, 1, number_player + 1) - - colors_vec = np.array([col for col in colors.values()]) - - tree = KDTree(colors_vec) - - color_names = list(colors.keys()) - - player_colors = [] - for hue in hue_values: - rgb = colorsys.hsv_to_rgb(hue, 1, 1) - query_color = np.array([int(c * 255) for c in rgb]) - _, index = tree.query(query_color, k=1) - player_colors.append(color_names[index]) - - return player_colors - - def send_action(self, action: Action): - """Sends an action to the game environment. - - Args: - action: The action to be sent. Contains the player, action type and move direction if action is a movement. - """ - self.simulator.enter_action(action) - - def handle_keys(self): - """Handles keyboard inputs. Sends action for the respective players. When a key is held down, every frame - an action is sent in this function. - """ - keys = pygame.key.get_pressed() - for player_idx, key_set in enumerate(self.player_key_sets): - relevant_keys = [keys[k] for k in key_set.player_keys] - if any(relevant_keys[:-2]): - move_vec = np.zeros(2) - for idx, pressed in enumerate(relevant_keys[:-2]): - if pressed: - move_vec += key_set.move_vectors[idx] - if np.linalg.norm(move_vec) != 0: - move_vec = move_vec / np.linalg.norm(move_vec) - - action = Action(key_set.name, "movement", move_vec) - self.send_action(action) - - def handle_key_event(self, event): - """Handles key events for the pickup and interaction keys. Pickup is a single action, - for interaction keydown and keyup is necessary, because the player has to be able to hold - the key down. - - Args: - event: Pygame event for extracting the key action. - """ - for key_set in self.player_key_sets: - if event.key == key_set.pickup_key and event.type == pygame.KEYDOWN: - action = Action(key_set.name, "pickup", "pickup") - self.send_action(action) - - if event.key == key_set.interact_key: - if event.type == pygame.KEYDOWN: - action = Action(key_set.name, "interact", "keydown") - self.send_action(action) - elif event.type == pygame.KEYUP: - action = Action(key_set.name, "interact", "keyup") - self.send_action(action) - - def draw_background(self): - """Visualizes a game background.""" - block_size = self.counter_size // 2 # Set the size of the grid block - for x in range(0, self.window_width, block_size): - for y in range(0, self.window_height, block_size): - rect = pygame.Rect(x, y, block_size, block_size) - pygame.draw.rect( - self.screen, - self.visualization_config["Kitchen"]["background_lines"], - rect, - 1, - ) - - def draw_image(self, img_path, size, pos, rot_angle=0): - cache_entry = f"{img_path}" - if cache_entry in self.image_cache_dict.keys(): - image = self.image_cache_dict[cache_entry] - else: - image = pygame.image.load( - ROOT_DIR / "pygame_gui" / img_path - ).convert_alpha() - self.image_cache_dict[cache_entry] = image - - image = pygame.transform.scale(image, (size, size)) - if rot_angle != 0: - image = pygame.transform.rotate(image, rot_angle) - rect = image.get_rect() - rect.center = pos - self.screen.blit(image, rect) - - def draw_players(self, state): - """Visualizes the players as circles with a triangle for the facing direction. - If the player holds something in their hands, it is displayed - - Args: - state: The game state returned by the environment. - """ - for p_idx, player in enumerate(state["players"].values()): - if USE_PLAYER_COOK_SPRITES: - img_path = self.visualization_config["Cook"]["parts"][0]["path"] - rel_x, rel_y = player.facing_direction - angle = -np.rad2deg(math.atan2(rel_y, rel_x)) + 90 - size = ( - self.visualization_config["Cook"]["parts"][0]["size"] - * self.counter_size - ) - self.draw_image(img_path, size, player.pos, angle) - - else: - pos = player.pos - size = player.radius * self.counter_size - color1 = self.player_colors[p_idx] - color2 = colors["white"] - - pygame.draw.circle(self.screen, color2, pos, size) - pygame.draw.circle(self.screen, BLUE, pos, size, width=1) - pygame.draw.circle(self.screen, colors[color1], pos, size // 2) - - pos = player.pos - facing = player.facing_direction - pygame.draw.polygon( - self.screen, - BLUE, - ( - ( - pos[0] + (facing[1] * 0.1 * self.counter_size), - pos[1] - (facing[0] * 0.1 * self.counter_size), - ), - ( - pos[0] - (facing[1] * 0.1 * self.counter_size), - pos[1] + (facing[0] * 0.1 * self.counter_size), - ), - player.pos + (facing * 0.5 * self.counter_size), - ), - ) - - if SHOW_INTERACTION_RANGE: - pygame.draw.circle( - self.screen, - BLUE, - player.facing_point, - player.interaction_range * self.counter_size, - width=1, - ) - pygame.draw.circle(self.screen, colors["red1"], player.facing_point, 4) - - if player.holding is not None: - holding_item_pos = player.pos + (20 * player.facing_direction) - self.draw_item(holding_item_pos, player.holding) - - if player.current_nearest_counter: - counter: Counter = player.current_nearest_counter - pygame.draw.rect( - self.screen, - colors[self.player_colors[p_idx]], - rect=pygame.Rect( - counter.pos[0] - (self.counter_size // 2), - counter.pos[1] - (self.counter_size // 2), - self.counter_size, - self.counter_size, - ), - width=2, - ) - - def draw_thing( - self, part_pos: npt.NDArray[float], parts: list[dict[str]], scale: float = 1.0 - ): - """Draws an item, based on its visual parts specified in the visualization config. - - Args: - pos: Where to draw the item parts. - parts: The visual parts to draw. - scale: Rescale the item by this factor. - """ - for part in parts: - pos = part_pos.copy() - - part_type = part["type"] - if part_type == "image": - if "center_offset" in part: - pos += np.array(part["center_offset"]) * self.counter_size - self.draw_image( - parts[0]["path"], - parts[0]["size"] * scale * self.counter_size, - pos, - ) - elif part_type == "rect": - height = part["height"] * self.counter_size - width = part["width"] * self.counter_size - color = part["color"] - if "center_offset" in part: - dx, dy = np.array(part["center_offset"]) * self.counter_size - rect = pygame.Rect(pos[0] + dx, pos[1] + dy, height, width) - pygame.draw.rect(self.screen, color, rect) - else: - rect = pygame.Rect( - pos[0] - (height / 2), - pos[1] - (width / 2), - height, - width, - ) - pygame.draw.rect(self.screen, color, rect) - elif part_type == "circle": - radius = part["radius"] * self.counter_size - color = colors[part["color"]] - if "center_offset" in part: - pygame.draw.circle( - self.screen, - color, - pos + np.array(part["center_offset"]), - radius, - ) - else: - pygame.draw.circle(self.screen, color, pos, radius) - - def draw_item( - self, pos: npt.NDArray[float], item: Item, scale: float = 1.0, plate=False - ): - """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 - different parts to be drawn. - - Args: - pos: The position of the item to draw. - item: The item do be drawn in the game. - scale: Rescale the item by this factor. - plate: Conditional drawing if the item is on a plate, relevant for soup. - """ - - if not isinstance(item, Meal): - if item.name in self.visualization_config: - self.draw_thing( - pos, self.visualization_config[item.name]["parts"], scale=scale - ) - - if isinstance(item, (ProgressibleItem, Plate)) and not item.finished: - self.draw_progress_bar(pos, item.progressed_steps, item.steps_needed) - - if isinstance(item, CookingEquipment) and item.content: - self.draw_item(pos, item.content, plate=isinstance(item, Plate)) - - if isinstance(item, Meal): - if item.finished: - if item.name in self.visualization_config: - if "Soup" in item.name and plate: - self.draw_thing( - pos, self.visualization_config[item.name + "Plate"]["parts"] - ) - else: - self.draw_thing( - pos, self.visualization_config[item.name]["parts"] - ) - else: - for idx, o in enumerate(item.parts): - 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, current, needed): - """Visualize progress of progressing item as a green bar under the item.""" - if current != 0: - bar_height = self.counter_size * 0.2 - progress_width = (current / needed) * self.counter_size - progress_bar = pygame.Rect( - pos[0] - (self.counter_size / 2), - pos[1] - (self.counter_size / 2) + self.counter_size - bar_height, - progress_width, - bar_height, - ) - pygame.draw.rect(self.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. - The visual composition of the counter is read in from visualization.yaml file, where it is specified as - different parts to be drawn. - - Args: - counter: The counter to visualize. - """ - - self.draw_thing(counter.pos, self.visualization_config["Counter"]["parts"]) - if str(counter) in self.visualization_config: - self.draw_thing( - counter.pos, self.visualization_config[str(counter)]["parts"] - ) - else: - self.draw_thing( - counter.pos, - self.visualization_config[counter.__class__.__name__]["parts"], - ) - - if counter.occupied_by is not None: - # Multiple plates on plate return: - if isinstance(counter.occupied_by, (list, deque)): - with self.simulator.env.lock: - for i, o in enumerate(counter.occupied_by): - self.draw_item( - np.abs([counter.pos[0], counter.pos[1] - (i * 3)]), o - ) - # All other items: - else: - self.draw_item(counter.pos, counter.occupied_by) - - def draw_counters(self, state): - """Visualizes the counters in the environment. - - Args: - state: The game state returned by the environment. - """ - for counter in state["counters"]: - self.draw_counter(counter) - if SHOW_COUNTER_CENTERS: - pygame.draw.circle(self.screen, colors["green1"], counter.pos, 3) - - def draw(self, state): - """Main visualization function. - - Args: - state: The game state returned by the environment. - """ - self.screen.fill( - colors[self.visualization_config["Kitchen"]["ground_tiles_color"]] - ) - self.draw_background() - - self.draw_counters(state) - self.draw_players(state) - - pygame.display.flip() - - def start_pygame(self): - """Starts pygame and the gui loop. Each frame the game state is visualized and keyboard inputs are read.""" - log.debug(f"Starting pygame gui at {self.FPS} fps") - pygame.init() - pygame.font.init() - - self.screen = pygame.display.set_mode((self.window_width, self.window_height)) - pygame.display.set_caption("Simple Overcooked Simulator") - self.screen.fill( - colors[self.visualization_config["Kitchen"]["ground_tiles_color"]] - ) - - clock = pygame.time.Clock() - - # Game loop - running = True - while running: - try: - for event in pygame.event.get(): - if event.type == pygame.QUIT: - running = False - if event.type in [pygame.KEYDOWN, pygame.KEYUP]: - self.handle_key_event(event) - - self.handle_keys() - clock.tick(self.FPS) - state = self.simulator.get_state() - self.draw(state) - - except KeyboardInterrupt: - pygame.quit() - self.simulator.stop() - sys.exit() - - pygame.quit() diff --git a/overcooked_simulator/simulation_runner.py b/overcooked_simulator/simulation_runner.py index 795b61e63a2fb35f6f6802de94e6ffdde8ddf913..8ad41e28ae7bb2834297cadeaa2d06f915e1c87a 100644 --- a/overcooked_simulator/simulation_runner.py +++ b/overcooked_simulator/simulation_runner.py @@ -1,5 +1,6 @@ import logging import time +from datetime import timedelta from threading import Thread import numpy as np @@ -26,7 +27,7 @@ class Simulator(Thread): def __init__( self, - env_layout_path, + env_config_path, layout_path, frequency: int, item_info_path=ROOT_DIR / "game_content" / "item_info.yaml", @@ -39,14 +40,14 @@ class Simulator(Thread): self.step_frequency: int = frequency self.preferred_sleep_time_ns: float = 1e9 / self.step_frequency self.env: Environment = Environment( - env_layout_path, layout_path, item_info_path + env_config_path, layout_path, item_info_path ) super().__init__() - def step(self): + def step(self, passed_time: timedelta): """One simulation step of the environment.""" - self.env.step() + self.env.step(passed_time) def enter_action(self, action: Action): """Takes an action and executes it in the environment. @@ -96,10 +97,10 @@ class Simulator(Thread): """Starts the simulator thread. Runs in a loop until stopped.""" overslept_in_ns = 0 - + self.env.reset_env_time() while not self.finished: step_start = time.time_ns() - self.step() + self.step(timedelta(seconds=overslept_in_ns / 1_000_000_000)) step_duration = time.time_ns() - step_start time_to_sleep_ns = self.preferred_sleep_time_ns - ( diff --git a/overcooked_simulator/utils.py b/overcooked_simulator/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..4e4ab7adac96f14999878ac6acd442f2bc8e03b5 --- /dev/null +++ b/overcooked_simulator/utils.py @@ -0,0 +1,7 @@ +from datetime import datetime + + +def create_init_env_time(): + return datetime( + year=2000, month=1, day=1, hour=0, minute=0, second=0, microsecond=0 + ) diff --git a/setup.py b/setup.py index 69f30ff36f7dbc7780cf15b9f4f6971aa54da00d..b3ceeba705d5d329be64f2dc239e4de19dbc5056 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ with open("README.md") as readme_file: with open("CHANGELOG.md") as history_file: history = history_file.read() -requirements = ["numpy", "pygame", "scipy", "pytest>=3", "pyyaml"] +requirements = ["numpy", "pygame", "scipy", "pytest>=3", "pyyaml", "pygame-gui"] test_requirements = [ "pytest>=3", diff --git a/tests/test_start.py b/tests/test_start.py index a8fc103ec0eb8457e6bb2403031ff931dbb49a2b..64fce11daf8fb14381605e96180e55b2db40e827 100644 --- a/tests/test_start.py +++ b/tests/test_start.py @@ -1,4 +1,5 @@ import time +from datetime import timedelta import numpy as np import pytest @@ -6,8 +7,9 @@ import pytest from overcooked_simulator import ROOT_DIR from overcooked_simulator.counters import Counter, CuttingBoard from overcooked_simulator.game_items import CuttableItem -from overcooked_simulator.overcooked_environment import Action +from overcooked_simulator.overcooked_environment import Action, Environment from overcooked_simulator.simulation_runner import Simulator +from overcooked_simulator.utils import create_init_env_time layouts_folder = ROOT_DIR / "game_content" / "layouts" @@ -51,9 +53,12 @@ def test_simulator_frequency(): def __init__(self): self.c = 0 - def step(self): + def step(self, passed_time): self.c += 1 + def reset_env_time(self): + pass + frequency = 2000 running_time_seconds = 2 @@ -68,7 +73,6 @@ def test_simulator_frequency(): time.sleep(running_time_seconds) sim.stop() - print(sim.env.c) accepted_tolerance = 0.02 lower = frequency * running_time_seconds * (1 - accepted_tolerance) upper = frequency * running_time_seconds * (1 + accepted_tolerance) @@ -82,7 +86,7 @@ def test_movement(): 200, ) player_name = "p1" - start_pos = np.array([50, 50]) + start_pos = np.array([1, 2]) sim.register_player(player_name, start_pos) move_direction = np.array([1, 0]) move_action = Action(player_name, "movement", move_direction) @@ -94,8 +98,8 @@ def test_movement(): move_direction * sim.env.players[player_name].move_dist ) - assert ( - np.linalg.norm(expected - sim.env.players[player_name].pos) == 0 + assert np.isclose( + np.linalg.norm(expected - sim.env.players[player_name].pos), 0 ), "Should be here?" @@ -105,11 +109,11 @@ def test_collision_detection(): layouts_folder / "basic.layout", 200, ) - counter_pos = np.array([50, 100]) + counter_pos = np.array([1, 2]) counter = Counter(counter_pos) sim.env.counters = [counter] - sim.register_player("p1", np.array([50, 50])) - sim.register_player("p2", np.array([50, 200])) # same player name + sim.register_player("p1", np.array([1, 1])) + sim.register_player("p2", np.array([1, 4])) # same player name player1 = sim.env.players["p1"] player2 = sim.env.players["p2"] @@ -142,10 +146,10 @@ def test_player_reach(): 200, ) - counter_pos = np.array([100, 100]) + counter_pos = np.array([2, 2]) counter = Counter(counter_pos) sim.env.counters = [counter] - sim.register_player("p1", np.array([100, 200])) + sim.register_player("p1", np.array([2, 4])) player = sim.env.players["p1"] assert not player.can_reach(counter), "Player is too far away." @@ -163,12 +167,12 @@ def test_pickup(): 200, ) - counter_pos = np.array([100, 100]) + counter_pos = np.array([2, 2]) counter = Counter(counter_pos) counter.occupied_by = CuttableItem(name="Tomato", item_info=None) sim.env.counters = [counter] - sim.register_player("p1", np.array([100, 160])) + sim.register_player("p1", np.array([2, 3])) player = sim.env.players["p1"] move_down = Action("p1", "movement", np.array([0, -1])) @@ -214,12 +218,12 @@ def test_processing(): ) sim.start() - counter_pos = np.array([100, 100]) + counter_pos = np.array([2, 2]) counter = CuttingBoard(counter_pos) - sim.env.counters = [counter] + sim.env.counters.append(counter) tomato = CuttableItem(name="Tomato", item_info=None) - sim.register_player("p1", np.array([100, 150])) + sim.register_player("p1", np.array([2, 3])) player = sim.env.players["p1"] player.holding = tomato @@ -243,3 +247,24 @@ def test_processing(): sim.enter_action(button_up) sim.stop() + + +def test_time_passed(): + np.random.seed(42) + env = Environment( + ROOT_DIR / "game_content" / "environment_config.yaml", + layouts_folder / "empty.layout", + ROOT_DIR / "game_content" / "item_info.yaml", + ) + env.reset_env_time() + passed_time = timedelta(seconds=10) + env.step(passed_time) + assert ( + env.env_time == create_init_env_time() + passed_time + ), "Env time needs to be updated via the step function" + + passed_time_2 = timedelta(seconds=12) + env.step(passed_time_2) + assert ( + env.env_time == create_init_env_time() + passed_time + passed_time_2 + ), "Env time needs to be updated via the step function"