diff --git a/overcooked_simulator/game_content/layouts/empty.layout b/overcooked_simulator/game_content/layouts/empty.layout index de0f1b9eb58638cd6a2b94b6ae4591bc72e6144d..3c68f9d5a46bca946f2ff71b3141b8ce809a9fd4 100644 --- a/overcooked_simulator/game_content/layouts/empty.layout +++ b/overcooked_simulator/game_content/layouts/empty.layout @@ -1,3 +1,4 @@ _____ _____ -____P \ No newline at end of file +____P + diff --git a/overcooked_simulator/game_content/player_config.yaml b/overcooked_simulator/game_content/player_config.yaml index 4e863b3d5a470b3c094cfb4c75362f96011eeddc..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: 0.2 +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/pygame_gui/images/tomato.png b/overcooked_simulator/gui_2d_vis/images/tomato.png similarity index 100% rename from overcooked_simulator/pygame_gui/images/tomato.png rename to overcooked_simulator/gui_2d_vis/images/tomato.png diff --git a/overcooked_simulator/pygame_gui/images/tomato_cut.png b/overcooked_simulator/gui_2d_vis/images/tomato_cut.png similarity index 100% rename from overcooked_simulator/pygame_gui/images/tomato_cut.png rename to overcooked_simulator/gui_2d_vis/images/tomato_cut.png diff --git a/overcooked_simulator/pygame_gui/images/tomato_soup.png b/overcooked_simulator/gui_2d_vis/images/tomato_soup.png similarity index 100% rename from overcooked_simulator/pygame_gui/images/tomato_soup.png rename to overcooked_simulator/gui_2d_vis/images/tomato_soup.png diff --git a/overcooked_simulator/pygame_gui/pygame_gui.py b/overcooked_simulator/gui_2d_vis/overcooked_gui.py similarity index 63% rename from overcooked_simulator/pygame_gui/pygame_gui.py rename to overcooked_simulator/gui_2d_vis/overcooked_gui.py index 7e4dfae288fd10406731cddc8b1380e6c43c4970..47edef81a9d458845866438dca2e7a387d1b7ad5 100644 --- a/overcooked_simulator/pygame_gui/pygame_gui.py +++ b/overcooked_simulator/gui_2d_vis/overcooked_gui.py @@ -3,10 +3,12 @@ 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 @@ -19,9 +21,9 @@ from overcooked_simulator.game_items import ( 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.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 @@ -29,6 +31,12 @@ 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]) @@ -86,9 +94,10 @@ class PyGameGUI: player_names: list[str], player_keys: list[pygame.key], ): - self.screen = None + self.game_screen = None self.FPS = 60 - self.simulator = simulator + self.simulator: Simulator = simulator + self.player_names = player_names self.player_keys = player_keys @@ -100,29 +109,43 @@ class PyGameGUI: ] # TODO cache loaded images? - with open(ROOT_DIR / "pygame_gui" / "visualization.yaml", "r") as file: + with open(ROOT_DIR / "gui_2d_vis" / "visualization.yaml", "r") as file: self.visualization_config = yaml.safe_load(file) if self.visualization_config["GameWindow"]["WhatIsFixed"] == "window_width": - self.window_width = self.visualization_config["GameWindow"]["size"] + self.world_width = self.visualization_config["GameWindow"]["size"] kitchen_aspect_ratio = ( simulator.env.kitchen_height / simulator.env.kitchen_width ) - self.window_height = int(self.window_width * kitchen_aspect_ratio) - self.grid_size = int(self.window_width / simulator.env.kitchen_width) + self.world_height = int(self.world_width * kitchen_aspect_ratio) + self.grid_size = int(self.world_width / simulator.env.kitchen_width) elif self.visualization_config["GameWindow"]["WhatIsFixed"] == "grid": self.grid_size = self.visualization_config["GameWindow"]["size"] - self.window_width, self.window_height = ( + self.world_width, self.world_height = ( simulator.env.kitchen_width * self.grid_size, simulator.env.kitchen_height * self.grid_size, ) + self.screen_margin = self.visualization_config["GameWindow"]["screen_margin"] + + self.window_width, self.window_height = ( + self.world_width + (2 * self.screen_margin), + self.world_height + (2 * self.screen_margin), + ) + self.game_width, self.game_height = ( + self.world_width, + self.world_height, + ) + self.images_path = ROOT_DIR / "pygame_gui" / "images" self.player_colors = self.create_player_colors() self.image_cache_dict = {} + self.menu_state = MenuStates.Start + self.manager: pygame_gui.UIManager + def create_player_colors(self) -> list[Color]: number_player = len(self.simulator.env.players) hue_values = np.linspace(0, 1, number_player + 1) @@ -192,11 +215,14 @@ class PyGameGUI: 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.screen, + self.game_screen, self.visualization_config["Kitchen"]["background_lines"], rect, 1, @@ -208,7 +234,7 @@ class PyGameGUI: image = self.image_cache_dict[cache_entry] else: image = pygame.image.load( - ROOT_DIR / "pygame_gui" / img_path + ROOT_DIR / "gui_2d_vis" / img_path ).convert_alpha() self.image_cache_dict[cache_entry] = image @@ -217,7 +243,7 @@ class PyGameGUI: image = pygame.transform.rotate(image, rot_angle) rect = image.get_rect() rect.center = pos - self.screen.blit(image, rect) + self.game_screen.blit(image, rect) def draw_players(self, state): """Visualizes the players as circles with a triangle for the facing direction. @@ -244,13 +270,13 @@ class PyGameGUI: 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) + 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.screen, + self.game_screen, BLUE, ( ( @@ -267,14 +293,20 @@ class PyGameGUI: if SHOW_INTERACTION_RANGE: pygame.draw.circle( - self.screen, + self.game_screen, BLUE, player.facing_point * self.grid_size, player.interaction_range * self.grid_size, width=1, ) pygame.draw.circle( - self.screen, colors["red1"], player.facing_point * self.grid_size, 4 + 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: @@ -287,7 +319,7 @@ class PyGameGUI: counter: Counter = player.current_nearest_counter pos = counter.pos * self.grid_size pygame.draw.rect( - self.screen, + self.game_screen, colors[self.player_colors[p_idx]], rect=pygame.Rect( pos[0] - (self.grid_size // 2), @@ -325,7 +357,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.screen, color, rect) + pygame.draw.rect(self.game_screen, color, rect) else: rect = pygame.Rect( pos[0] - (height / 2), @@ -333,19 +365,19 @@ class PyGameGUI: height, width, ) - pygame.draw.rect(self.screen, color, rect) + 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.screen, + self.game_screen, color, - pos + (np.array(part["center_offset"])*self.grid_size), + pos + (np.array(part["center_offset"]) * self.grid_size), radius, ) else: - pygame.draw.circle(self.screen, color, pos, radius) + 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. @@ -390,7 +422,7 @@ class PyGameGUI: progress_width, bar_height, ) - pygame.draw.rect(self.screen, colors["green1"], progress_bar) + 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. @@ -431,9 +463,7 @@ class PyGameGUI: for counter in state["counters"]: self.draw_counter(counter) if SHOW_COUNTER_CENTERS: - pygame.draw.circle( - self.screen, colors["green1"], counter.pos * self.grid_size, 3 - ) + pygame.draw.circle(self.game_screen, colors["green1"], counter.pos, 3) def draw(self, state): """Main visualization function. @@ -441,15 +471,150 @@ class PyGameGUI: 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) + 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], + ) - pygame.display.flip() + 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 change_to_start_window(self): + self.simulator.stop() + self.menu_state = 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() + + def change_to_game_window(self): + self.menu_state = MenuStates.Game + self.start_button.hide() + self.back_button.show() + self.score_label.hide() + self.finished_button.show() + self.layout_selection.hide() + 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) + + def change_to_end_window(self): + self.simulator.stop() + self.menu_state = 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_pygame(self): """Starts pygame and the gui loop. Each frame the game state is visualized and keyboard inputs are read.""" @@ -457,32 +622,90 @@ class PyGameGUI: 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"]] + self.init_ui_elements() + + self.screen_margin = 100 + self.main_window = pygame.display.set_mode( + ( + self.window_width, + self.window_height, + ) + ) + self.game_screen = pygame.Surface( + ( + self.game_width, + self.game_height, + ), ) + pygame.display.set_caption("Simple Overcooked Simulator") clock = pygame.time.Clock() + self.change_to_start_window() # Game loop running = True while running: try: + time_delta = clock.tick(self.FPS) / 1000.0 + for event in pygame.event.get(): if event.type == pygame.QUIT: running = False - if event.type in [pygame.KEYDOWN, pygame.KEYUP]: + if event.type == pygame_gui.UI_BUTTON_PRESSED: + if event.ui_element == self.start_button: + self.change_to_game_window() + if event.ui_element == self.back_button: + self.change_to_start_window() + if event.ui_element == self.finished_button: + self.change_to_end_window() + if event.ui_element == self.quit_button: + running = False + log.debug("Quitting game") + + if ( + event.type in [pygame.KEYDOWN, pygame.KEYUP] + and self.menu_state == MenuStates.Game + ): self.handle_key_event(event) - self.handle_keys() - clock.tick(self.FPS) - state = self.simulator.get_state() - self.draw(state) + 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: - pygame.quit() 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 99% rename from overcooked_simulator/pygame_gui/visualization.yaml rename to overcooked_simulator/gui_2d_vis/visualization.yaml index cc229b5e0a4332e9f12dab0c34c553735a38e7c8..081f6b0b36357f94985050eaee13c5bb50ab6c35 100644 --- a/overcooked_simulator/pygame_gui/visualization.yaml +++ b/overcooked_simulator/gui_2d_vis/visualization.yaml @@ -2,7 +2,8 @@ GameWindow: WhatIsFixed: window_width # entweder grid oder window_width - size: 600 + size: 500 + screen_margin: 100 Kitchen: ground_tiles_color: sgigray76 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/simulation_runner.py b/overcooked_simulator/simulation_runner.py index 795b61e63a2fb35f6f6802de94e6ffdde8ddf913..d479efb8bb3c9d8c94c8fa811248a0abc461e3c0 100644 --- a/overcooked_simulator/simulation_runner.py +++ b/overcooked_simulator/simulation_runner.py @@ -26,7 +26,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,7 +39,7 @@ 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__() 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",