diff --git a/overcooked_simulator/__main__.py b/overcooked_simulator/__main__.py index f48180ac4aa23e525646e844d9e53b9f49a71595..1520c7e666ea6ed3919885240abb174d3b78e4bb 100644 --- a/overcooked_simulator/__main__.py +++ b/overcooked_simulator/__main__.py @@ -8,6 +8,8 @@ from overcooked_simulator.utils import ( add_list_of_manager_ids_arguments, ) +USE_STUDY_SERVER = True + def start_game_server(cli_args): from overcooked_simulator.game_server import main @@ -15,40 +17,98 @@ def start_game_server(cli_args): main(cli_args.url, cli_args.port, cli_args.manager_ids) +def start_study_server(cli_args): + from overcooked_simulator.example_study_server import main + + main( + cli_args.url, + cli_args.port, + game_server_url_="localhost:8000", + manager_ids=cli_args.manager_ids, + ) + + def start_pygame_gui(cli_args): from overcooked_simulator.gui_2d_vis.overcooked_gui import main - main(cli_args.url, cli_args.port, cli_args.manager_ids) + main( + cli_args.url, + cli_args.port, + cli_args.manager_ids, + CONNECT_WITH_STUDY_SERVER=USE_STUDY_SERVER, + ) def main(cli_args=None): + study_server = None + study_server_args = None + if USE_STUDY_SERVER: + parser = argparse.ArgumentParser( + prog="Overcooked Simulator Study Server", + description="Study Server: Match Making, client pre and post managing.", + epilog="For further information, see https://scs.pages.ub.uni-bielefeld.de/cocosy/overcooked-simulator/overcooked_simulator.html", + ) + url_and_port_arguments( + parser=parser, server_name="Study Server", default_port=8080 + ) + add_list_of_manager_ids_arguments(parser=parser) + study_server_args = parser.parse_args() + if cli_args is None: parser = argparse.ArgumentParser( prog="Overcooked Simulator", description="Game Engine Server + PyGameGUI: Starts overcooked game engine server and a PyGame 2D Visualization window in two processes.", epilog="For further information, see https://scs.pages.ub.uni-bielefeld.de/cocosy/overcooked-simulator/overcooked_simulator.html", ) - url_and_port_arguments(parser) disable_websocket_logging_arguments(parser) add_list_of_manager_ids_arguments(parser) cli_args = parser.parse_args() + game_server = None pygame_gui = None try: print("Start game engine:") game_server = Process(target=start_game_server, args=(cli_args,)) game_server.start() - time.sleep(1) + time.sleep(0.5) + + if USE_STUDY_SERVER: + print("Start study server:") + study_server = Process(target=start_study_server, args=(study_server_args,)) + study_server.start() + time.sleep(0.5) + print("Start PyGame GUI:") pygame_gui = Process(target=start_pygame_gui, args=(cli_args,)) pygame_gui.start() - while pygame_gui.is_alive() and game_server.is_alive(): - time.sleep(1) + + if USE_STUDY_SERVER: + print("Start PyGame GUI:") + pygame_gui_2 = Process(target=start_pygame_gui, args=(cli_args,)) + pygame_gui_2.start() + + print("Start PyGame GUI:") + pygame_gui_3 = Process(target=start_pygame_gui, args=(cli_args,)) + pygame_gui_3.start() + while ( + pygame_gui.is_alive() + and pygame_gui_2.is_alive() + and pygame_gui_3.is_alive() + ): + time.sleep(1) + + else: + while pygame_gui.is_alive(): + time.sleep(1) + except KeyboardInterrupt: print("Received Keyboard interrupt") finally: + if USE_STUDY_SERVER and study_server is not None and study_server.is_alive(): + print("Terminate study server") + study_server.terminate() if game_server is not None and game_server.is_alive(): print("Terminate game server") game_server.terminate() diff --git a/overcooked_simulator/counter_factory.py b/overcooked_simulator/counter_factory.py index 639459a56508c627eb5015aa5b3080bfcd2a1737..a2c91e2cc4dd462e9377d689994a23d952ea9f58 100644 --- a/overcooked_simulator/counter_factory.py +++ b/overcooked_simulator/counter_factory.py @@ -195,8 +195,8 @@ class CounterFactory: assert self.can_map(c), f"Can't map counter char {c}" counter_class = None - if c == "@": - print("-") + # if c == "@": + # print("-") if self.layout_chars_config[c] in self.item_info: item_info = self.item_info[self.layout_chars_config[c]] if item_info.type == ItemType.Equipment and item_info.equipment: diff --git a/overcooked_simulator/example_study_server.py b/overcooked_simulator/example_study_server.py index 778135f47054e0cdc4a948f7f3a1098b95542426..663976d3a9638bf70b639a3d7342839e722802e4 100644 --- a/overcooked_simulator/example_study_server.py +++ b/overcooked_simulator/example_study_server.py @@ -14,10 +14,12 @@ The environment starts when all players connected. import argparse import asyncio import logging -from typing import Tuple +from pathlib import Path +from typing import Tuple, TypedDict import requests import uvicorn +import yaml from fastapi import FastAPI from overcooked_simulator import ROOT_DIR @@ -32,7 +34,6 @@ NUMBER_PLAYER_PER_ENV = 2 log = logging.getLogger(__name__) - app = FastAPI() game_server_url = "localhost:8000" @@ -53,48 +54,74 @@ server_manager_id = None # </html> # """ -running_envs: dict[str, Tuple[int, dict[str, PlayerInfo], list[str]]] = {} -current_free_envs = [] + +HARDCODED_MANAGER_ID = "1234" + + +running_tutorials: dict[str, Tuple[int, dict[str, PlayerInfo], list[str]]] = {} + + +class LevelConfig(TypedDict): + name: str + config_path: str + layout_path: str + item_info_path: str + + +class StudyConfig(TypedDict): + levels: list[LevelConfig] + num_players: int -@app.post("/connect_to_game/{request_id}") -async def want_to_play(request_id: str): - global current_free_envs - # TODO based on study desing / internal state of request id current state (which level to play) - if current_free_envs: - current_free_env = current_free_envs.pop() +class StudyState: + def __init__(self, study_config_path: str | Path): + with open(study_config_path, "r") as file: + env_config_f = file.read() - running_envs[current_free_env][2].append(request_id) - new_running_env = ( - running_envs[current_free_env][0] + 1, - running_envs[current_free_env][1], - running_envs[current_free_env][2], + self.study_config: StudyConfig = yaml.load( + str(env_config_f), Loader=yaml.SafeLoader ) - player_info = running_envs[current_free_env][1][str(new_running_env[0])] - running_envs[current_free_env] = new_running_env - if new_running_env[0] < NUMBER_PLAYER_PER_ENV - 1: - current_free_env.append(current_free_env) - return player_info - else: - environment_config_path = ROOT_DIR / "game_content" / "environment_config.yaml" - layout_path = ROOT_DIR / "game_content" / "layouts" / "basic.layout" - item_info_path = ROOT_DIR / "game_content" / "item_info.yaml" - with open(item_info_path, "r") as file: + self.levels: list[LevelConfig] = self.study_config["levels"] + self.current_level_idx: int = 0 + + self.participant_id_to_player_info = {} + self.player_ids = {} + self.num_connected_players: int = 0 + + self.current_running_env = None + self.next_level_env = None + self.players_done = {} + + @property + def study_done(self): + return self.current_level_idx >= len(self.levels) + + @property + def is_full(self): + return ( + len(self.participant_id_to_player_info) == self.study_config["num_players"] + ) + + def create_env(self, level): + with open(ROOT_DIR / "game_content" / level["item_info_path"], "r") as file: item_info = file.read() - with open(layout_path, "r") as file: + with open( + ROOT_DIR / "game_content" / "layouts" / level["layout_path"], "r" + ) as file: layout = file.read() - with open(environment_config_path, "r") as file: + with open(ROOT_DIR / "game_content" / level["config_path"], "r") as file: environment_config = file.read() + creation_json = CreateEnvironmentConfig( manager_id=server_manager_id, - number_players=NUMBER_PLAYER_PER_ENV, + number_players=self.study_config["num_players"], environment_settings={"all_player_can_pause_game": False}, item_info_config=item_info, environment_config=environment_config, layout_config=layout, seed=1234567890, ).model_dump(mode="json") - # todo async + env_info = requests.post( game_server_url + "/manage/create_env/", json=creation_json ) @@ -102,16 +129,179 @@ async def want_to_play(request_id: str): if env_info.status_code == 403: raise ValueError(f"Forbidden Request: {env_info.json()['detail']}") env_info = env_info.json() - print(env_info) - running_envs[env_info["env_id"]] = (0, env_info["player_info"], [request_id]) - current_free_envs.append(env_info["env_id"]) - return env_info["player_info"]["0"] + return env_info + + def start(self): + level = self.levels[self.current_level_idx] + print("CREATING LEVEL:", level["name"]) + self.current_running_env = self.create_env(level) + # if len(self.levels) > 1: + # next_level = self.levels[self.current_level_idx] + # print("CREATING NEXT LEVEL:", next_level["name"]) + # self.next_level_env = self.create_env(next_level) + + def next_level(self): + requests.post( + f"{game_server_url}/manage/stop_env/", + json={ + "manager_id": HARDCODED_MANAGER_ID, + "env_id": self.current_running_env["env_id"], + "reason": "Next level", + }, + ) + + self.current_level_idx += 1 + if not self.study_done: + level = self.levels[self.current_level_idx] + print("CREATING LEVEL:", level["name"]) + self.current_running_env = self.create_env(level) + for participant_id, player_id in self.player_ids.items(): + player_id = self.player_ids[participant_id] + self.participant_id_to_player_info[ + participant_id + ] = self.current_running_env["player_info"][player_id] + + for key in self.players_done: + self.players_done[key] = False + return False + else: + return True + + def add_participant(self, participant_id: str): + player_name = str(self.num_connected_players) + player_info = self.current_running_env["player_info"][player_name] + self.participant_id_to_player_info[participant_id] = player_info + self.player_ids[participant_id] = player_info["player_id"] + self.num_connected_players += 1 + return player_info + + def player_finished_level(self, participant_id): + self.players_done[participant_id] = True + level_done = all(self.players_done.values()) + if level_done: + self.next_level() + return {"study_finished": self.study_done} + + def get_connection(self, participant_id: str): + player_info = self.participant_id_to_player_info[participant_id] + return player_info + + +class StudyManager: + def __init__(self): + self.running_studies: list[StudyState] = [] -def main(host, port, game_server_url_, manager_id): + self.participant_id_to_study_map: dict[str, StudyState] = {} + self.running_envs: dict[str, Tuple[int, dict[str, PlayerInfo], list[str]]] = {} + self.current_free_envs = [] + + def create_study(self): + study = StudyState(ROOT_DIR / "game_content" / "study_config.yaml") + study.start() + self.running_studies.append(study) + + def add_participant(self, participant_id): + player_info = None + if all([s.is_full for s in self.running_studies]): + self.create_study() + + for study in self.running_studies: + if not study.is_full: + player_info = study.add_participant(participant_id) + self.participant_id_to_study_map[participant_id] = study + return player_info + + def player_finished_level(self, participant_id: str): + assigned_study = self.participant_id_to_study_map[participant_id] + return assigned_study.player_finished_level(participant_id) + + def get_participant_game_connection(self, participant_id: str): + assigned_study = self.participant_id_to_study_map[participant_id] + return assigned_study.participant_id_to_player_info[participant_id] + + +study_manager = StudyManager() + + +@app.post("/start_study/{participant_id}") +async def start_study(participant_id: str): + player_info = study_manager.add_participant(participant_id) + print() + return player_info + + +@app.post("/level_done/{participant_id}") +async def next_level(participant_id: str): + is_there_next_level = study_manager.player_finished_level(participant_id) + return is_there_next_level + + +@app.post("/get_game_connection/{participant_id}") +async def finished_game(participant_id: str): + return study_manager.get_participant_game_connection(participant_id) + + +@app.post("/finished_game/{participant_id}") +async def finished_game(participant_id: str): + print(f"{participant_id} finished game.") + return None + + +@app.post("/connect_to_tutorial/{participant_id}") +async def want_to_play_tutorial(participant_id: str): + environment_config_path = ( + ROOT_DIR / "game_content" / "tutorial" / "tutorial_env_config.yaml" + ) + layout_path = ROOT_DIR / "game_content" / "tutorial" / "tutorial.layout" + item_info_path = ROOT_DIR / "game_content" / "item_info.yaml" + with open(item_info_path, "r") as file: + item_info = file.read() + with open(layout_path, "r") as file: + layout = file.read() + with open(environment_config_path, "r") as file: + environment_config = file.read() + creation_json = CreateEnvironmentConfig( + manager_id=server_manager_id, + number_players=1, + environment_settings={"all_player_can_pause_game": False}, + item_info_config=item_info, + environment_config=environment_config, + layout_config=layout, + seed=1234567890, + ).model_dump(mode="json") + # todo async + print("CREATING TUTORIAL ENVIRONMENT") + env_info = requests.post( + game_server_url + "/manage/create_env/", json=creation_json + ) + + if env_info.status_code == 403: + raise ValueError(f"Forbidden Request: {env_info.json()['detail']}") + env_info = env_info.json() + running_tutorials[participant_id] = env_info + return env_info["player_info"]["0"] + + +@app.post("/disconnect_from_tutorial/{participant_id}") +async def want_to_play_tutorial(participant_id: str): + requests.post( + f"{game_server_url}/manage/stop_env/", + json={ + "manager_id": HARDCODED_MANAGER_ID, + "env_id": running_tutorials[participant_id]["env_id"], + "reason": "Finished tutorial", + }, + ) + + +def main(host, port, game_server_url_, manager_ids): global game_server_url, server_manager_id + + manager_ids = ["1234"] + game_server_url = "http://" + game_server_url_ - server_manager_id = manager_id[0] + server_manager_id = manager_ids[0] print(f"Use {server_manager_id=} for {game_server_url=}") loop = asyncio.new_event_loop() config = uvicorn.Config(app, host=host, port=port, loop=loop) @@ -132,5 +322,5 @@ if __name__ == "__main__": args.url, args.port, game_server_url_="localhost:8000", - manager_id=args.manager_ids, + manager_ids=args.manager_ids, ) diff --git a/overcooked_simulator/game_content/environment_config.yaml b/overcooked_simulator/game_content/environment_config.yaml index 00716059a6b64f9aba3a7ebb2fc00b541582d04f..11a443d00780a210bf368f4fcd00e8a66c3d8559 100644 --- a/overcooked_simulator/game_content/environment_config.yaml +++ b/overcooked_simulator/game_content/environment_config.yaml @@ -5,7 +5,7 @@ plates: # range of seconds until the dirty plate arrives. game: - time_limit_seconds: 300 + time_limit_seconds: 12 meals: all: true diff --git a/overcooked_simulator/game_content/environment_config_dark.yaml b/overcooked_simulator/game_content/environment_config_dark.yaml new file mode 100644 index 0000000000000000000000000000000000000000..b11ae4fdda0647e4cf772698dc961b0659e309d2 --- /dev/null +++ b/overcooked_simulator/game_content/environment_config_dark.yaml @@ -0,0 +1,157 @@ +plates: + clean_plates: 1 + dirty_plates: 2 + plate_delay: [ 5, 10 ] + # range of seconds until the dirty plate arrives. + +game: + time_limit_seconds: 20 + +meals: + all: true + # if all: false -> only orders for these meals are generated + # TODO: what if this list is empty? + list: + - TomatoSoup + - OnionSoup + - Salad + +layout_chars: + _: Free + hash: Counter # # + A: Agent + pipe: Extinguisher + P: PlateDispenser + C: CuttingBoard + X: Trashcan + $: ServingWindow + S: Sink + +: SinkAddon + at: Plate # @ just a clean plate on a counter + U: Pot # with Stove + Q: Pan # with Stove + O: Peel # with Oven + F: Basket # with DeepFryer + T: Tomato + N: Onion # oNioN + L: Lettuce + K: Potato # Kartoffel + I: Fish # fIIIsh + D: Dough + E: Cheese # chEEEse + G: Sausage # sausaGe + B: Bun + M: Meat + question: Counter # ? mushroom + ↓: Counter + ^: Counter + right: Counter + left: Counter + wave: Free # ~ Water + minus: Free # - Ice + dquote: Counter # " wall/truck + p: Counter # second plate return ?? + + +orders: + order_gen_class: !!python/name:overcooked_simulator.order.RandomOrderGeneration '' + # the class to that receives the kwargs. Should be a child class of OrderGeneration in order.py + order_gen_kwargs: + order_duration_random_func: + # how long should the orders be alive + # 'random' library call with getattr, kwargs are passed to the function + func: uniform + kwargs: + a: 40 + b: 60 + max_orders: 6 + # maximum number of active orders at the same time + num_start_meals: 2 + # number of orders generated at the start of the environment + sample_on_dur_random_func: + # 'random' library call with getattr, kwargs are passed to the function + func: uniform + kwargs: + a: 10 + b: 20 + sample_on_serving: false + # Sample the delay for the next order only after a meal was served. + score_calc_gen_func: !!python/name:overcooked_simulator.order.simple_score_calc_gen_func '' + score_calc_gen_kwargs: + # the kwargs for the score_calc_gen_func + other: 20 + scores: + Burger: 15 + OnionSoup: 10 + Salad: 5 + TomatoSoup: 10 + expired_penalty_func: !!python/name:overcooked_simulator.order.simple_expired_penalty '' + expired_penalty_kwargs: + default: -5 + serving_not_ordered_meals: !!python/name:overcooked_simulator.order.serving_not_ordered_meals_with_zero_score '' + # a func that calcs a store for not ordered but served meals. Input: meal + penalty_for_trash: !!python/name:overcooked_simulator.order.penalty_for_each_item '' + # a func that calcs the penalty for items that the player puts into the trashcan. + +player_config: + radius: 0.4 + player_speed_units_per_seconds: 6 + interaction_range: 1.6 + restricted_view: True + view_angle: 70 + view_range: 4.5 # in grid units, can be "null" + +effect_manager: + FireManager: + class: !!python/name:overcooked_simulator.effect_manager.FireEffectManager '' + kwargs: + spreading_duration: [ 5, 10 ] + fire_burns_ingredients_and_meals: true + + +extra_setup_functions: + # json_states: + # func: !!python/name:overcooked_simulator.recording.class_recording_with_hooks '' + # kwargs: + # hooks: [ json_state ] + # log_class: !!python/name:overcooked_simulator.recording.LogRecorder '' + # log_class_kwargs: + # log_path: USER_LOG_DIR/ENV_NAME/json_states.jsonl + actions: + func: !!python/name:overcooked_simulator.recording.class_recording_with_hooks '' + kwargs: + hooks: [ pre_perform_action ] + log_class: !!python/name:overcooked_simulator.recording.LogRecorder '' + log_class_kwargs: + log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl + random_env_events: + func: !!python/name:overcooked_simulator.recording.class_recording_with_hooks '' + kwargs: + hooks: [ order_duration_sample, plate_out_of_kitchen_time ] + log_class: !!python/name:overcooked_simulator.recording.LogRecorder '' + log_class_kwargs: + log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl + add_hook_ref: true + env_configs: + func: !!python/name:overcooked_simulator.recording.class_recording_with_hooks '' + kwargs: + hooks: [ env_initialized, item_info_config ] + log_class: !!python/name:overcooked_simulator.recording.LogRecorder '' + log_class_kwargs: + log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl + add_hook_ref: true +# info_msg: +# func: !!python/name:overcooked_simulator.recording.class_recording_with_hooks '' +# kwargs: +# hooks: [ cutting_board_100 ] +# log_class: !!python/name:overcooked_simulator.info_msg.InfoMsgManager '' +# log_class_kwargs: +# msg: Glückwunsch du hast was geschnitten! +# fire_msg: +# func: !!python/name:overcooked_simulator.recording.class_recording_with_hooks '' +# kwargs: +# hooks: [ new_fire ] +# log_class: !!python/name:overcooked_simulator.info_msg.InfoMsgManager '' +# log_class_kwargs: +# msg: Feuer, Feuer, Feuer +# level: Warning \ No newline at end of file diff --git a/overcooked_simulator/game_content/layouts/large_t.layout b/overcooked_simulator/game_content/layouts/large_t.layout index 304e6f7746f4e0c7510f69395b55c7f691de84f6..de56d63203bcd0740d8663c1a945025704afecf9 100644 --- a/overcooked_simulator/game_content/layouts/large_t.layout +++ b/overcooked_simulator/game_content/layouts/large_t.layout @@ -4,7 +4,7 @@ #____________________________________# #____________________________________# #____________________________________K -W____________________________________I +$____________________________________I #____________________________________# #____________________________________# #__A_____A___________________________D diff --git a/overcooked_simulator/game_content/layouts/godot_test_layout.layout b/overcooked_simulator/game_content/layouts/test_layouts/godot_test_layout.layout similarity index 100% rename from overcooked_simulator/game_content/layouts/godot_test_layout.layout rename to overcooked_simulator/game_content/layouts/test_layouts/godot_test_layout.layout diff --git a/overcooked_simulator/game_content/layouts/test_layouts/test1.layout b/overcooked_simulator/game_content/layouts/test_layouts/test1.layout new file mode 100644 index 0000000000000000000000000000000000000000..8ccd0a1152692fd1c9a0a911b0bb677bab98cb95 --- /dev/null +++ b/overcooked_simulator/game_content/layouts/test_layouts/test1.layout @@ -0,0 +1 @@ +____A___P \ No newline at end of file diff --git a/overcooked_simulator/game_content/layouts/test_layouts/test2.layout b/overcooked_simulator/game_content/layouts/test_layouts/test2.layout new file mode 100644 index 0000000000000000000000000000000000000000..8cfae98af3e7748e2df2dcdaae2ffffbe8c4a074 --- /dev/null +++ b/overcooked_simulator/game_content/layouts/test_layouts/test2.layout @@ -0,0 +1,9 @@ +_ +_ +_ +A +_ +_ +_ +_ +P \ No newline at end of file diff --git a/overcooked_simulator/game_content/layouts/test_layouts/test3.layout b/overcooked_simulator/game_content/layouts/test_layouts/test3.layout new file mode 100644 index 0000000000000000000000000000000000000000..8847c681b18500df23abe068dac76fb4dfbdd6d2 --- /dev/null +++ b/overcooked_simulator/game_content/layouts/test_layouts/test3.layout @@ -0,0 +1,4 @@ +___ +_A_ +___ +__P \ No newline at end of file diff --git a/overcooked_simulator/game_content/layouts/test_layouts/test4.layout b/overcooked_simulator/game_content/layouts/test_layouts/test4.layout new file mode 100644 index 0000000000000000000000000000000000000000..09d7551fa64358eb61a97d4c562617dca82b9655 --- /dev/null +++ b/overcooked_simulator/game_content/layouts/test_layouts/test4.layout @@ -0,0 +1,3 @@ +____ +_A__ +___P \ No newline at end of file diff --git a/overcooked_simulator/game_content/study_config.yaml b/overcooked_simulator/game_content/study_config.yaml new file mode 100644 index 0000000000000000000000000000000000000000..226b16638b0837b0c511fc80794969aa21ba5850 --- /dev/null +++ b/overcooked_simulator/game_content/study_config.yaml @@ -0,0 +1,23 @@ +levels: + - config_path: environment_config.yaml + layout_path: overcooked-1/1-1-far-apart.layout + item_info_path: item_info.yaml + name: "Level 1-1: Far Apart" + + - config_path: environment_config.yaml + layout_path: overcooked-1/1-4-bottleneck.layout + item_info_path: item_info.yaml + name: "Level 1-4: Bottleneck" + + # - config_path: environment_config.yaml + # layout_path: overcooked-1/1-5-circle.layout + # item_info_path: item_info.yaml + # name: "Level 1-5: Circle" + + - config_path: environment_config_dark.yaml + layout_path: overcooked-1/4-2-dark.layout + item_info_path: item_info.yaml + name: "Level 4-2: Dark" + + +num_players: 3 diff --git a/overcooked_simulator/game_content/tutorial/tutorial.layout b/overcooked_simulator/game_content/tutorial/tutorial.layout new file mode 100644 index 0000000000000000000000000000000000000000..8f4cf0f8ffa67a79836f4f0e69bbb0f2ec60410e --- /dev/null +++ b/overcooked_simulator/game_content/tutorial/tutorial.layout @@ -0,0 +1,5 @@ +#C## +#__X +T__# +L__$ +#P## diff --git a/overcooked_simulator/game_content/tutorial/tutorial_env_config.yaml b/overcooked_simulator/game_content/tutorial/tutorial_env_config.yaml new file mode 100644 index 0000000000000000000000000000000000000000..2d03eb20e13515ff609bdfdc7bc69e9c11363eaf --- /dev/null +++ b/overcooked_simulator/game_content/tutorial/tutorial_env_config.yaml @@ -0,0 +1,157 @@ +plates: + clean_plates: 2 + dirty_plates: 0 + plate_delay: [ 5, 10 ] + # range of seconds until the dirty plate arrives. + +game: + time_limit_seconds: 90000 + +meals: + all: true + # if all: false -> only orders for these meals are generated + # TODO: what if this list is empty? + list: + - TomatoSoup + - OnionSoup + - Salad + +layout_chars: + _: Free + hash: Counter # # + A: Agent + pipe: Extinguisher + P: PlateDispenser + C: CuttingBoard + X: Trashcan + $: ServingWindow + S: Sink + +: SinkAddon + at: Plate # @ just a clean plate on a counter + U: Pot # with Stove + Q: Pan # with Stove + O: Peel # with Oven + F: Basket # with DeepFryer + T: Tomato + N: Onion # oNioN + L: Lettuce + K: Potato # Kartoffel + I: Fish # fIIIsh + D: Dough + E: Cheese # chEEEse + G: Sausage # sausaGe + B: Bun + M: Meat + question: Counter # ? mushroom + ↓: Counter + ^: Counter + right: Counter + left: Counter + wave: Free # ~ Water + minus: Free # - Ice + dquote: Counter # " wall/truck + p: Counter # second plate return ?? + + +orders: + order_gen_class: !!python/name:overcooked_simulator.order.RandomOrderGeneration '' + # the class to that receives the kwargs. Should be a child class of OrderGeneration in order.py + order_gen_kwargs: + order_duration_random_func: + # how long should the orders be alive + # 'random' library call with getattr, kwargs are passed to the function + func: uniform + kwargs: + a: 40 + b: 60 + max_orders: 0 + # maximum number of active orders at the same time + num_start_meals: 2 + # number of orders generated at the start of the environment + sample_on_dur_random_func: + # 'random' library call with getattr, kwargs are passed to the function + func: uniform + kwargs: + a: 10 + b: 20 + sample_on_serving: false + # Sample the delay for the next order only after a meal was served. + score_calc_gen_func: !!python/name:overcooked_simulator.order.simple_score_calc_gen_func '' + score_calc_gen_kwargs: + # the kwargs for the score_calc_gen_func + other: 20 + scores: + Burger: 15 + OnionSoup: 10 + Salad: 5 + TomatoSoup: 10 + expired_penalty_func: !!python/name:overcooked_simulator.order.simple_expired_penalty '' + expired_penalty_kwargs: + default: -5 + serving_not_ordered_meals: !!python/name:overcooked_simulator.order.serving_not_ordered_meals_with_zero_score '' + # a func that calcs a store for not ordered but served meals. Input: meal + penalty_for_trash: !!python/name:overcooked_simulator.order.penalty_for_each_item '' + # a func that calcs the penalty for items that the player puts into the trashcan. + +player_config: + radius: 0.4 + player_speed_units_per_seconds: 6 + interaction_range: 1.6 + restricted_view: False + view_angle: 70 + view_range: 5.5 # in grid units, can be "null" + +effect_manager: + FireManager: + class: !!python/name:overcooked_simulator.effect_manager.FireEffectManager '' + kwargs: + spreading_duration: [ 5, 10 ] + fire_burns_ingredients_and_meals: true + + +extra_setup_functions: + # json_states: + # func: !!python/name:overcooked_simulator.recording.class_recording_with_hooks '' + # kwargs: + # hooks: [ json_state ] + # log_class: !!python/name:overcooked_simulator.recording.LogRecorder '' + # log_class_kwargs: + # log_path: USER_LOG_DIR/ENV_NAME/json_states.jsonl + actions: + func: !!python/name:overcooked_simulator.recording.class_recording_with_hooks '' + kwargs: + hooks: [ pre_perform_action ] + log_class: !!python/name:overcooked_simulator.recording.LogRecorder '' + log_class_kwargs: + log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl + random_env_events: + func: !!python/name:overcooked_simulator.recording.class_recording_with_hooks '' + kwargs: + hooks: [ order_duration_sample, plate_out_of_kitchen_time ] + log_class: !!python/name:overcooked_simulator.recording.LogRecorder '' + log_class_kwargs: + log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl + add_hook_ref: true + env_configs: + func: !!python/name:overcooked_simulator.recording.class_recording_with_hooks '' + kwargs: + hooks: [ env_initialized, item_info_config ] + log_class: !!python/name:overcooked_simulator.recording.LogRecorder '' + log_class_kwargs: + log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl + add_hook_ref: true +# info_msg: +# func: !!python/name:overcooked_simulator.recording.class_recording_with_hooks '' +# kwargs: +# hooks: [ cutting_board_100 ] +# log_class: !!python/name:overcooked_simulator.info_msg.InfoMsgManager '' +# log_class_kwargs: +# msg: Glückwunsch du hast was geschnitten! +# fire_msg: +# func: !!python/name:overcooked_simulator.recording.class_recording_with_hooks '' +# kwargs: +# hooks: [ new_fire ] +# log_class: !!python/name:overcooked_simulator.info_msg.InfoMsgManager '' +# log_class_kwargs: +# msg: Feuer, Feuer, Feuer +# level: Warning \ No newline at end of file diff --git a/overcooked_simulator/game_server.py b/overcooked_simulator/game_server.py index da019095c73fe56a3d58917e5b67db608cd3f2e7..995d579df9208abd26d2f76d18d4b418d59a3685 100644 --- a/overcooked_simulator/game_server.py +++ b/overcooked_simulator/game_server.py @@ -36,7 +36,6 @@ from overcooked_simulator.server_results import ( PlayerRequestResult, ) from overcooked_simulator.utils import ( - setup_logging, url_and_port_arguments, add_list_of_manager_ids_arguments, disable_websocket_logging_arguments, @@ -204,6 +203,7 @@ class EnvironmentHandler: env_id=config.env_id, player_id=player_id, ) + log.debug(f"Added player {player_id} to env {config.env_id}") return new_player_info def start_env(self, env_id: str): @@ -219,6 +219,7 @@ class EnvironmentHandler: self.envs[env_id].start_time = start_time self.envs[env_id].last_step_time = time.time_ns() self.envs[env_id].environment.reset_env_time() + self.envs[env_id].environment.all_players_ready = True def get_state( self, player_hash: str @@ -300,8 +301,11 @@ class EnvironmentHandler: if self.envs[env_id].status != EnvironmentStatus.STOPPED: self.envs[env_id].status = EnvironmentStatus.STOPPED self.envs[env_id].stop_reason = reason + log.debug(f"Stopped environment: id={env_id}, reason={reason}") return 0 + log.debug(f"Could not stop environment: id={env_id}, env is not running") return 2 + log.debug(f"Could not stop environment: id={env_id}, no env with this id") return 1 def set_player_ready(self, player_hash) -> bool: @@ -755,7 +759,9 @@ async def websocket_player_endpoint(websocket: WebSocket, client_id: str): def main( host: str, port: int, manager_ids: list[str], enable_websocket_logging: bool = False ): - setup_logging(enable_websocket_logging) + manager_ids = ["1234"] + + # setup_logging(enable_websocket_logging) loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) environment_handler.extend_allowed_manager(manager_ids) diff --git a/overcooked_simulator/gui_2d_vis/gui_theme.json b/overcooked_simulator/gui_2d_vis/gui_theme.json index 8e7e819a989131890f69df5df701b99f810ecb50..9f41fea590a7d36ecb6d2f3a175c46c5500a348d 100644 --- a/overcooked_simulator/gui_2d_vis/gui_theme.json +++ b/overcooked_simulator/gui_2d_vis/gui_theme.json @@ -157,5 +157,14 @@ "bold": 1, "colour": "#000000" } + }, + "#wait_players_label": { + "colours": { + "normal_text": "#ff0000" + }, + "font": { + "size": 45, + "bold": 1 + } } } \ No newline at end of file diff --git a/overcooked_simulator/gui_2d_vis/overcooked_gui.py b/overcooked_simulator/gui_2d_vis/overcooked_gui.py index ef3d93f75aa5ec48a18cbc6f46582320c104b3ee..2c8d00604ebab4e85be0350a6eb46680069558d5 100644 --- a/overcooked_simulator/gui_2d_vis/overcooked_gui.py +++ b/overcooked_simulator/gui_2d_vis/overcooked_gui.py @@ -2,11 +2,13 @@ import argparse import dataclasses import json import logging +import os import random +import signal +import subprocess import sys import uuid from enum import Enum -from pathlib import Path from subprocess import Popen import numpy as np @@ -31,15 +33,15 @@ from overcooked_simulator.utils import ( url_and_port_arguments, disable_websocket_logging_arguments, add_list_of_manager_ids_arguments, - setup_logging, ) -CONNECT_WITH_STUDY_SERVER = False - class MenuStates(Enum): Start = "Start" + ControllerTutorial = "ControllerTutorial" + PreGame = "PreGame" Game = "Game" + PostGame = "PostGame" End = "End" @@ -58,6 +60,7 @@ class PlayerKeySet: pickup_key: pygame.key, switch_key: pygame.key, players: list[int], + joystick: int, ): """Creates a player key set which contains information about which keyboard keys control the player. @@ -69,6 +72,7 @@ class PlayerKeySet: pickup_key: The key to pick items up or put them down. switch_key: The key for switching through controllable players. players: The player indices which this keyset can control. + joystick: number of joystick (later check if available) """ self.move_vectors: list[list[int]] = [[-1, 0], [1, 0], [0, -1], [0, 1]] self.key_to_movement: dict[pygame.key, list[int]] = { @@ -82,6 +86,7 @@ class PlayerKeySet: self.current_player: int = players[0] if players else 0 self.current_idx = 0 self.other_keyset: list[PlayerKeySet] = [] + self.joystick = joystick def set_controlled_players(self, controlled_players: list[int]) -> None: self.controlled_players = controlled_players @@ -106,17 +111,23 @@ class PyGameGUI: url: str, port: int, manager_ids: list[str], + CONNECT_WITH_STUDY_SERVER: bool, + USE_AAAMBOS_AGENT: bool, ): + self.CONNECT_WITH_STUDY_SERVER = CONNECT_WITH_STUDY_SERVER + self.USE_AAAMBOS_AGENT = USE_AAAMBOS_AGENT + pygame.init() pygame.display.set_icon( pygame.image.load(ROOT_DIR / "gui_2d_vis" / "images" / "fish3.png") ) + self.participant_id = uuid.uuid4().hex + self.game_screen: pygame.Surface = None self.FPS = 60 self.running = True - self.reset_gui_values() self.key_sets: list[PlayerKeySet] = [] self.websocket_url = f"ws://{url}:{port}/ws/player/" @@ -155,61 +166,26 @@ class PyGameGUI: self.sub_processes = [] - self.beeped_once = False - - def get_window_sizes_from_state(self, state: dict): - self.kitchen_width = state["kitchen"]["width"] - self.kitchen_height = state["kitchen"]["height"] - self.kitchen_aspect_ratio = self.kitchen_height / self.kitchen_width - game_width = self.visualization_config["GameWindow"]["min_width"] - ( - 2 * self.screen_margin - ) - game_height = self.visualization_config["GameWindow"]["min_height"] - ( - 2 * self.screen_margin + self.layout_file_paths = sorted( + (ROOT_DIR / "game_content" / "layouts").rglob("*.layout") ) + self.current_layout_idx = 0 - if self.kitchen_width > game_width: - self.game_height = game_width * self.kitchen_aspect_ratio - self.grid_size = game_width / self.kitchen_width - else: - self.game_width = game_height / self.kitchen_aspect_ratio - self.grid_size = game_width / self.kitchen_width - - self.window_width_windowed = self.min_width - self.window_height_windowed = self.min_height - - def recalc_game_size(self): - log.debug("Resizing game screen") - max_width = self.window_width - (2 * self.screen_margin) - max_height = self.window_height - (2 * self.screen_margin) - if max_width < max_height: - self.game_width = max_width - self.game_height = max_width * self.kitchen_aspect_ratio - self.grid_size = int(self.game_height / self.kitchen_height) - - else: - self.game_height = max_height - self.game_width = max_height / self.kitchen_aspect_ratio - self.grid_size = int(self.game_width / self.kitchen_width) - - self.game_width = max(self.game_width, 100) - self.game_height = max(self.game_height, 100) - self.grid_size = max(self.grid_size, 1) - - residual_x = self.game_width - (self.kitchen_width * self.grid_size) - residual_y = self.game_height - (self.kitchen_height * self.grid_size) - self.game_width -= residual_x - self.game_height -= residual_y + self.beeped_once = False - def setup_player_keys(self, n=1, disjunct=False): - if n: - players = list(range(self.number_humans_to_be_added)) + def setup_player_keys(self, number_players, number_key_sets=1, disjunct=False): + # 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. + if number_key_sets: + players = list(range(number_players)) key_set1 = PlayerKeySet( move_keys=[pygame.K_a, pygame.K_d, pygame.K_w, pygame.K_s], interact_key=pygame.K_f, pickup_key=pygame.K_e, switch_key=pygame.K_SPACE, players=players, + joystick=0, ) key_set2 = PlayerKeySet( move_keys=[pygame.K_LEFT, pygame.K_RIGHT, pygame.K_UP, pygame.K_DOWN], @@ -217,19 +193,20 @@ class PyGameGUI: pickup_key=pygame.K_o, switch_key=pygame.K_p, players=players, + joystick=1, ) key_sets = [key_set1, key_set2] if disjunct: key_set1.set_controlled_players(players[::2]) key_set2.set_controlled_players(players[1::2]) - elif n > 1: + elif number_key_sets > 1: key_set1.set_controlled_players(players) key_set2.set_controlled_players(players) key_set1.other_keyset = [key_set2] key_set2.other_keyset = [key_set1] key_set2.next_player() - return key_sets[:n] + return key_sets[:number_key_sets] else: return [] @@ -258,6 +235,47 @@ class PyGameGUI: ) self.send_action(action) + def handle_joy_stick_input(self, joysticks): + """Handles joystick inputs for movement every frame + Args: + joysticks: list of joysticks + """ + # Axis 0: joy stick left: -1 = left, ~0 = center, 1 = right + # Axis 1: joy stick left: -1 = up, ~0 = center, 1 = down + # see control stuff here (at the end of the page): https://www.pygame.org/docs/ref/joystick.html + for key_set in self.key_sets: + current_player_name = str(key_set.current_player) + # if a joystick is connected for current player + if key_set.joystick in joysticks: + # Usually axis run in pairs, up/down for one, and left/right for the other. Triggers count as axes. + # You may want to take into account some tolerance to handle jitter, and + # joystick drift may keep the joystick from centering at 0 or using the full range of position values. + tolerance_threshold = 0.2 + # axis 0 = joy stick left --> left & right + axis_left_right = joysticks[key_set.joystick].get_axis(0) + axis_up_down = joysticks[key_set.joystick].get_axis(1) + if ( + abs(axis_left_right) > tolerance_threshold + or abs(axis_up_down) > tolerance_threshold + ): + move_vec = np.zeros(2) + if abs(axis_left_right) > tolerance_threshold: + move_vec[0] += axis_left_right + # axis 1 = joy stick right --> up & down + if abs(axis_up_down) > tolerance_threshold: + move_vec[1] += axis_up_down + + # if np.linalg.norm(move_vec) != 0: + # move_vec = move_vec / np.linalg.norm(move_vec) + + action = Action( + current_player_name, + ActionType.MOVEMENT, + move_vec, + duration=self.time_delta, + ) + 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 @@ -270,7 +288,7 @@ class PyGameGUI: for key_set in self.key_sets: current_player_name = str(key_set.current_player) if event.key == key_set.pickup_key and event.type == pygame.KEYDOWN: - action = Action(current_player_name, ActionType.PUT, "pickup") + action = Action(self.player_id, ActionType.PUT, "pickup") self.send_action(action) if event.key == key_set.interact_key: @@ -281,159 +299,146 @@ class PyGameGUI: self.send_action(action) elif event.type == pygame.KEYUP: action = Action( - current_player_name, ActionType.INTERACT, InterActionData.STOP + self.player_id, ActionType.INTERACT, InterActionData.STOP ) self.send_action(action) - if event.key == key_set.switch_key and not CONNECT_WITH_STUDY_SERVER: + if event.key == key_set.switch_key and not self.CONNECT_WITH_STUDY_SERVER: if event.type == pygame.KEYDOWN: key_set.next_player() - 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") + def handle_joy_stick_event(self, event, joysticks): + """Handles joy stick events for the pickup and interaction keys. Pickup is a single action, + for interaction buttondown and buttonup is necessary, because the player has to be able to hold + the button down. - self.start_button = pygame_gui.elements.UIButton( - relative_rect=pygame.Rect( - (0, 0), (self.buttons_width, self.buttons_height) - ), - text="Start Game", - manager=self.manager, - anchors={"center": "center"}, - ) - self.start_button.can_hover() + Args: + event: Pygame event for extracting the button action. + joysticks: list of joysticks + """ + for key_set in self.key_sets: + current_player_name = str(key_set.current_player) + # if a joystick is connected for current player + if key_set.joystick in joysticks: + # pickup = Button A <-> 0 + if ( + joysticks[key_set.joystick].get_button(0) + and event.type == pygame.JOYBUTTONDOWN + ): + action = Action(current_player_name, ActionType.PUT, "pickup") + self.send_action(action) + + # interact = Button X <-> 2 + if ( + joysticks[key_set.joystick].get_button(2) + and event.type == pygame.JOYBUTTONDOWN + ): + action = Action( + current_player_name, ActionType.INTERACT, InterActionData.START + ) + self.send_action(action) + # stop interaction if last pressed button was X <-> 2 + if event.button == 2 and event.type == pygame.JOYBUTTONUP: + action = Action( + current_player_name, ActionType.INTERACT, InterActionData.STOP + ) + self.send_action(action) + # switch button Y <-> 3 + if ( + joysticks[key_set.joystick].get_button(3) + and not self.CONNECT_WITH_STUDY_SERVER + ): + if event.type == pygame.JOYBUTTONDOWN: + key_set.next_player() + + def set_window_size(self): + if self.fullscreen: + flags = pygame.FULLSCREEN + self.window_width = self.window_width_fullscreen + self.window_height = self.window_height_fullscreen + else: + flags = 0 + self.window_width = self.window_width_windowed + self.window_height = self.window_height_windowed - quit_rect = pygame.Rect( + self.main_window = pygame.display.set_mode( ( - 0, - 0, + self.window_width, + self.window_height, ), - (self.buttons_width, self.buttons_height), - ) - quit_rect.topright = (0, 0) - self.quit_button = pygame_gui.elements.UIButton( - relative_rect=quit_rect, - text="Quit Game", - manager=self.manager, - object_id="#quit_button", - anchors={"right": "right", "top": "top"}, + flags=flags, ) - self.quit_button.can_hover() - fullscreen_button_rect = pygame.Rect( - (0, 0), (self.buttons_width * 0.7, self.buttons_height) - ) - fullscreen_button_rect.topright = (-self.buttons_width, 0) - self.fullscreen_button = pygame_gui.elements.UIButton( - relative_rect=fullscreen_button_rect, - text="Fullscreen", - manager=self.manager, - object_id="#fullscreen_button", - anchors={"right": "right", "top": "top"}, - ) - self.fullscreen_button.can_hover() + def reset_window_size(self): + self.game_width = 0 + self.game_height = 0 + self.set_window_size() - reset_button_rect = pygame.Rect((0, 0), (self.screen_margin * 0.75, 50)) - reset_button_rect.topright = (0, 2 * self.buttons_height) - self.reset_button = pygame_gui.elements.UIButton( - relative_rect=reset_button_rect, - text="RESET", - manager=self.manager, - object_id="#reset_button", - anchors={"right": "right", "top": "top"}, - ) - self.reset_button.can_hover() + def set_game_size(self, max_width=None, max_height=None): + if max_width is None: + max_width = self.window_width - (2 * self.screen_margin) + if max_height is None: + max_height = self.window_height - (2 * self.screen_margin) - self.finished_button = pygame_gui.elements.UIButton( - relative_rect=pygame.Rect( - ( - (self.window_width - self.buttons_width), - (self.window_height - self.buttons_height), - ), - (self.buttons_width, self.buttons_height), - ), - text="Finish round", - manager=self.manager, - ) - self.finished_button.can_hover() + self.kitchen_aspect_ratio = self.kitchen_height / self.kitchen_width + if self.kitchen_width > self.kitchen_height: + self.game_width = max_width + self.game_height = self.game_width * self.kitchen_aspect_ratio - self.back_button = pygame_gui.elements.UIButton( - relative_rect=pygame.Rect( - ( - (0), - (self.window_height - self.buttons_height), - ), - (self.buttons_width, self.buttons_height), - ), - text="Back to menu", - manager=self.manager, - ) - self.back_button.can_hover() + if self.game_height > max_height: + self.game_height = max_height + self.game_width = self.game_height / self.kitchen_aspect_ratio + else: + self.game_height = max_height + self.game_width = self.game_height / self.kitchen_aspect_ratio - self.score_label = pygame_gui.elements.UILabel( - text=f"Score: _", - relative_rect=pygame.Rect( - ( - (0), - self.window_height - self.screen_margin, - ), - (self.screen_margin * 2, self.screen_margin), - ), - manager=self.manager, - object_id="#score_label", - ) + if self.game_width > max_width: + self.game_width = max_width + self.game_height = self.game_width * self.kitchen_aspect_ratio - self.layout_file_paths = { - str(p.name): p - for p in [ - Path(f) - for f in sorted( - (ROOT_DIR / "game_content" / "layouts").rglob("*.layout") - ) - ] - } + self.grid_size = int(self.game_width / self.kitchen_width) - assert len(self.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=list(self.layout_file_paths.keys()), - starting_option="basic.layout" - if "basic.layout" in self.layout_file_paths - else random.choice(list(self.layout_file_paths.keys())), + self.game_width = max(self.game_width, 100) + self.game_height = max(self.game_height, 100) + self.grid_size = max(self.grid_size, 1) + + residual_x = self.game_width - (self.kitchen_width * self.grid_size) + residual_y = self.game_height - (self.kitchen_height * self.grid_size) + self.game_width -= residual_x + self.game_height -= residual_y + + self.game_screen = pygame.Surface( + ( + self.game_width, + self.game_height, + ) ) - self.timer_label = pygame_gui.elements.UILabel( - text="GAMETIME", + + 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") + + ######################################################################## + # Start screen + ######################################################################## + self.start_button = pygame_gui.elements.UIButton( relative_rect=pygame.Rect( - (self.screen_margin, self.window_height - self.screen_margin), - (self.game_width, self.screen_margin), + (0, 0), (self.buttons_width, self.buttons_height) ), + text="Start Game", manager=self.manager, - object_id="#timer_label", - ) - - self.orders_label = pygame_gui.elements.UILabel( - text="Orders:", - relative_rect=pygame.Rect(0, 0, self.screen_margin, self.screen_margin), - manager=self.manager, - object_id="#orders_label", + anchors={"center": "center"}, ) - self.conclusion_label = pygame_gui.elements.UILabel( - text="Your final score was _", - relative_rect=pygame.Rect(0, 0, self.window_width, self.window_height), + rect = pygame.Rect((0, 0), (self.buttons_width, self.buttons_height)) + rect.topright = (0, 0) + self.quit_button = pygame_gui.elements.UIButton( + relative_rect=rect, + text="Quit Game", manager=self.manager, - object_id="#score_label", + object_id="#quit_button", + anchors={"right": "right", "top": "top"}, ) - ####################### - player_selection_rect = pygame.Rect( (0, 0), ( @@ -468,34 +473,6 @@ class PyGameGUI: anchors={"centerx": "centerx", "centery": "centery"}, object_id="#split_players_button", ) - if self.multiple_keysets: - self.split_players_button.show() - else: - self.split_players_button.hide() - - xbox_controller_button_rect = pygame.Rect((0, 0), (190, 50)) - xbox_controller_button_rect.right = 0 - self.xbox_controller_button = pygame_gui.elements.UIButton( - relative_rect=xbox_controller_button_rect, - manager=self.manager, - container=self.player_selection_container, - text="Controller?", - anchors={"right": "right", "centery": "centery"}, - object_id="#controller_button", - ) - - ######## - # - # panel = pygame_gui.elements.UIPanel( - # pygame.Rect((50, 50), (700, 500)), - # manager=manager, - # anchors={ - # "left": "left", - # "right": "right", - # "top": "top", - # "bottom": "bottom", - # }, - # ) players_container_rect = pygame.Rect( (0, 0), @@ -534,7 +511,7 @@ class PyGameGUI: manager=self.manager, object_id="#number_players_label", container=self.player_number_container, - text=f"Humans to be added: {self.number_humans_to_be_added}", + text=f"Humans to be added: -", anchors={"center": "center"}, ) @@ -544,7 +521,7 @@ class PyGameGUI: manager=self.manager, object_id="#number_bots_label", container=self.bot_number_container, - text=f"Bots to be added: {self.number_bots_to_be_added}", + text=f"Bots to be added: -", anchors={"center": "center"}, ) @@ -559,7 +536,6 @@ class PyGameGUI: container=self.player_number_container, anchors={"right": "right", "centery": "centery"}, ) - self.add_human_player_button.can_hover() remove_player_button_rect = pygame.Rect((0, 0), (size, size)) remove_player_button_rect.left = 0 @@ -571,7 +547,6 @@ class PyGameGUI: container=self.player_number_container, anchors={"left": "left", "centery": "centery"}, ) - self.remove_human_button.can_hover() add_bot_button_rect = pygame.Rect((0, 0), (size, size)) add_bot_button_rect.right = 0 @@ -583,7 +558,6 @@ class PyGameGUI: container=self.bot_number_container, anchors={"right": "right", "centery": "centery"}, ) - self.add_bot_button.can_hover() remove_bot_button_rect = pygame.Rect((0, 0), (size, size)) remove_bot_button_rect.left = 0 @@ -595,9 +569,370 @@ class PyGameGUI: container=self.bot_number_container, anchors={"left": "left", "centery": "centery"}, ) - self.remove_bot_button.can_hover() - def draw(self, state): + ######################################################################## + # Tutorial screen + ######################################################################## + + image = pygame.image.load( + ROOT_DIR / "gui_2d_vis" / "tutorial_files" / "tutorial.drawio.png" + ).convert_alpha() + image_rect = image.get_rect() + image_rect.topleft = (20, self.buttons_height) + self.tutorial_image = pygame_gui.elements.UIImage( + image_rect, + image, + manager=self.manager, + anchors={"top": "top", "left": "left"}, + ) + img_width = self.window_width * 0.8 + # img_width = img_height * (image_rect.width / image_rect.height) + img_height = img_width * (image_rect.height / image_rect.width) + new_dims = (img_width, img_height) + self.tutorial_image.set_dimensions(new_dims) + + button_rect = pygame.Rect((0, 0), (220, 80)) + button_rect.bottom = -20 + self.continue_button = pygame_gui.elements.UIButton( + relative_rect=button_rect, + text="Continue", + manager=self.manager, + anchors={"centerx": "centerx", "bottom": "bottom"}, + ) + + fullscreen_button_rect = pygame.Rect( + (0, 0), (self.buttons_width * 0.7, self.buttons_height) + ) + fullscreen_button_rect.topright = (-self.buttons_width, 0) + self.fullscreen_button = pygame_gui.elements.UIButton( + relative_rect=fullscreen_button_rect, + text="Fullscreen", + manager=self.manager, + object_id="#fullscreen_button", + anchors={"right": "right", "top": "top"}, + ) + + ######################################################################## + # PreGame screen + ######################################################################## + + image = pygame.image.load( + ROOT_DIR / "gui_2d_vis" / "tutorial_files" / "recipe_mock.png" + ).convert_alpha() + image_rect = image.get_rect() + image_rect.top = 50 + self.recipe_image = pygame_gui.elements.UIImage( + image_rect, + image, + manager=self.manager, + anchors={"centerx": "centerx", "top": "top"}, + ) + img_height = self.window_height * 0.7 + img_width = img_height * (image_rect.width / image_rect.height) + new_dims = (img_width, img_height) + self.recipe_image.set_dimensions(new_dims) + + self.level_name = pygame_gui.elements.UILabel( + text=f"Next level: {self.layout_file_paths[self.current_layout_idx].stem}", + relative_rect=pygame.Rect( + (0, 0), + (self.window_width * 0.7, self.window_height * 0.2), + ), + manager=self.manager, + object_id="#score_label", + anchors={"centerx": "centerx", "top": "top"}, + ) + + ######################################################################## + # Game screen + ######################################################################## + + self.finished_button = pygame_gui.elements.UIButton( + relative_rect=pygame.Rect( + ( + (self.window_width - self.buttons_width), + (self.window_height - self.buttons_height), + ), + (self.buttons_width, self.buttons_height), + ), + text="Finish round", + manager=self.manager, + ) + + self.orders_label = pygame_gui.elements.UILabel( + text="Orders:", + relative_rect=pygame.Rect(0, 0, self.screen_margin, self.screen_margin), + manager=self.manager, + object_id="#orders_label", + ) + + rect = pygame.Rect( + (0, 0), + (self.window_width * 0.2, self.buttons_height), + ) + rect.bottomleft = (0, 0) + self.score_label = pygame_gui.elements.UILabel( + text=f"Score not set", + relative_rect=rect, + manager=self.manager, + object_id="#score_label", + anchors={"bottom": "bottom", "left": "left"}, + ) + + rect = pygame.Rect( + (0, 0), + (self.window_width * 0.4, self.buttons_height), + ) + rect.bottom = 0 + self.timer_label = pygame_gui.elements.UILabel( + text="GAMETIME not set", + relative_rect=rect, + manager=self.manager, + object_id="#timer_label", + anchors={"bottom": "bottom", "centerx": "centerx"}, + ) + + rect = pygame.Rect( + (0, 0), + (self.window_width, self.screen_margin), + ) + rect.right = 20 + self.wait_players_label = pygame_gui.elements.UILabel( + text="WAITING FOR OTHER PLAYERS", + relative_rect=rect, + manager=self.manager, + object_id="#wait_players_label", + anchors={"centery": "centery", "right": "right"}, + ) + + ######################################################################## + # PostGame screen + ######################################################################## + + conclusion_rect = pygame.Rect(0, 0, self.window_width, self.window_height * 0.4) + conclusion_rect.top = 50 + self.conclusion_label = pygame_gui.elements.UILabel( + text="not set", + relative_rect=conclusion_rect, + manager=self.manager, + object_id="#score_label", + anchors={"centerx": "centerx", "top": "top"}, + ) + + next_game_button_rect = pygame.Rect((0, 0), (190, 50)) + next_game_button_rect.center = (self.buttons_width // 2, 200) + self.next_game_button = pygame_gui.elements.UIButton( + relative_rect=next_game_button_rect, + manager=self.manager, + text="Next game", + anchors={"centerx": "centerx", "centery": "centery"}, + object_id="#split_players_button", + ) + + retry_button_rect = pygame.Rect((0, 0), (190, 50)) + retry_button_rect.center = (self.buttons_width // 2 - 200, 200) + self.retry_button = pygame_gui.elements.UIButton( + relative_rect=retry_button_rect, + manager=self.manager, + text="Retry last game", + anchors={"center": "center"}, + object_id="#split_players_button", + ) + + finish_study_rect = pygame.Rect((0, 0), (190, 50)) + finish_study_rect.center = (self.buttons_width // 2 + 200, 200) + self.finish_study_button = pygame_gui.elements.UIButton( + relative_rect=finish_study_rect, + manager=self.manager, + text="Finish study", + anchors={"center": "center"}, + object_id="#split_players_button", + ) + + ######################################################################## + # End screen + ######################################################################## + + conclusion_rect = pygame.Rect( + 0, 0, self.window_width * 0.6, self.window_height * 0.4 + ) + self.thank_you_label = pygame_gui.elements.UILabel( + text="Thank you!", + relative_rect=conclusion_rect, + manager=self.manager, + object_id="#score_label", + anchors={"center": "center"}, + ) + + ######################################################################## + + self.start_screen_elements = [ + self.start_button, + self.quit_button, + self.fullscreen_button, + self.player_selection_container, + ] + + self.tutorial_screen_elements = [ + self.tutorial_image, + self.continue_button, + ] + + self.pregame_screen_elements = [ + self.recipe_image, + self.level_name, + self.continue_button, + ] + + self.game_screen_elements = [ + self.orders_label, + self.score_label, + self.timer_label, + self.wait_players_label, + ] + + self.postgame_screen_elements = [ + self.conclusion_label, + self.next_game_button, + ] + + self.end_screen_elements = [ + self.fullscreen_button, + self.quit_button, + self.thank_you_label, + ] + + self.rest = [ + self.fullscreen_button, + self.quit_button, + self.retry_button, + self.finish_study_button, + self.finished_button, + ] + + def show_screen_elements(self, elements: list): + for element in ( + self.start_screen_elements + + self.tutorial_screen_elements + + self.pregame_screen_elements + + self.game_screen_elements + + self.postgame_screen_elements + + self.end_screen_elements + + self.rest + ): + element.hide() + for element in elements: + element.show() + + def update_screen_elements(self): + match self.menu_state: + case MenuStates.Start: + self.show_screen_elements(self.start_screen_elements) + + if self.CONNECT_WITH_STUDY_SERVER: + self.player_selection_container.hide() + + self.update_selection_elements() + case MenuStates.ControllerTutorial: + self.show_screen_elements(self.tutorial_screen_elements) + self.setup_game(tutorial=True) + self.set_game_size( + max_height=self.window_height * 0.3, + max_width=self.window_width * 0.3, + ) + self.set_window_size() + self.game_center = ( + self.window_width - self.game_width / 2 - 20, + self.window_height - self.game_height / 2 - 20, + ) + case MenuStates.PreGame: + self.show_screen_elements(self.pregame_screen_elements) + case MenuStates.Game: + self.show_screen_elements(self.game_screen_elements) + case MenuStates.PostGame: + self.show_screen_elements(self.postgame_screen_elements) + if self.study_finished: + self.next_game_button.hide() + self.finish_study_button.show() + else: + self.next_game_button.show() + self.finish_study_button.hide() + case MenuStates.End: + self.show_screen_elements(self.end_screen_elements) + + def draw_main_window(self): + self.main_window.fill( + colors[self.visualization_config["GameWindow"]["background_color"]] + ) + + match self.menu_state: + case MenuStates.Start: + pass + case MenuStates.ControllerTutorial: + self.draw_tutorial_screen_frame() + case MenuStates.Game: + self.draw_game_screen_frame() + case MenuStates.PostGame: + self.update_conclusion_label(self.last_state) + + self.manager.draw_ui(self.main_window) + self.manager.update(self.time_delta) + pygame.display.flip() + + def draw_tutorial_screen_frame(self): + self.handle_keys() + self.handle_joy_stick_input(joysticks=self.joysticks) + + state = self.request_state() + self.vis.draw_gamescreen( + self.game_screen, + state, + self.grid_size, + [k.current_player for k in self.key_sets], + ) + + game_screen_rect = self.game_screen.get_rect() + game_screen_rect.center = self.game_center + self.main_window.blit(self.game_screen, game_screen_rect) + + def draw_game_screen_frame(self): + self.last_state = self.request_state() + + self.handle_keys() + self.handle_joy_stick_input(joysticks=self.joysticks) + + if not self.beeped_once and self.last_state["play_beep"]: + self.beeped_once = True + self.play_bell_sound() + + if self.last_state["ended"]: + self.menu_state = MenuStates.PostGame + self.finished_button_press() + self.disconnect_websockets() + + if self.CONNECT_WITH_STUDY_SERVER: + self.send_level_done() + + self.update_screen_elements() + + else: + self.draw_game(self.last_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) + + if not self.last_state["all_players_ready"]: + self.wait_players_label.show() + else: + self.wait_players_label.hide() + + def draw_game(self, state): """Main visualization function. Args: state: The game state returned by the environment.""" @@ -608,9 +943,6 @@ class PyGameGUI: [k.current_player for k in self.key_sets], ) - # self.manager.draw_ui(self.main_window) - self.update_remaining_time(state["remaining_time"]) - self.vis.draw_orders( screen=self.main_window, state=state, @@ -636,6 +968,7 @@ class PyGameGUI: ) self.update_score_label(state) + self.update_remaining_time(state["remaining_time"]) if state["info_msg"]: for idx, msg in enumerate(reversed(state["info_msg"])): @@ -655,80 +988,6 @@ class PyGameGUI: ), ) - def set_window_size(self): - self.game_screen = pygame.Surface( - ( - self.game_width, - self.game_height, - ) - ) - - if self.fullscreen: - flags = pygame.FULLSCREEN - self.window_width = self.window_width_fullscreen - self.window_height = self.window_height_fullscreen - else: - flags = 0 - self.window_width = self.window_width_windowed - self.window_height = self.window_height_windowed - - self.main_window = pygame.display.set_mode( - ( - self.window_width, - self.window_height, - ), - flags=flags, - display=0, - ) - - def reset_window_size(self): - self.game_width = 0 - self.game_height = 0 - self.set_window_size() - self.init_ui_elements() - - 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.reset_button.hide() - self.score_label.hide() - self.finished_button.hide() - self.layout_selection.show() - self.timer_label.hide() - self.orders_label.hide() - self.conclusion_label.hide() - - self.player_selection_container.show() - case MenuStates.Game: - self.start_button.hide() - self.back_button.hide() - self.score_label.show() - self.reset_button.show() - self.score_label.show() - self.finished_button.show() - self.layout_selection.hide() - self.timer_label.show() - self.orders_label.show() - self.conclusion_label.hide() - - self.player_selection_container.hide() - - case MenuStates.End: - self.start_button.hide() - self.back_button.show() - self.score_label.hide() - self.reset_button.hide() - self.finished_button.hide() - self.layout_selection.hide() - self.timer_label.hide() - self.orders_label.hide() - self.conclusion_label.show() - - self.player_selection_container.hide() - def update_score_label(self, state): score = state["score"] self.score_label.set_text(f"Score {score}") @@ -743,52 +1002,109 @@ class PyGameGUI: display_time = f"{minutes}:{'%02d' % seconds}" self.timer_label.set_text(f"Time remaining: {display_time}") - def setup_environment(self): - if CONNECT_WITH_STUDY_SERVER: + def create_env_on_game_server(self, tutorial): + if tutorial: + layout_path = ROOT_DIR / "game_content" / "tutorial" / "tutorial.layout" + environment_config_path = ( + ROOT_DIR / "game_content" / "tutorial" / "tutorial_env_config.yaml" + ) + else: + environment_config_path = ( + ROOT_DIR / "game_content" / "environment_config.yaml" + ) + layout_path = self.layout_file_paths[self.current_layout_idx] + + item_info_path = ROOT_DIR / "game_content" / "item_info.yaml" + with open(item_info_path, "r") as file: + item_info = file.read() + with open(layout_path, "r") as file: + layout = file.read() + with open(environment_config_path, "r") as file: + environment_config = file.read() + + num_players = 1 if tutorial else self.number_players + seed = 161616161616 + creation_json = CreateEnvironmentConfig( + manager_id=self.manager_id, + number_players=num_players, + environment_settings={"all_player_can_pause_game": False}, + item_info_config=item_info, + environment_config=environment_config, + layout_config=layout, + seed=seed, + ).model_dump(mode="json") + + # print(CreateEnvironmentConfig.model_validate_json(json_data=creation_json)) + env_info = requests.post( + f"{self.request_url}/manage/create_env/", + json=creation_json, + ) + if env_info.status_code == 403: + raise ValueError(f"Forbidden Request: {env_info.json()['detail']}") + env_info = env_info.json() + assert isinstance(env_info, dict), "Env info must be a dictionary" + self.current_env_id = env_info["env_id"] + self.player_info = env_info["player_info"] + if tutorial: + self.player_id = str(list(self.player_info.keys())[0]) + + def get_game_connection(self): + if self.menu_state == MenuStates.ControllerTutorial: self.player_info = requests.post( - f"http://localhost:8080/connect_to_game/{uuid.uuid4().hex}" + f"http://localhost:8080/connect_to_tutorial/{self.participant_id}" ).json() self.key_sets[0].current_player = int(self.player_info["player_id"]) + self.player_id = self.player_info["player_id"] self.player_info = {self.player_info["player_id"]: self.player_info} else: - environment_config_path = ( - ROOT_DIR / "game_content" / "environment_config.yaml" + self.player_info = requests.post( + f"http://localhost:8080/get_game_connection/{self.participant_id}" + ).json() + self.key_sets[0].current_player = int(self.player_info["player_id"]) + self.player_id = self.player_info["player_id"] + self.player_info = {self.player_info["player_id"]: self.player_info} + + def create_and_connect_bot(self, player_id, player_info): + player_hash = player_info["player_hash"] + print( + f'--general_plus="agent_websocket:{self.websocket_url + player_info["client_id"]};player_hash:{player_hash};agent_id:{player_id}"' + ) + if self.USE_AAAMBOS_AGENT: + sub = Popen( + " ".join( + [ + "exec", + "aaambos", + "run", + "--arch_config", + str(ROOT_DIR / "game_content" / "agents" / "arch_config.yml"), + "--run_config", + str(ROOT_DIR / "game_content" / "agents" / "run_config.yml"), + f'--general_plus="agent_websocket:{self.websocket_url + player_info["client_id"]};player_hash:{player_hash};agent_id:{player_id}"', + f"--instance={player_hash}", + ] + ), + shell=True, ) - layout_path = self.layout_file_paths[self.layout_selection.selected_option] - item_info_path = ROOT_DIR / "game_content" / "item_info.yaml" - with open(item_info_path, "r") as file: - item_info = file.read() - with open(layout_path, "r") as file: - layout = file.read() - with open(environment_config_path, "r") as file: - environment_config = file.read() - - seed = 161616161616 - creation_json = CreateEnvironmentConfig( - manager_id=self.manager_id, - number_players=self.number_players, - environment_settings={"all_player_can_pause_game": False}, - item_info_config=item_info, - environment_config=environment_config, - layout_config=layout, - seed=seed, - ).model_dump(mode="json") - - # print(CreateEnvironmentConfig.model_validate_json(json_data=creation_json)) - env_info = requests.post( - f"{self.request_url}/manage/create_env/", - json=creation_json, + else: + sub = Popen( + " ".join( + [ + "python", + str(ROOT_DIR / "game_content" / "agents" / "random_agent.py"), + f'--uri {self.websocket_url + player_info["client_id"]}', + f"--player_hash {player_hash}", + f"--player_id {player_id}", + ] + ), + shell=True, ) - if env_info.status_code == 403: - raise ValueError(f"Forbidden Request: {env_info.json()['detail']}") - env_info = env_info.json() - assert isinstance(env_info, dict), "Env info must be a dictionary" - self.current_env_id = env_info["env_id"] - self.player_info = env_info["player_info"] - - state = None + self.sub_processes.append(sub) + + def connect_websockets(self): for p, (player_id, player_info) in enumerate(self.player_info.items()): if p < self.number_humans_to_be_added: + # add player websockets websocket = connect(self.websocket_url + player_info["client_id"]) websocket.send( json.dumps( @@ -799,147 +1115,95 @@ class PyGameGUI: json.loads(websocket.recv())["status"] == 200 ), "not accepted player" self.websockets[player_id] = websocket + else: - player_hash = player_info["player_hash"] - print( - f'--general_plus="agent_websocket:{self.websocket_url + player_info["client_id"]};player_hash:{player_hash};agent_id:{player_id}"' - ) - # sub = Popen( - # " ".join( - # [ - # "exec", - # "aaambos", - # "run", - # "--arch_config", - # str( - # ROOT_DIR / "game_content" / "agents" / "arch_config.yml" - # ), - # "--run_config", - # str( - # ROOT_DIR / "game_content" / "agents" / "run_config.yml" - # ), - # f'--general_plus="agent_websocket:{self.websocket_url + player_info["client_id"]};player_hash:{player_hash};agent_id:{player_id}"', - # f"--instance={player_hash}", - # ] - # ), - # shell=True, - # ) - sub = Popen( - " ".join( - [ - "python", - str( - ROOT_DIR / "game_content" / "agents" / "random_agent.py" - ), - f'--uri {self.websocket_url + player_info["client_id"]}', - f"--player_hash {player_hash}", - f"--player_id {player_id}", - ] - ), - shell=True, - ) - self.sub_processes.append(sub) + # create bots and add bot websockets + self.create_and_connect_bot(player_id, player_info) - if p + 1 == self.number_humans_to_be_added: + if p == 0: + # set initial player_id for requesting the state self.state_player_id = player_id - websocket.send( - json.dumps( - {"type": "get_state", "player_hash": player_info["player_hash"]} - ) - ) - state = json.loads(websocket.recv()) - - if not self.number_humans_to_be_added: - player_id = "0" - player_info = self.player_info[player_id] - websocket = connect(self.websocket_url + player_info["client_id"]) - websocket.send( - json.dumps({"type": "ready", "player_hash": player_info["player_hash"]}) - ) - assert json.loads(websocket.recv())["status"] == 200, "not accepted player" - self.websockets[player_id] = websocket - self.state_player_id = player_id - websocket.send( - json.dumps( - {"type": "get_state", "player_hash": player_info["player_hash"]} - ) - ) - state = json.loads(websocket.recv()) - - self.get_window_sizes_from_state(state) + # websocket.send( + # json.dumps( + # {"type": "get_state", "player_hash": player_info["player_hash"]} + # ) + # ) + # state = json.loads(websocket.recv()) - def start_button_press(self): - self.menu_state = MenuStates.Game + # return state - self.number_players = ( - self.number_humans_to_be_added + self.number_bots_to_be_added - ) - self.vis.create_player_colors(self.number_players) - - if self.split_players: - assert ( - self.number_humans_to_be_added > 1 - ), "Not enough players for key configuration." - num_key_set = 2 if self.multiple_keysets else 1 - self.key_sets = self.setup_player_keys( - min(self.number_humans_to_be_added, num_key_set), self.split_players - ) + def setup_game(self, tutorial=False): + if tutorial: + self.key_sets = self.setup_player_keys(1, 1, False) + self.vis.create_player_colors(1) + else: + self.number_players = ( + self.number_humans_to_be_added + self.number_bots_to_be_added + ) - self.setup_environment() + if self.split_players: + assert ( + self.number_humans_to_be_added > 1 + ), "Not enough players for key configuration." + num_key_set = 2 if self.multiple_keysets else 1 + self.key_sets = self.setup_player_keys( + self.number_humans_to_be_added, + min(self.number_humans_to_be_added, num_key_set), + self.split_players, + ) - self.recalc_game_size() - self.set_window_size() - self.init_ui_elements() - self.beeped_once = False - log.debug("Pressed start button") + if self.CONNECT_WITH_STUDY_SERVER: + self.get_game_connection() + else: + self.create_env_on_game_server(tutorial) - def back_button_press(self): - self.menu_state = MenuStates.Start - self.reset_window_size() + self.connect_websockets() - self.update_selection_elements() + state = self.request_state() - self.beeped_once = False - log.debug("Pressed back button") + self.vis.create_player_colors(len(state["players"])) - def quit_button_press(self): - self.running = False - self.menu_state = MenuStates.Start - log.debug("Pressed quit button") + self.kitchen_width = state["kitchen"]["width"] + self.kitchen_height = state["kitchen"]["height"] - def reset_button_press(self): - # self.reset_gui_values() - if not CONNECT_WITH_STUDY_SERVER: + def stop_game(self, reason: str) -> None: + log.debug(f"Stopping game: {reason}") + if not self.CONNECT_WITH_STUDY_SERVER: requests.post( - f"{self.request_url}/manage/stop_env", + f"{self.request_url}/manage/stop_env/", json={ "manager_id": self.manager_id, "env_id": self.current_env_id, - "reason": "reset button pressed", + "reason": reason, }, ) - self.beeped_once = False - # self.websocket.send(json.dumps("reset_game")) - # answer = self.websocket.recv() log.debug("Pressed reset button") + + def send_tutorial_finished(self): + requests.post( + f"http://localhost:8080/disconnect_from_tutorial/{self.participant_id}", + ) def finished_button_press(self): - if not CONNECT_WITH_STUDY_SERVER: - requests.post( - f"{self.request_url}/manage/stop_env/", - json={ - "manager_id": self.manager_id, - "env_id": self.current_env_id, - "reason": "finish button pressed", - }, - ) - self.menu_state = MenuStates.End + if not self.CONNECT_WITH_STUDY_SERVER: + self.stop_game("finished_button_pressed") + self.menu_state = MenuStates.PostGame self.reset_window_size() log.debug("Pressed finished button") + self.update_screen_elements() + + def fullscreen_button_press(self): + self.fullscreen = not self.fullscreen + self.set_window_size() + self.init_ui_elements() + self.set_game_size() def reset_gui_values(self): self.currently_controlled_player_idx = 0 self.number_humans_to_be_added = 1 self.number_bots_to_be_added = 0 + self.number_players = ( + self.number_humans_to_be_added + self.number_bots_to_be_added + ) self.split_players = False self.multiple_keysets = False self.player_minimum = 1 @@ -950,13 +1214,17 @@ class PyGameGUI: self.number_humans_to_be_added = self.player_minimum else: self.remove_human_button.enable() + self.number_humans_to_be_added = max( self.player_minimum, self.number_humans_to_be_added ) + self.number_players = ( + self.number_humans_to_be_added + self.number_bots_to_be_added + ) + text = "WASD+ARROW" if self.multiple_keysets else "WASD" self.multiple_keysets_button.set_text(text) - # self.split_players_button self.added_players_label.set_text( f"Humans to be added: {self.number_humans_to_be_added}" ) @@ -971,6 +1239,11 @@ class PyGameGUI: else: self.split_players_button.hide() + if self.number_players == 0: + self.start_button.disable() + else: + self.start_button.enable() + def send_action(self, action: Action): """Sends an action to the game environment. @@ -982,7 +1255,7 @@ class PyGameGUI: float(action.action_data[0]), float(action.action_data[1]), ] - self.websockets[action.player].send( + self.websockets[self.player_id].send( json.dumps( { "type": "action", @@ -993,16 +1266,16 @@ class PyGameGUI: } ) ) - self.websockets[action.player].recv() + self.websockets[self.player_id].recv() def request_state(self): self.websockets[self.state_player_id].send( json.dumps( { "type": "get_state", - "player_hash": self.player_info[ - str(self.key_sets[0].current_player) - ]["player_hash"], + "player_hash": self.player_info[self.state_player_id][ + "player_hash" + ], } ) ) @@ -1012,12 +1285,15 @@ class PyGameGUI: def disconnect_websockets(self): for sub in self.sub_processes: try: - sub.kill() - # pgrp = os.getpgid(sub.pid) - # os.killpg(pgrp, signal.SIGINT) - # subprocess.run( - # "kill $(ps aux | grep 'aaambos' | awk '{print $2}')", shell=True - # ) + if self.USE_AAAMBOS_AGENT: + pgrp = os.getpgid(sub.pid) + os.killpg(pgrp, signal.SIGINT) + subprocess.run( + "kill $(ps aux | grep 'aaambos' | awk '{print $2}')", shell=True + ) + else: + sub.kill() + except ProcessLookupError: pass @@ -1033,6 +1309,136 @@ class PyGameGUI: mixer.music.play() log.log(logging.INFO, "Started game, played bell sound") + def start_study(self): + self.player_info = requests.post( + f"http://localhost:8080/start_study/{self.participant_id}" + ).json() + self.study_finished = False + + def send_level_done(self): + answer = requests.post( + f"http://localhost:8080/level_done/{self.participant_id}" + ).json() + self.study_finished = answer["study_finished"] + print("\nSTUDY DONE:", self.study_finished, "\n") + + def manage_button_event(self, event): + if event.ui_element == self.quit_button: + self.running = False + self.disconnect_websockets() + self.stop_game("Quit button") + self.menu_state = MenuStates.Start + log.debug("Pressed quit button") + return + + elif event.ui_element == self.fullscreen_button: + self.fullscreen_button_press() + log.debug("Pressed fullscreen button") + return + + # Filter by shown screen page + match self.menu_state: + ############################################ + case MenuStates.Start: + match event.ui_element: + case self.start_button: + if not ( + self.number_humans_to_be_added + + self.number_bots_to_be_added + ): + pass + else: + self.menu_state = MenuStates.ControllerTutorial + + case self.add_human_player_button: + self.number_humans_to_be_added += 1 + case self.add_bot_button: + self.number_bots_to_be_added += 1 + + case self.remove_human_button: + self.number_humans_to_be_added = max( + self.player_minimum, self.number_humans_to_be_added - 1 + ) + case self.remove_bot_button: + self.number_bots_to_be_added = max( + 0, self.number_bots_to_be_added - 1 + ) + case self.multiple_keysets_button: + self.multiple_keysets = not self.multiple_keysets + self.split_players = False + case self.split_players_button: + self.split_players = not self.split_players + if self.split_players: + self.player_minimum = 2 + else: + self.player_minimum = 0 + + ############################################ + + case MenuStates.ControllerTutorial: + match event.ui_element: + case self.continue_button: + self.menu_state = MenuStates.PreGame + + if self.CONNECT_WITH_STUDY_SERVER: + self.send_tutorial_finished() + self.start_study() + else: + self.stop_game("tutorial_finished") + self.disconnect_websockets() + + ############################################ + + case MenuStates.PreGame: + match event.ui_element: + case self.continue_button: + self.setup_game() + self.set_game_size() + self.menu_state = MenuStates.Game + + ############################################ + + case MenuStates.Game: + match event.ui_element: + case self.finished_button: + self.menu_state = MenuStates.PostGame + self.disconnect_websockets() + self.finished_button_press() + self.handle_joy_stick_input(joysticks=self.joysticks) + + if self.CONNECT_WITH_STUDY_SERVER: + self.send_level_done() + + ############################################ + + case MenuStates.PostGame: + match event.ui_element: + case self.retry_button: + if not self.CONNECT_WITH_STUDY_SERVER: + self.stop_game("Retry button") + self.menu_state = MenuStates.PreGame + + case self.next_game_button: + if not self.CONNECT_WITH_STUDY_SERVER: + self.current_layout_idx += 1 + if self.current_layout_idx == len(self.layout_file_paths): + self.current_layout_idx = 0 + else: + log.debug( + f"LEVEL: {self.layout_file_paths[self.current_layout_idx]}" + ) + self.menu_state = MenuStates.PreGame + + case self.finish_study_button: + self.menu_state = MenuStates.End + + ############################################ + + case MenuStates.End: + match event.ui_element: + case other: + pass + 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") @@ -1044,164 +1450,98 @@ class PyGameGUI: clock = pygame.time.Clock() self.reset_window_size() - self.init_ui_elements() - self.manage_button_visibility() - - self.update_selection_elements() + self.reset_gui_values() + self.update_screen_elements() # Game loop self.running = True + # This dict can be left as-is, since pygame will generate a + # pygame.JOYDEVICEADDED event for every joystick connected + # at the start of the program. + self.joysticks = {} + while self.running: try: self.time_delta = clock.tick(self.FPS) / 1000 - # print(clock.get_time()) + # PROCESSING EVENTS for event in pygame.event.get(): if event.type == pygame.QUIT: + self.disconnect_websockets() self.running = False - # elif event.type == pygame.VIDEORESIZE: - # # scrsize = event.size - # self.window_width_windowed = event.w - # self.window_height_windowed = event.h - # self.recalc_game_size() - # self.set_window_size() - # self.init_ui_elements() - # self.manage_button_visibility() - - if event.type == pygame_gui.UI_BUTTON_PRESSED: - match event.ui_element: - case self.start_button: - if not ( - self.number_humans_to_be_added - + self.number_bots_to_be_added - ): - continue - self.start_button_press() - - case self.back_button: - self.back_button_press() - self.disconnect_websockets() - - case self.finished_button: - self.finished_button_press() - self.disconnect_websockets() - - case self.quit_button: - self.quit_button_press() - self.disconnect_websockets() - - case self.reset_button: - self.reset_button_press() - self.disconnect_websockets() - self.start_button_press() - - case self.add_human_player_button: - self.number_humans_to_be_added += 1 - case self.remove_human_button: - self.number_humans_to_be_added = max( - 0, self.number_humans_to_be_added - 1 - ) - case self.add_bot_button: - self.number_bots_to_be_added += 1 - case self.remove_bot_button: - self.number_bots_to_be_added = max( - 0, self.number_bots_to_be_added - 1 - ) - case self.multiple_keysets_button: - self.multiple_keysets = not self.multiple_keysets - self.split_players = False - case self.split_players_button: - self.split_players = not self.split_players - if self.split_players: - self.player_minimum = 2 - else: - self.player_minimum = 1 - - case self.xbox_controller_button: - print("xbox_controller_button pressed.") - - case self.fullscreen_button: - self.fullscreen = not self.fullscreen - if self.fullscreen: - self.window_width = self.window_width_fullscreen - self.window_height = self.window_height_fullscreen - else: - self.window_width = self.window_width_windowed - self.window_height = self.window_height_windowed - self.recalc_game_size() - self.set_window_size() - self.init_ui_elements() - - self.update_selection_elements() - - self.manage_button_visibility() - + # connect joystick if ( - event.type in [pygame.KEYDOWN, pygame.KEYUP] - and self.menu_state == MenuStates.Game + pygame.joystick.get_count() > 0 + and event.type == pygame.JOYDEVICEADDED ): - pass - self.handle_key_event(event) + # This event will be generated when the program starts for every + # joystick, filling up the list without needing to create them manually. + joy = pygame.joystick.Joystick(event.device_index) + self.joysticks[joy.get_instance_id()] = joy + print(f"Joystick {joy.get_instance_id()} connected") + + # disconnect joystick + if event.type == pygame.JOYDEVICEREMOVED: + del self.joysticks[event.instance_id] + print(f"Joystick {event.instance_id} disconnected") + print("Number of joysticks:", pygame.joystick.get_count()) - self.manager.process_events(event) - - # drawing: - self.main_window.fill( - colors[self.visualization_config["GameWindow"]["background_color"]] - ) - self.manager.draw_ui(self.main_window) - - match self.menu_state: - case MenuStates.Start: - pass - - case MenuStates.Game: - state = self.request_state() - - if not self.beeped_once and state["play_beep"]: - self.beeped_once = True - self.play_bell_sound() - - self.handle_keys() - - if state["ended"]: - self.finished_button_press() - self.disconnect_websockets() - self.manage_button_visibility() - else: - self.draw(state) - - game_screen_rect = self.game_screen.get_rect() - - game_screen_rect.center = [ - self.window_width // 2, - self.window_height // 2, - ] + if event.type == pygame_gui.UI_BUTTON_PRESSED: + self.manage_button_event(event) + self.update_screen_elements() + + if event.type in [ + pygame.KEYDOWN, + pygame.KEYUP, + ] and self.menu_state in [ + MenuStates.Game, + MenuStates.ControllerTutorial, + ]: + self.handle_key_event(event) - self.main_window.blit(self.game_screen, game_screen_rect) + if event.type in [ + pygame.JOYBUTTONDOWN, + pygame.JOYBUTTONUP, + ] and self.menu_state in [ + MenuStates.Game, + MenuStates.ControllerTutorial, + ]: + self.handle_joy_stick_event(event, joysticks=self.joysticks) - case MenuStates.End: - self.update_conclusion_label(state) + self.manager.process_events(event) - self.manager.update(self.time_delta) - pygame.display.flip() + # DRAWING + self.draw_main_window() except (KeyboardInterrupt, SystemExit): self.running = False + self.disconnect_websockets() + self.stop_game("Program exited.") self.disconnect_websockets() + self.stop_game("Program exited") pygame.quit() sys.exit() -def main(url: str, port: int, manager_ids: list[str]): - setup_logging() +def main( + url: str, + port: int, + manager_ids: list[str], + CONNECT_WITH_STUDY_SERVER=False, + USE_AAAMBOS_AGENT=False, +): + manager_ids = ["1234"] + + # setup_logging() gui = PyGameGUI( url=url, port=port, manager_ids=manager_ids, + CONNECT_WITH_STUDY_SERVER=CONNECT_WITH_STUDY_SERVER, + USE_AAAMBOS_AGENT=CONNECT_WITH_STUDY_SERVER, ) gui.start_pygame() diff --git a/overcooked_simulator/gui_2d_vis/tutorial.png b/overcooked_simulator/gui_2d_vis/tutorial.png new file mode 100644 index 0000000000000000000000000000000000000000..151f8e85b9a76c30df20d334be4deffd8d4e3d6a Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/tutorial.png differ diff --git a/overcooked_simulator/gui_2d_vis/tutorial_files/recipe_mock.png b/overcooked_simulator/gui_2d_vis/tutorial_files/recipe_mock.png new file mode 100644 index 0000000000000000000000000000000000000000..4ac719852dfd2470712a56ee249f86baa7a891d6 Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/tutorial_files/recipe_mock.png differ diff --git a/overcooked_simulator/gui_2d_vis/tutorial_files/tutorial.drawio.png b/overcooked_simulator/gui_2d_vis/tutorial_files/tutorial.drawio.png new file mode 100644 index 0000000000000000000000000000000000000000..8824cce8164089cc251217e21263c75fcd552254 Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/tutorial_files/tutorial.drawio.png differ diff --git a/overcooked_simulator/gui_2d_vis/visualization.yaml b/overcooked_simulator/gui_2d_vis/visualization.yaml index a0d7989c1c1bf9c2a96fcc28e1baf09bcd96e55e..7c664c1c372923ebec5bffb14a21a9656813389b 100644 --- a/overcooked_simulator/gui_2d_vis/visualization.yaml +++ b/overcooked_simulator/gui_2d_vis/visualization.yaml @@ -109,7 +109,7 @@ ServingWindow: - type: image path: images/bell_gold.png size: 0.5 - center_offset: [ -0.4, 0.1 ] + center_offset: [ -0.2, -0.05 ] rotate_image: False Stove: diff --git a/overcooked_simulator/overcooked_environment.py b/overcooked_simulator/overcooked_environment.py index f4b511604740e19206f5f339c31fe7b76218c194..3e1759b8d26030a112da2f31906a63c02dd4ef65 100644 --- a/overcooked_simulator/overcooked_environment.py +++ b/overcooked_simulator/overcooked_environment.py @@ -56,8 +56,6 @@ from overcooked_simulator.utils import create_init_env_time, get_closest log = logging.getLogger(__name__) -FOG_OF_WAR = True - PREVENT_SQUEEZING_INTO_OTHER_PLAYERS = False @@ -296,6 +294,8 @@ class Environment: env_start_time_worldtime=datetime.now(), ) + self.all_players_ready = False + def overwrite_counters(self, counters): self.counters = counters self.counter_positions = np.array([c.pos for c in self.counters]) @@ -431,6 +431,8 @@ class Environment: grid = [] + max_width = 0 + lines = list(filter(lambda l: l != "", lines)) for line in lines: line = line.replace(" ", "") @@ -438,6 +440,7 @@ class Environment: break current_x: float = starting_at grid_line = [] + for character in line: # character = character.capitalize() pos = np.array([current_x, current_y]) @@ -460,10 +463,15 @@ class Environment: current_x += 1 + if len(line) >= max_width: + max_width = len(line) + grid.append(grid_line) current_y += 1 - self.kitchen_width: float = len(lines[0]) + starting_at + grid = [line + ([0] * (max_width - len(line))) for line in grid] + + self.kitchen_width: float = max_width + starting_at self.kitchen_height = current_y self.determine_counter_orientations( @@ -823,6 +831,7 @@ class Environment: "kitchen": {"width": self.kitchen_width, "height": self.kitchen_height}, "score": self.order_and_score.score, "orders": self.order_and_score.order_state(), + "all_players_ready": self.all_players_ready, "ended": self.game_ended, "env_time": self.env_time.isoformat(), "remaining_time": max(