From e7cdb3baaafa597a778df74fb403fc3e905e9c61 Mon Sep 17 00:00:00 2001 From: fheinrich <fheinrich@techfak.uni-bielefeld.de> Date: Wed, 10 Jan 2024 15:41:26 +0100 Subject: [PATCH] Started decoupling pygame gui from simulator --- .../layouts/godot_test_layout.layout | 9 ++ .../game_content/layouts/split.layout | 9 ++ overcooked_simulator/game_server.py | 98 ++++++++++++ .../gui_2d_vis/overcooked_gui.py | 139 +++++++++++++----- overcooked_simulator/main.py | 6 +- .../overcooked_environment.py | 24 ++- overcooked_simulator/simulation_runner.py | 4 +- overcooked_simulator/websocket_connector.py | 0 setup.py | 10 +- 9 files changed, 254 insertions(+), 45 deletions(-) create mode 100644 overcooked_simulator/game_content/layouts/godot_test_layout.layout create mode 100644 overcooked_simulator/game_content/layouts/split.layout create mode 100644 overcooked_simulator/game_server.py create mode 100644 overcooked_simulator/websocket_connector.py diff --git a/overcooked_simulator/game_content/layouts/godot_test_layout.layout b/overcooked_simulator/game_content/layouts/godot_test_layout.layout new file mode 100644 index 00000000..06db451a --- /dev/null +++ b/overcooked_simulator/game_content/layouts/godot_test_layout.layout @@ -0,0 +1,9 @@ +########## +#________# +#________# +#________# +#________# +#________# +#________# +#________# +#########P diff --git a/overcooked_simulator/game_content/layouts/split.layout b/overcooked_simulator/game_content/layouts/split.layout new file mode 100644 index 00000000..39bace3e --- /dev/null +++ b/overcooked_simulator/game_content/layouts/split.layout @@ -0,0 +1,9 @@ +#QU#T###NLB# +#__________M +#____A_____# +W__________# +############ +C__________# +C_____A____# +#__________X +#P#S+####S+# \ No newline at end of file diff --git a/overcooked_simulator/game_server.py b/overcooked_simulator/game_server.py new file mode 100644 index 00000000..3832221d --- /dev/null +++ b/overcooked_simulator/game_server.py @@ -0,0 +1,98 @@ +import asyncio +import json +import logging +import os +import sys +import threading +from datetime import datetime + +import numpy as np +from websockets.server import serve + +from overcooked_simulator import ROOT_DIR +from overcooked_simulator.overcooked_environment import Action +from overcooked_simulator.simulation_runner import Simulator + +log = logging.getLogger(__name__) + + +PORT = 8765 + + +class Connector: + def __init__(self, simulator: Simulator): + self.simulator: Simulator = simulator + + super().__init__() + + async def process_message(self, websocket): + async for message in websocket: + if message.replace('"', "") != "get_state": + message_dict = json.loads(message) + if message_dict["act_type"] == "movement": + value = np.array(message_dict["value"]) + else: + value = None + action = Action( + message_dict["player_name"], message_dict["act_type"], value + ) + self.simulator.enter_action(action) + + json_answer = self.simulator.get_state_simple_json() + + # print("json:", json_answer, type(json_answer)) + await websocket.send(json_answer) + + async def connection_server(self): + async with serve(self.process_message, "localhost", PORT): + await asyncio.Future() # run forever + + def set_sim(self, simulation_runner: Simulator): + self.simulator = simulation_runner + + def start_connector(self): + asyncio.run(self.connection_server()) + + +def setup_logging(): + path_logs = ROOT_DIR.parent / "logs" + os.makedirs(path_logs, exist_ok=True) + logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s %(levelname)-8s %(name)-50s %(message)s", + handlers=[ + logging.FileHandler( + path_logs / f"{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}_debug.log", + encoding="utf-8", + ), + logging.StreamHandler(sys.stdout), + ], + ) + + +def main(): + simulator = Simulator( + ROOT_DIR / "game_content" / "environment_config.yaml", + ROOT_DIR / "game_content" / "layouts" / "basic.layout", + 600, + ) + number_player = 2 + for i in range(number_player): + player_name = f"p{i}" + simulator.register_player(player_name) + + connector = Connector(simulator) + connector.start_connector() + + +if __name__ == "__main__": + setup_logging() + try: + main() + except Exception as e: + log.exception(e) + for thread in threading.enumerate(): + if isinstance(thread, Simulator): + thread.stop() + thread.join() + sys.exit(1) diff --git a/overcooked_simulator/gui_2d_vis/overcooked_gui.py b/overcooked_simulator/gui_2d_vis/overcooked_gui.py index 7c910c07..7d7e8aea 100644 --- a/overcooked_simulator/gui_2d_vis/overcooked_gui.py +++ b/overcooked_simulator/gui_2d_vis/overcooked_gui.py @@ -1,4 +1,5 @@ import colorsys +import json import logging import math import sys @@ -11,6 +12,7 @@ import pygame import pygame_gui import yaml from scipy.spatial import KDTree +from websockets.sync.client import connect from overcooked_simulator import ROOT_DIR from overcooked_simulator.counters import Counter @@ -26,9 +28,9 @@ 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 +USE_PLAYER_COOK_SPRITES = False SHOW_INTERACTION_RANGE = False -SHOW_COUNTER_CENTERS = False +SHOW_COUNTER_CENTERS = True class MenuStates(Enum): @@ -90,13 +92,11 @@ class PyGameGUI: 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 @@ -109,6 +109,8 @@ class PyGameGUI: ) ] + self.websocket_url = "ws://localhost:8765" + # TODO cache loaded images? with open(ROOT_DIR / "gui_2d_vis" / "visualization.yaml", "r") as file: self.visualization_config = yaml.safe_load(file) @@ -134,22 +136,35 @@ class PyGameGUI: 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 + state = self.request_state() + print("THIS:", state["counters"]) + counter_positions = np.array([c["pos"] for c in state["counters"]]) + print(counter_positions) + kitchen_width = np.max(counter_positions[:, 0]) + 0.5 + kitchen_height = np.max(counter_positions[:, 1]) + 0.5 + + print(kitchen_width, kitchen_height) + + # 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 + + grid_size = 40 + game_width = kitchen_width * grid_size + game_height = kitchen_height * grid_size window_width, window_height = ( game_width + (2 * self.screen_margin), @@ -159,7 +174,7 @@ class PyGameGUI: 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) + number_player = len(self.player_keys) hue_values = np.linspace(0, 1, number_player + 1) colors_vec = np.array([col for col in colors.values()]) @@ -177,14 +192,6 @@ class PyGameGUI: 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. @@ -230,8 +237,8 @@ class PyGameGUI: 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): + for x in range(0, int(self.window_width), block_size): + for y in range(0, int(self.window_height), block_size): rect = pygame.Rect(x, y, block_size, block_size) pygame.draw.rect( self.game_screen, @@ -257,7 +264,7 @@ class PyGameGUI: rect.center = pos self.game_screen.blit(image, rect) - def draw_players(self, state): + def draw_players(self, state, state_dict): """Visualizes the players as circles with a triangle for the facing direction. If the player holds something in their hands, it is displayed @@ -265,11 +272,14 @@ class PyGameGUI: state: The game state returned by the environment. """ for p_idx, player in enumerate(state["players"].values()): - pos = player.pos * self.grid_size + # pos = player.pos * self.grid_size + pos = np.array(state_dict["players"][p_idx]["pos"]) * self.grid_size + + facing = np.array(state_dict["players"][p_idx]["facing"]) if USE_PLAYER_COOK_SPRITES: img_path = self.visualization_config["Cook"]["parts"][0]["path"] - rel_x, rel_y = player.facing_direction + rel_x, rel_y = pos = facing angle = -np.rad2deg(math.atan2(rel_y, rel_x)) + 90 size = ( self.visualization_config["Cook"]["parts"][0]["size"] @@ -286,7 +296,7 @@ class PyGameGUI: 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 + facing = facing pygame.draw.polygon( self.game_screen, BLUE, @@ -475,9 +485,11 @@ class PyGameGUI: for counter in state["counters"]: self.draw_counter(counter) if SHOW_COUNTER_CENTERS: - pygame.draw.circle(self.game_screen, colors["green1"], counter.pos, 3) + pygame.draw.circle( + self.game_screen, colors["green1"], counter.pos * self.grid_size, 3 + ) - def draw(self, state): + def draw(self, state, state_dict): """Main visualization function. Args: @@ -487,7 +499,7 @@ class PyGameGUI: self.draw_background() self.draw_counters(state) - self.draw_players(state) + self.draw_players(state, state_dict) self.manager.draw_ui(self.game_screen) def init_ui_elements(self): @@ -654,6 +666,8 @@ class PyGameGUI: self.init_ui_elements() log.debug("Pressed start button") + # self.api.set_sim(self.simulator) + def back_button_press(self): self.simulator.stop() self.menu_state = MenuStates.Start @@ -669,6 +683,35 @@ class PyGameGUI: self.menu_state = MenuStates.End log.debug("Pressed finished button") + 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. + """ + if isinstance(action.action, np.ndarray): + value = [float(action.action[0]), float(action.action[1])] + else: + value = action.action + message_dict = { + "player_name": action.player, + "act_type": action.act_type, + "value": value, + } + _ = self.websocket_communicate(message_dict) + + def websocket_communicate(self, message_dict: dict | str): + with connect(self.websocket_url) as websocket: + print(message_dict) + websocket.send(json.dumps(message_dict)) + answer = websocket.recv() + print(answer) + return json.loads(answer) + + def request_state(self): + state_dict = self.websocket_communicate("get_state") + return state_dict + 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") @@ -727,8 +770,9 @@ class PyGameGUI: self.handle_keys() + state_dict = self.request_state() state = self.simulator.get_state() - self.draw(state) + self.draw(state, state_dict) game_screen_rect = self.game_screen.get_rect() game_screen_rect.center = [ @@ -753,3 +797,20 @@ class PyGameGUI: self.simulator.stop() pygame.quit() sys.exit() + + +if __name__ == "__main__": + # TODO maybe read the player names and keyboard keys from config file? + keys1 = [ + pygame.K_LEFT, + pygame.K_RIGHT, + pygame.K_UP, + pygame.K_DOWN, + pygame.K_SPACE, + pygame.K_i, + ] + keys2 = [pygame.K_a, pygame.K_d, pygame.K_w, pygame.K_s, pygame.K_f, pygame.K_e] + + number_players = 2 + gui = PyGameGUI([f"p{i}" for i in range(number_players)], [keys1, keys2]) + gui.start_pygame() diff --git a/overcooked_simulator/main.py b/overcooked_simulator/main.py index 86caba35..81a5d8f0 100644 --- a/overcooked_simulator/main.py +++ b/overcooked_simulator/main.py @@ -51,10 +51,14 @@ def main(): ] 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]) + gui = PyGameGUI( + simulator, None, [f"p{i}" for i in range(number_player)], [keys1, keys2] + ) gui.start_pygame() sys.exit() + print("HERE?") + if __name__ == "__main__": setup_logging() diff --git a/overcooked_simulator/overcooked_environment.py b/overcooked_simulator/overcooked_environment.py index 8f5adf96..c3bf5502 100644 --- a/overcooked_simulator/overcooked_environment.py +++ b/overcooked_simulator/overcooked_environment.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json import logging import random from datetime import timedelta @@ -177,6 +178,7 @@ class Environment: for character in line: character = character.capitalize() pos = np.array([current_x, current_y]) + print(pos) counter_class = self.SYMBOL_TO_CHARACTER_MAP[character] if not isinstance(counter_class, str): counter = counter_class(pos) @@ -450,13 +452,31 @@ class Environment: """ return {"players": self.players, "counters": self.counters, "score": self.score} - def get_state_json(self): + def get_state_simple_json(self): """Get the current state of the game environment as a json-like nested dictionary. Returns: Json-like string of the current game state. """ - pass + players = [ + { + "pos": [float(p.pos[0]), float(p.pos[1])], + "facing": [float(p.facing_direction[0]), float(p.facing_direction[1])], + } + for p in self.players.values() + ] + + counters = [ + {"type": str(c.__class__), "pos": [float(c.pos[0]), float(c.pos[1])]} + for c in self.counters + ] + + gamestate_dict = { + "players": players, + "counters": counters, + } + answer = json.dumps(gamestate_dict) + return answer def init_counters(self): plate_dispenser = self.get_counter_of_type(PlateDispenser) diff --git a/overcooked_simulator/simulation_runner.py b/overcooked_simulator/simulation_runner.py index 8ad41e28..f004ecdd 100644 --- a/overcooked_simulator/simulation_runner.py +++ b/overcooked_simulator/simulation_runner.py @@ -66,14 +66,14 @@ class Simulator(Thread): return self.env.get_state() - def get_state_json(self): + def get_state_simple_json(self): """Get the current game state in json-like dict. Returns: The gamest ate encoded in a json style nested dict. """ - return self.env.get_state_json() + return self.env.get_state_simple_json() def register_player(self, player_name: str, pos=None): """Adds a player to the environment. diff --git a/overcooked_simulator/websocket_connector.py b/overcooked_simulator/websocket_connector.py new file mode 100644 index 00000000..e69de29b diff --git a/setup.py b/setup.py index b3ceeba7..f0324963 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,15 @@ 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", "pygame-gui"] +requirements = [ + "numpy", + "pygame", + "scipy", + "pytest>=3", + "pyyaml", + "pygame-gui", + "websockets", +] test_requirements = [ "pytest>=3", -- GitLab