Skip to content
Snippets Groups Projects
Commit 5cfe68e3 authored by Fabian Heinrich's avatar Fabian Heinrich
Browse files

Implemented basic study flow in with the study server. Added Controls tutorial.

- Can be used via main with flag
parent 3dc3e293
No related branches found
No related tags found
1 merge request!62Resolve "Game Flow"
Pipeline #46873 passed
Showing
with 795 additions and 283 deletions
......@@ -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,92 @@ 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():
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():
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()
......
......@@ -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,
)
......@@ -5,7 +5,7 @@ plates:
# range of seconds until the dirty plate arrives.
game:
time_limit_seconds: 100
time_limit_seconds: 20
meals:
all: true
......
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
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
......@@ -64,7 +64,7 @@ orders:
kwargs:
a: 40
b: 60
max_orders: 6
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
......
......@@ -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
......@@ -296,8 +297,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:
......@@ -751,7 +755,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)
......
......@@ -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
This diff is collapsed.
overcooked_simulator/gui_2d_vis/tutorial_files/tutorial.drawio.png

3.33 MiB

......@@ -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])
......@@ -469,11 +469,7 @@ class Environment:
grid.append(grid_line)
current_y += 1
for line in grid:
print(line)
grid = [line + ([0] * (max_width - len(line))) for line in grid]
for line in grid:
print(line)
self.kitchen_width: float = max_width + starting_at
self.kitchen_height = current_y
......@@ -826,6 +822,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(
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment