Skip to content
GitLab
Explore
Sign in
Register
Primary navigation
Search or go to…
Project
Cooperative Cuisine Environment
Manage
Activity
Members
Labels
Plan
Issues
Issue boards
Milestones
Wiki
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Snippets
Build
Pipelines
Jobs
Pipeline schedules
Artifacts
Deploy
Releases
Package registry
Container Registry
Model registry
Operate
Environments
Terraform modules
Monitor
Incidents
Service Desk
Analyze
Value stream analytics
Contributor analytics
CI/CD analytics
Repository analytics
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
Community forum
Contribute to GitLab
Provide feedback
Terms and privacy
Keyboard shortcuts
?
Snippets
Groups
Projects
Admin message
Looking for advice? Join the
Matrix channel for GitLab users in Bielefeld
!
Show more breadcrumbs
Social Cognitive Systems
CoCoSy
Cooperative Cuisine Environment
Commits
18ce03fa
Commit
18ce03fa
authored
1 year ago
by
Fabian Heinrich
Browse files
Options
Downloads
Patches
Plain Diff
Docstrings
parent
1b72bbdf
No related branches found
No related tags found
1 merge request
!72
Resolve "Too large number of selected players does not break the gui and environment"
Pipeline
#47838
passed
1 year ago
Stage: test
Changes
1
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
1 changed file
cooperative_cuisine/study_server.py
+188
-59
188 additions, 59 deletions
cooperative_cuisine/study_server.py
with
188 additions
and
59 deletions
cooperative_cuisine/study_server.py
+
188
−
59
View file @
18ce03fa
...
...
@@ -32,7 +32,7 @@ from pydantic import BaseModel
from
cooperative_cuisine
import
ROOT_DIR
from
cooperative_cuisine.environment
import
EnvironmentConfig
from
cooperative_cuisine.game_server
import
CreateEnvironmentConfig
,
EnvironmentData
from
cooperative_cuisine.server_results
import
PlayerInfo
from
cooperative_cuisine.server_results
import
PlayerInfo
,
CreateEnvResult
from
cooperative_cuisine.utils
import
(
url_and_port_arguments
,
add_list_of_manager_ids_arguments
,
...
...
@@ -62,7 +62,6 @@ class LevelConfig(BaseModel):
class
LevelInfo
(
BaseModel
):
name
:
str
last_level
:
bool
recipes
:
list
[
str
]
recipe_graphs
:
list
[
dict
]
...
...
@@ -72,8 +71,8 @@ class StudyConfig(BaseModel):
num_bots
:
int
class
Study
State
:
def
__init__
(
self
,
study_config_path
:
str
|
Path
,
game_url
,
game_port
):
class
Study
:
def
__init__
(
self
,
study_config_path
:
str
|
Path
,
game_url
:
str
,
game_port
:
int
):
with
open
(
study_config_path
,
"
r
"
)
as
file
:
env_config_f
=
file
.
read
()
...
...
@@ -85,55 +84,70 @@ class StudyState:
"""
List of level configs for each of the levels which the study runs through.
"""
self
.
current_level_idx
:
int
=
0
"""
Counter of which level is currently run in the config.
"""
self
.
participant_id_to_player_info
=
{}
self
.
participant_id_to_player_info
:
dict
[
str
,
PlayerInfo
]
=
{}
"""
A dictionary which maps participants to player infos.
"""
self
.
num_connected_players
:
int
=
0
"""
Number of currently connected players.
"""
self
.
current_running_env
:
EnvironmentData
|
None
=
None
self
.
current_running_env
:
CreateEnvResult
|
None
=
None
"""
Information about the current running environment.
"""
self
.
p
layers_done
=
{}
self
.
p
articipants_done
:
dict
[
str
,
bool
]
=
{}
"""
A dictionary which saves which player has sent ready.
"""
self
.
current_config
:
dict
|
None
=
None
"""
Save current environment config
"""
self
.
use_aaambos_agent
=
False
self
.
websocket_url
=
f
"
ws://
{
game_url
}
:
{
game_port
}
/ws/player/
"
self
.
sub_processes
=
[]
self
.
current_item_info
=
None
self
.
current_config
=
None
self
.
use_aaambos_agent
:
bool
=
False
"""
Use aaambos-agents or simple python scripts.
"""
self
.
bot_websocket_url
:
str
=
f
"
ws://
{
game_url
}
:
{
game_port
}
/ws/player/
"
"""
The websocket url for the bots to use.
"""
self
.
sub_processes
:
list
[
Popen
]
=
[]
"""
Save subprocesses of the bots to be able to kill them afterwards.
"""
@property
def
study_done
(
self
):
def
study_done
(
self
)
->
bool
:
return
self
.
current_level_idx
>=
len
(
self
.
levels
)
@property
def
last_level
(
self
):
def
last_level
(
self
)
->
bool
:
return
self
.
current_level_idx
>=
len
(
self
.
levels
)
-
1
@property
def
is_full
(
self
):
def
is_full
(
self
)
->
bool
:
return
(
len
(
self
.
participant_id_to_player_info
)
==
self
.
study_config
[
"
num_players
"
]
)
def
can_add_participants
(
self
,
num_participants
:
int
)
->
bool
:
"""
Checks whether the number of participants fit in this study.
Args:
num_participants: Number of participants wished to be added.
Returns: True of the participants fit in this study, False if not.
"""
filled
=
(
self
.
num_connected_players
+
num_participants
<=
self
.
study_config
[
"
num_players
"
]
)
return
filled
and
not
self
.
is_full
def
create_env
(
self
,
level
):
def
create_env
(
self
,
level
:
LevelConfig
)
->
EnvironmentData
:
"""
Creates/starts an environment on the game server,
given the configuration file paths specified in the level.
Args:
level: LevelConfig which contains the paths to the env config, layout and item info files.
Returns: EnvironmentData which contains information about the newly created environment.
Raises: ValueError if the gameserver returned a conflict, HTTPError with 500 if the game server crashes.
"""
item_info_path
=
expand_path
(
level
[
"
item_info_path
"
])
layout_path
=
expand_path
(
level
[
"
layout_path
"
])
config_path
=
expand_path
(
level
[
"
config_path
"
])
with
open
(
item_info_path
,
"
r
"
)
as
file
:
item_info
=
file
.
read
()
self
.
current_item_info
:
EnvironmentConfig
=
yaml
.
load
(
item_info
,
Loader
=
yaml
.
Loader
)
with
open
(
layout_path
,
"
r
"
)
as
file
:
layout
=
file
.
read
()
with
open
(
config_path
,
"
r
"
)
as
file
:
...
...
@@ -173,10 +187,13 @@ class StudyState:
return
env_info
def
start_level
(
self
):
level
=
self
.
levels
[
self
.
current
_
level
_idx
]
self
.
current_running_env
=
self
.
create_env
(
level
)
"""
Starts an environment based on the
current
level
index.
"""
self
.
current_running_env
=
self
.
create_env
(
self
.
levels
[
self
.
current_level_idx
]
)
def
next_level
(
self
):
"""
Stops the last environment, starts the next one and
remaps the participants to the new player infos.
"""
requests
.
post
(
f
"
{
study_manager
.
game_server_url
}
/manage/stop_env/
"
,
json
=
{
...
...
@@ -199,10 +216,16 @@ class StudyState:
}
self
.
participant_id_to_player_info
[
participant_id
]
=
new_player_info
for
key
in
self
.
p
layer
s_done
:
self
.
p
layer
s_done
[
key
]
=
False
for
key
in
self
.
p
articipant
s_done
:
self
.
p
articipant
s_done
[
key
]
=
False
def
add_participant
(
self
,
participant_id
:
str
,
number_players
:
int
):
"""
Adds a participant to the study, one participant can control multiple players.
Args:
participant_id: The participant id for which to register the participant.
number_players: The number of players which the participant controls.
"""
player_names
=
[
str
(
self
.
num_connected_players
+
i
)
for
i
in
range
(
number_players
)
]
...
...
@@ -213,35 +236,51 @@ class StudyState:
self
.
participant_id_to_player_info
[
participant_id
]
=
player_info
self
.
num_connected_players
+=
number_players
def
player_finished_level
(
self
,
participant_id
):
self
.
players_done
[
participant_id
]
=
True
if
all
(
self
.
players_done
.
values
()):
def
participant_finished_level
(
self
,
participant_id
:
str
):
"""
Signals the server if a player has finished a level.
If all participants finished the level, the next level is started.
"""
self
.
participants_done
[
participant_id
]
=
True
if
all
(
self
.
participants_done
.
values
()):
self
.
next_level
()
def
get_connection
(
self
,
participant_id
:
str
)
->
Tuple
[
PlayerInfo
|
None
,
LevelInfo
|
None
]:
"""
Get the assigned connections to the game server for a participant.
Args:
participant_id: The participant id which requests the connections.
Returns: The player info for the game server connections, level name and
information if the level is the last one and which recipes are possible in the level.
Raises: HTTPException(409) if the player is not found in the dictionary keys which saves the connections.
"""
if
participant_id
in
self
.
participant_id_to_player_info
.
keys
():
player_info
=
self
.
participant_id_to_player_info
[
participant_id
]
current_level
=
self
.
levels
[
self
.
current_level_idx
]
if
self
.
current_config
[
"
meals
"
][
"
all
"
]:
recipes
=
[
"
all
"
]
else
:
recipes
=
self
.
current_config
[
"
meals
"
][
"
list
"
]
level_info
=
LevelInfo
(
name
=
current_level
[
"
name
"
],
last_level
=
self
.
last_level
,
recipes
=
recipes
,
recipe_graphs
=
self
.
current_running_env
[
"
recipe_graphs
"
],
)
return
player_info
,
level_info
else
:
return
None
,
None
return
player_info
,
level_info
raise
HTTPException
(
status_code
=
409
,
detail
=
f
"
Participant not registered in this study.
"
,
)
def
create_and_connect_bot
(
self
,
player_id
:
str
,
player_info
:
PlayerInfo
):
"""
Creates and connects a bot to the current environment.
def
create_and_connect_bot
(
self
,
player_id
,
player_info
):
Args:
player_id: player id of the player the bot controls.
player_info: Connection info for the bot.
"""
player_hash
=
player_info
[
"
player_hash
"
]
ws_address
=
self
.
bot_websocket_url
+
player_info
[
"
client_id
"
]
print
(
f
'
--general_plus=
"
agent_websocket:
{
self
.
websocket_url
+
player_info
[
"
client_id
"
]
}
;player_hash:
{
player_hash
}
;agent_id:
{
player_id
}
"'
f
'
--general_plus=
"
agent_websocket:
{
ws_address
}
;player_hash:
{
player_hash
}
;agent_id:
{
player_id
}
"'
)
if
self
.
use_aaambos_agent
:
sub
=
Popen
(
...
...
@@ -254,7 +293,7 @@ class StudyState:
str
(
ROOT_DIR
/
"
configs
"
/
"
agents
"
/
"
arch_config.yml
"
),
"
--run_config
"
,
str
(
ROOT_DIR
/
"
configs
"
/
"
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
'
--general_plus=
"
agent_websocket:
{
ws_address
}
;player_hash:
{
player_hash
}
;agent_id:
{
player_id
}
"'
,
f
"
--instance=
{
player_hash
}
"
,
]
),
...
...
@@ -266,7 +305,7 @@ class StudyState:
[
"
python
"
,
str
(
ROOT_DIR
/
"
configs
"
/
"
agents
"
/
"
random_agent.py
"
),
f
'
--uri
{
self
.
websocket_url
+
player_info
[
"
client_id
"
]
}
'
,
f
'
--uri
{
self
.
bot_
websocket_url
+
player_info
[
"
client_id
"
]
}
'
,
f
"
--player_hash
{
player_hash
}
"
,
f
"
--player_id
{
player_id
}
"
,
]
...
...
@@ -276,6 +315,7 @@ class StudyState:
self
.
sub_processes
.
append
(
sub
)
def
kill_bots
(
self
):
"""
Terminates the subprocesses of the bots.
"""
for
sub
in
self
.
sub_processes
:
try
:
if
self
.
use_aaambos_agent
:
...
...
@@ -297,26 +337,34 @@ class StudyState:
class
StudyManager
:
def
__init__
(
self
):
self
.
game_host
:
str
|
None
=
None
self
.
game_port
:
str
|
None
=
None
self
.
game_server_url
:
str
|
None
=
None
self
.
server_manager_id
:
str
|
None
=
None
self
.
running_studies
:
list
[
StudyState
]
=
[]
"""
Class which manages different studies, their creation and connecting participants to them.
"""
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
__init__
(
self
):
self
.
game_host
:
str
"""
Host address of the game server where the studies are running their environments.
"""
self
.
game_port
:
int
"""
Port of the game server where the studies are running their environments.
"""
self
.
game_server_url
:
str
"""
Combined URL of the game server where the studies are running their environments.
"""
self
.
server_manager_id
:
str
"""
Manager id of this manager which will be registered in the game server.
"""
self
.
running_studies
:
list
[
Study
]
=
[]
"""
List of currently running studies.
"""
self
.
participant_id_to_study_map
:
dict
[
str
,
Study
]
=
{}
"""
Dict which maps participants to studies.
"""
self
.
running_tutorials
:
dict
[
str
,
Tuple
[
int
,
dict
[
str
,
PlayerInfo
],
list
[
str
]]
]
=
{}
"""
Dict which saves currently running tutorial envs, as these do not need advanced player management.
"""
self
.
study_config_path
=
ROOT_DIR
/
"
configs
"
/
"
study
"
/
"
study_config.yml
"
"""
Path to the configuration file for the studies.
"""
def
create_study
(
self
):
study
=
StudyState
(
"""
Creates a study with the path of the config files and the connection to the game server.
"""
study
=
Study
(
self
.
study_config_path
,
self
.
game_host
,
self
.
game_port
,
...
...
@@ -325,6 +373,15 @@ class StudyManager:
self
.
running_studies
.
append
(
study
)
def
add_participant
(
self
,
participant_id
:
str
,
number_players
:
int
):
"""
Adds participants to a study. Creates a new study if all other
studies have not enough free player slots
Args:
participant_id: ID of the participant which wants to connect to a study.
number_players: The number of player the participant wants to connect.
Raises: HTTPException(409) if the participants requests more players than can fit in a study.
"""
if
not
self
.
running_studies
or
all
(
[
not
s
.
can_add_participants
(
number_players
)
for
s
in
self
.
running_studies
]
):
...
...
@@ -335,36 +392,70 @@ class StudyManager:
study
.
add_participant
(
participant_id
,
number_players
)
self
.
participant_id_to_study_map
[
participant_id
]
=
study
return
raise
HTTPException
(
status_code
=
409
,
detail
=
"
Could not add
participant
(s)
.
"
)
raise
HTTPException
(
status_code
=
409
,
detail
=
"
Too many
participant
s to add
.
"
)
def
player_finished_level
(
self
,
participant_id
:
str
):
"""
A participant signals the study manager that they finished a level.
Args:
participant_id: ID of the participant.
Raises: HTTPException(409) if this participant is not registered in any study.
"""
if
participant_id
in
self
.
participant_id_to_study_map
.
keys
():
assigned_study
=
self
.
participant_id_to_study_map
[
participant_id
]
assigned_study
.
p
layer
_finished_level
(
participant_id
)
assigned_study
.
p
articipant
_finished_level
(
participant_id
)
else
:
raise
HTTPException
(
status_code
=
409
,
detail
=
"
Participant not in any study.
"
)
def
get_participant_game_connection
(
self
,
participant_id
:
str
)
->
Tuple
[
PlayerInfo
,
LevelInfo
]:
"""
Get the assigned connections to the game server for a participant.
Args:
participant_id: ID of the participant.
Returns: The player info for the game server connections, level name and
information if the level is the last one and which recipes are possible in the level.
Raises: HTTPException(409) if the player not registered in any study.
"""
if
participant_id
in
self
.
participant_id_to_study_map
.
keys
():
assigned_study
=
self
.
participant_id_to_study_map
[
participant_id
]
player_info
,
level_info
=
assigned_study
.
get_connection
(
participant_id
)
return
player_info
,
level_info
else
:
raise
HTTPException
(
status_code
=
409
,
detail
=
"
Participant not in this study.
"
)
raise
HTTPException
(
status_code
=
409
,
detail
=
"
Participant not in any study.
"
)
def
set_game_server_url
(
self
,
game_host
:
str
,
game_port
:
str
):
def
set_game_server_url
(
self
,
game_host
:
str
,
game_port
:
int
):
"""
Set the game server host address, port and combined url. These values are set this way because
the fastapi requests act on top level of the python script.
Args:
game_host: The game server host address.
game_port: The game server port.
"""
self
.
game_host
=
game_host
self
.
game_port
=
game_port
self
.
game_server_url
=
f
"
http://
{
self
.
game_host
}
:
{
self
.
game_port
}
"
def
set_manager_id
(
self
,
manager_id
:
str
):
"""
Set the manager id of the study server. This value is set this way because
the fastapi requests act on top level of the python script.
Args:
manager_id: Manager ID for this study manager so that it matches in the game server.
"""
self
.
server_manager_id
=
manager_id
def
set_study_config
(
self
,
study_config_path
:
str
):
"""
Set the study config path of the study server. This value is set this way because
the fastapi requests act on top level of the python script.
Args:
study_config_path: Path to the study config file for the studies.
"""
# TODO validate study_config?
self
.
study_config_path
=
study_config_path
...
...
@@ -374,12 +465,25 @@ study_manager = StudyManager()
@app.post
(
"
/start_study/{participant_id}/{number_players}
"
)
async
def
start_study
(
participant_id
:
str
,
number_players
:
int
):
"""
Request to start a study.
Args:
participant_id: ID of the requesting participant.
number_players: Number of player the participant wants to add to a study.
"""
log
.
debug
(
f
"
ADDING PLAYERS:
{
number_players
}
"
)
study_manager
.
add_participant
(
participant_id
,
number_players
)
@app.post
(
"
/level_done/{participant_id}
"
)
async
def
level_done
(
participant_id
:
str
):
"""
Request to signal that a participant has finished a level.
For synchronizing level endings and starting a new level.
Args:
participant_id: ID of the requesting participant.
"""
study_manager
.
player_finished_level
(
participant_id
)
...
...
@@ -387,6 +491,14 @@ async def level_done(participant_id: str):
async
def
get_game_connection
(
participant_id
:
str
,
)
->
dict
[
str
,
dict
[
str
,
PlayerInfo
]
|
LevelInfo
]:
"""
Request to get the connection to the game server of a participant.
Args:
participant_id: ID of the requesting participant.
Returns: A dict containing the game server connection information and information about the current level.
"""
player_info
,
level_info
=
study_manager
.
get_participant_game_connection
(
participant_id
)
...
...
@@ -395,6 +507,15 @@ async def get_game_connection(
@app.post
(
"
/connect_to_tutorial/{participant_id}
"
)
async
def
connect_to_tutorial
(
participant_id
:
str
)
->
JSONResponse
:
"""
Request of a participant to start a tutorial env and connect to it.
Args:
participant_id: ID of the requesting participant.
Returns: Player info which contains game server connection information.
Raises:
HTTPException(403) if the game server returns 403
HTTPException(500) if the game server returns 500
"""
environment_config_path
=
ROOT_DIR
/
"
configs
"
/
"
tutorial_env_config.yaml
"
layout_path
=
ROOT_DIR
/
"
configs
"
/
"
layouts
"
/
"
tutorial.layout
"
item_info_path
=
ROOT_DIR
/
"
configs
"
/
"
item_info.yaml
"
...
...
@@ -432,12 +553,19 @@ async def connect_to_tutorial(participant_id: str) -> JSONResponse:
case
500
:
raise
HTTPException
(
status_code
=
500
,
detail
=
f
"
Game server crashed
.
"
,
detail
=
f
"
Game server crashed
:
{
env_info
.
json
()[
'
detail
'
]
}
"
,
)
@app.post
(
"
/disconnect_from_tutorial/{participant_id}
"
)
async
def
disconnect_from_tutorial
(
participant_id
:
str
):
"""
A participant disconnects from a tutorial environment, which is then stopped on the game server.
Args:
participant_id: The participant which disconnects from the tutorial.
Raises: HTTPException(503) if the game server returns some error.
"""
answer
=
requests
.
post
(
f
"
{
study_manager
.
game_server_url
}
/manage/stop_env/
"
,
json
=
{
...
...
@@ -448,7 +576,7 @@ async def disconnect_from_tutorial(participant_id: str):
)
if
answer
.
status_code
!=
200
:
raise
HTTPException
(
status_code
=
4
03
,
detail
=
"
Could not disconnect from tutorial
"
status_code
=
5
03
,
detail
=
"
Could not disconnect from tutorial
"
)
...
...
@@ -470,7 +598,8 @@ if __name__ == "__main__":
parser
=
argparse
.
ArgumentParser
(
prog
=
"
Cooperative Cuisine 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
"
,
epilog
=
"
For further information,
"
"
see https://scs.pages.ub.uni-bielefeld.de/cocosy/overcooked-simulator/overcooked_simulator.html
"
,
)
url_and_port_arguments
(
parser
=
parser
,
...
...
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment