Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • scs/cocosy/cooperative-cuisine
1 result
Show changes
Commits on Source (25)
Showing
with 299 additions and 84 deletions
......@@ -16,12 +16,13 @@
- Send full websocket url in player_info.
- ">90"% code coverage in tests
- i18n for the gui
- Controller hotplugging
- Controller hot-plugging
- Hook when returning items to dispensers
- Displaying image of served meals on game conclusion screen
- Pathfinding in random agent
- Level layouts from 2d-grid-overcooked-literature
- Caching of graph recipe layouts
- Score label changes color when score changes
### Changed
......@@ -38,6 +39,7 @@
- Better drawing of orders, now in a pygame_gui UIImage
- Buttons for setting player controls in the GUI disappear depending on number of players
- Icon for serving window, now a star
- Additional state content is stored in own variable. Is no longer passed via a kwarg to the step function.
### Deprecated
......
......@@ -13,6 +13,7 @@ game:
layout_chars:
_: Free
hash: Counter # #
equal: EdgeCounter # =
A: Agent
pipe: Extinguisher
P: PlateDispenser
......@@ -186,6 +187,8 @@ hook_callbacks:
- progress_finished
- content_ready
- dispenser_item_returned
- additional_state_update
- game_ended_step
callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder ''
callback_class_kwargs:
......
......@@ -184,6 +184,8 @@ hook_callbacks:
- progress_finished
- content_ready
- dispenser_item_returned
- additional_state_update#
- game_ended_step
callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder ''
callback_class_kwargs:
......
......@@ -172,6 +172,11 @@ FishAndChips:
needs: [ FriedFish, Chips ]
equipment: ~
BurgerWithChips:
type: Meal
needs: [ Burger, Chips ]
equipment: ~
Pizza:
type: Meal
needs: [ PizzaBase, ChoppedTomato, GratedCheese, ChoppedSausage ]
......
###N#T##U####
#___________|
L___A___A___#
#___A___A___#
#___________S
##########__+
P___________#
......
=#N#T###U###=
#___________|
#___A_______#
#___________S
=#########__+
P___________#
$_______A___#
$___________X
=##C#C###@@#=
; seconds=150
; plates={c:0, d:0}
; dirty_plates=true
; link: https://overcooked.fandom.com/wiki/1-1_(Overcooked!)
\ No newline at end of file
=#S+#====C#C#=
T____#==#____|
M_A__####__A_#
B____________#
L____####____$
#____#==#____$
#____#==#____P
X____#==#____#
=QQ#U====#@@@=
; seconds=240
; plates={c:0, d:0}
; dirty_plates=true
; link: https://overcooked.fandom.com/wiki/1-4_(Overcooked!)
\ No newline at end of file
====P$$#====
=T#N____##|=
#_A________X
#_###S+###_#
#_#======#_#
#_#======#_#
#_####@@##_#
#________A_#
=#C#C###U#U=
; seconds=240
; plates={c:0, d:0}
; dirty_plates=true
; link: https://overcooked.fandom.com/wiki/1-5_(Overcooked!)
\ No newline at end of file
=#C#C#####$$=
#___________#
#_A_________P
#_____##@#@#=
X_____#_____X
=###@##_____|
F_________A_#
Q___________K
=#+S###LT#MB=
\ No newline at end of file
levels:
- config_path: CONFIGS_DIR/environment_config.yaml
layout_path: LAYOUTS_DIR/overcooked-1/1-1-far-apart.layout
layout_path: LAYOUTS_DIR/study_layouts/1-1-far-apart.layout
item_info_path: CONFIGS_DIR/item_info.yaml
name: "Level 1"
seed: 12345
config_overwrite:
player_config:
speed_units_per_seconds: 5
game:
time_limit_seconds: 300
validate_recipes: false
plates:
clean_plates: 0
dirty_plates: 0
orders:
order_gen_kwargs:
order_duration_random_func:
kwargs:
a: 60
b: 70
return_dirty: true
orders:
meals:
all: false
list:
- TomatoSoup
- OnionSoup
order_gen_kwargs:
order_duration_random_func:
kwargs:
a: 60
b: 70
sample_on_dur_random_func:
func: uniform
kwargs:
a: 45
b: 35
- config_path: CONFIGS_DIR/environment_config.yaml
layout_path: LAYOUTS_DIR/overcooked-1/1-4-bottleneck.layout
layout_path: LAYOUTS_DIR/study_layouts/1-4-bottleneck.layout
item_info_path: CONFIGS_DIR/item_info.yaml
name: "Level 2"
seed: 12345
config_overwrite:
player_config:
speed_units_per_seconds: 5
game:
time_limit_seconds: 300
plates:
clean_plates: 1
dirty_plates: 0
return_dirty: true
orders:
meals:
all: false
list:
- Burger
- Salad
- TomatoSoup
order_gen_kwargs:
order_duration_random_func:
kwargs:
a: 60
b: 70
sample_on_dur_random_func:
func: uniform
kwargs:
a: 45
b: 35
- config_path: CONFIGS_DIR/environment_config.yaml
layout_path: LAYOUTS_DIR/overcooked-1/1-5-circle.layout
layout_path: LAYOUTS_DIR/study_layouts/1-5-circle.layout
item_info_path: CONFIGS_DIR/item_info.yaml
name: "Level 3"
seed: 12345
config_overwrite:
player_config:
speed_units_per_seconds: 5
game:
time_limit_seconds: 300
validate_recipes: true
plates:
clean_plates: 1
dirty_plates: 0
return_dirty: false
return_dirty: true
orders:
meals:
all: false
list:
- TomatoSoup
- OnionSoup
order_gen_kwargs:
order_duration_random_func:
kwargs:
a: 60
b: 70
sample_on_dur_random_func:
func: uniform
kwargs:
a: 45
b: 35
- config_path: CONFIGS_DIR/environment_config.yaml
layout_path: LAYOUTS_DIR/overcooked-1/4-1-moving-counters.layout
layout_path: LAYOUTS_DIR/study_layouts/forced-cooperation.layout
item_info_path: CONFIGS_DIR/item_info.yaml
name: "Level 4"
seed: 12345
config_overwrite:
player_config:
speed_units_per_seconds: 300
game:
time_limit_seconds: 300
time_limit_seconds: 10
plates:
clean_plates: 0
dirty_plates: 0
return_dirty: true
orders:
meals:
all: false
list:
- Burger
- Salad
- Chips
- BurgerWithChips
order_gen_kwargs:
order_duration_random_func:
kwargs:
a: 60
b: 70
sample_on_dur_random_func:
func: uniform
kwargs:
a: 40
b: 50
num_players: 1
num_bots: 0
......@@ -333,6 +333,10 @@ class Counter:
}
class EdgeCounter(Counter):
...
class CuttingBoard(Counter):
"""Cutting ingredients on. The requirement in a new object could look like.
......
......@@ -52,6 +52,7 @@ from cooperative_cuisine.hooks import (
ITEM_INFO_CONFIG,
POST_STEP,
hooks_via_callback_class,
ADDITIONAL_STATE_UPDATE,
)
from cooperative_cuisine.items import (
ItemInfo,
......@@ -313,6 +314,10 @@ class Environment:
self.info_msgs_per_player: dict[str, list[InfoMsg]] = defaultdict(list)
"""Cache of info messages per player which should be showed in the visualization of each player."""
self.additional_state_content = {}
"""The environment will extend the content of each state with this dictionary. Adapt it with the setter
function."""
self.hook(
ENV_INITIALIZED,
environment_config=env_config,
......@@ -483,14 +488,11 @@ class Environment:
effect_manager.progress(passed_time=passed_time, now=self.env_time)
self.hook(POST_STEP, passed_time=passed_time)
def get_state(
self, player_id: str = None, additional_key_values: dict = None
) -> dict:
def get_state(self, player_id: str = None) -> dict:
"""Get the current state of the game environment. The state here is accessible by the current python objects.
Args:
player_id: The player for which to get the state.
additional_key_values: Additional dict that is added to the state
Returns:
The state of the game as a dict.
......@@ -529,26 +531,23 @@ class Environment:
for msg in self.info_msgs_per_player[player_id]
if msg["start_time"] < self.env_time < msg["end_time"]
],
**(additional_key_values if additional_key_values else {}),
**self.additional_state_content,
}
self.hook(STATE_DICT, state=state, player_id=player_id)
return state
raise ValueError(f"No valid {player_id=}")
def get_json_state(
self, player_id: str = None, additional_key_values: dict = None
) -> str:
def get_json_state(self, player_id: str = None) -> str:
"""Return the current state of the game formatted in json dict.
Args:
player_id: The player for which to get the state.
additional_key_values: Additional dict that is added to the state
Returns:
The state of the game formatted as a json-string
"""
state = self.get_state(player_id, additional_key_values)
state = self.get_state(player_id)
json_data = json.dumps(state)
self.hook(JSON_STATE, json_data=json_data, player_id=player_id)
# assert StateRepresentation.model_validate_json(json_data=json_data)
......@@ -586,3 +585,7 @@ class Environment:
"""Add a value to the current score and log it."""
self.score += score
log.debug(f"Score: {self.score} ({score}) - {info}")
def update_additional_state_content(self, **kwargs):
self.hook(ADDITIONAL_STATE_UPDATE, update=kwargs)
self.additional_state_content.update(kwargs)
......@@ -115,8 +115,6 @@ class EnvironmentData:
"""Time of when the environment was started."""
last_step_time: int | None = None
"""Time of the last performed step of the environment."""
all_players_ready: bool = False
"""Did all players send 'ready'."""
# add manager_id?
......@@ -192,6 +190,8 @@ class EnvironmentHandler:
graphs = env.recipe_validation.get_recipe_graphs()
kitchen_size = (env.kitchen_width, env.kitchen_height)
env.update_additional_state_content(all_players_ready=False)
res = CreateEnvResult(
env_id=env_id,
player_info=player_info,
......@@ -280,7 +280,9 @@ 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].all_players_ready = True
self.envs[env_id].environment.update_additional_state_content(
all_players_ready=True
)
def get_state(
self, player_hash: str
......@@ -301,7 +303,6 @@ class EnvironmentHandler:
env_data = self.envs[self.player_data[player_hash].env_id]
state = env_data.environment.get_json_state(
self.player_data[player_hash].player_id,
additional_key_values={"all_players_ready": env_data.all_players_ready},
)
return state
if player_hash not in self.player_data:
......
......@@ -511,6 +511,14 @@ Args:
counter (Counter): the last interacted counter.
"""
# -- extra --
ADDITIONAL_STATE_UPDATE = "additional_state_update"
"""Update of the additional content of the state.
Args:
update (dict[str, Any]): update of the additional state content.
"""
class Hooks:
"""Represents a collection of hooks and provides methods to register callbacks for hooks and invoke the callbacks when hooks are triggered.
......
......@@ -402,7 +402,7 @@ class RandomOrderGeneration(OrderGeneration):
"""For efficient checking to update order removable."""
self.number_cur_orders: int = 0
"""How many orders are currently open."""
self.needed_orders: int = 0
self.num_needed_orders: int = 0
"""For the sample on dur but when it was restricted due to max order number."""
def init_orders(self, now) -> list[Order]:
......@@ -435,26 +435,25 @@ class RandomOrderGeneration(OrderGeneration):
if new_finished_orders:
self.create_random_next_time_delta(now)
return []
# print(
# " - -",
# self.needed_orders,
# self.number_cur_orders,
# self.next_order_time,
# now,
# )
if self.needed_orders:
self.needed_orders -= len(new_finished_orders)
self.needed_orders = max(self.needed_orders, 0)
self.number_cur_orders += len(new_finished_orders)
# print(self.number_cur_orders, self.num_needed_orders)
if self.num_needed_orders:
# self.num_needed_orders -= len(new_finished_orders)
# self.num_needed_orders = max(self.num_needed_orders, 0)
# self.number_cur_orders += len(new_finished_orders)
return self.create_orders_for_meals(
self.random.choices(self.available_meals, k=len(new_finished_orders)),
self.random.choices(
self.available_meals,
k=len(new_finished_orders) + len(expired_orders),
),
now,
)
if self.next_order_time <= now:
if self.number_cur_orders >= self.kwargs.max_orders:
self.needed_orders += 1
self.num_needed_orders += 1
else:
if not self.kwargs.sample_on_serving:
self.create_random_next_time_delta(now)
......
......@@ -426,7 +426,7 @@ class Visualizer:
grid_size,
grid_size,
),
width=2,
width=3,
)
def draw_thing(
......@@ -924,7 +924,22 @@ class Visualizer:
self, screen: pygame.Surface, graph_dict, width, height, grid_size
) -> None:
# screen.fill(self.config["GameWindow"]["background_color"])
positions_dict = graph_dict["layout"]
positions = np.array(list(positions_dict.values()))
unique_x_vals = np.unique(positions[:, 0])
new_positions_unique = np.linspace(
start=0,
stop=np.max(positions[:, 0]),
num=len(unique_x_vals),
)
replace_map = {
unique_x_vals[i]: new_positions_unique[i] for i in range(len(unique_x_vals))
}
for k, v in graph_dict["layout"].items():
graph_dict["layout"][k] = (replace_map[v[0]], v[1])
positions = np.array(list(positions_dict.values()))
positions = positions - positions.min(axis=0)
positions[positions == 0] = 0.000001
......
......@@ -173,7 +173,6 @@ class PyGameGUI:
self.FPS = self.visualization_config["GameWindow"]["FPS"]
self.screen_margin = self.visualization_config["GameWindow"]["screen_margin"]
self.min_width = self.visualization_config["GameWindow"]["min_width"]
self.min_height = self.visualization_config["GameWindow"]["min_height"]
self.buttons_width = self.visualization_config["GameWindow"]["buttons_width"]
......@@ -203,6 +202,10 @@ class PyGameGUI:
self.images_path = ROOT_DIR / "pygame_gui" / "images"
self.vis = Visualizer(self.visualization_config)
self.last_score: float = 0
self.switch_score_color: bool = False
self.count_frames_score_label: int = 0
self.fullscreen = False if self.show_debug_elements else True
self.menu_state = MenuStates.Start
......@@ -424,6 +427,9 @@ class PyGameGUI:
self.window_width = self.window_width_windowed
self.window_height = self.window_height_windowed
self.screen_margin = self.visualization_config["GameWindow"][
"screen_margin_proportion"
] * min(self.window_width, self.window_height)
self.main_window = pygame.display.set_mode(
(
self.window_width,
......@@ -531,24 +537,30 @@ class PyGameGUI:
object_id="#start_button",
)
img = pygame.image.load(
ROOT_DIR / "pygame_2d_vis" / "gui_images" / f"continue_{self.language}.png"
).convert_alpha()
image_rect = img.get_rect()
img_width = self.buttons_width * 1.5
img_height = img_width * (image_rect.height / image_rect.width)
new_dims = (img_width, img_height)
img = pygame.transform.smoothscale(img, new_dims)
image_rect = img.get_rect()
image_rect.centery += 80
self.press_a_image = pygame_gui.elements.UIImage(
image_rect,
img,
manager=self.manager,
anchors={"centerx": "centerx", "centery": "centery"},
)
if self.visualization_config["Gui"]["press_button_to_continue"]:
img = pygame.image.load(
ROOT_DIR
/ "pygame_2d_vis"
/ "gui_images"
/ f"continue_{self.language}.png"
).convert_alpha()
image_rect = img.get_rect()
img_width = self.buttons_width * 1.5
img_height = img_width * (image_rect.height / image_rect.width)
new_dims = (img_width, img_height)
img = pygame.transform.smoothscale(img, new_dims)
image_rect = img.get_rect()
image_rect.centery += 80
self.press_a_image = pygame_gui.elements.UIImage(
image_rect,
img,
manager=self.manager,
anchors={"centerx": "centerx", "centery": "centery"},
)
else:
self.press_a_image = None
# self.press_a_image.set_dimensions(new_dims)
if not self.CONNECT_WITH_STUDY_SERVER:
......@@ -827,13 +839,12 @@ class PyGameGUI:
anchors={"centerx": "centerx", "top_target": self.level_name_label},
)
scroll_height = (
self.scroll_height = (
self.continue_button.get_abs_rect().top
- self.text_recipes_label.get_abs_rect().bottom
)
self.scroll_width = self.window_width
self.scroll_space_recipes = pygame_gui.elements.UIScrollingContainer(
relative_rect=pygame.Rect((0, 0), (self.scroll_width, scroll_height)),
relative_rect=pygame.Rect((0, 0), (self.window_width, self.scroll_height)),
manager=self.manager,
anchors={"centerx": "centerx", "top_target": self.text_recipes_label},
)
......@@ -947,7 +958,7 @@ class PyGameGUI:
)
self.scroll_width_completed_meals = self.window_width
self.scroll_space_completed_meals = pygame_gui.elements.UIScrollingContainer(
relative_rect=pygame.Rect((0, 0), (self.scroll_width, scroll_height)),
relative_rect=pygame.Rect((0, 0), (self.window_width, scroll_height)),
manager=self.manager,
anchors={
"centerx": "centerx",
......@@ -1089,9 +1100,11 @@ class PyGameGUI:
+ self.other_elements
)
for element in all_elements:
element.hide()
if element:
element.hide()
for element in elements + self.on_all_screens:
element.show()
if element:
element.show()
def setup_tutorial_screen(self):
"""Updates the tutorial screen with the current tutorial image and the continue button."""
......@@ -1327,7 +1340,7 @@ class PyGameGUI:
self.vis.draw_orders(
screen=self.orders_image.image,
state=state,
grid_size=self.buttons_height,
grid_size=self.screen_margin * 0.68,
width=self.orders_container_width,
height=self.screen_margin,
config=self.visualization_config,
......@@ -1425,6 +1438,33 @@ class PyGameGUI:
"translations.score", text_kwargs={"score": str(score)}
)
if self.switch_score_color:
self.count_frames_score_label += 1
duration_color_change = 90
if score > self.last_score:
self.score_label.update_theming(
'{"colours": {"normal_text": "#03b706"}, "font": { "size": 20, "bold": 1}}'
)
self.count_frames_score_label = 0
self.switch_score_color = True
elif score < self.last_score:
self.score_label.update_theming(
'{"colours": {"normal_text": "#e22312"}, "font": { "size": 20, "bold": 1}}'
)
self.count_frames_score_label = 0
self.switch_score_color = True
elif self.switch_score_color:
if self.count_frames_score_label >= duration_color_change:
self.score_label.update_theming(
'{"colours": {"normal_text": "#000000"}, "font": { "size": 20, "bold": 1}}'
)
self.count_frames_score_label = 0
self.switch_score_color = False
self.last_score = score
def update_remaining_time(self, remaining_time: float):
"""Updates the remaining time label.
......@@ -1472,7 +1512,7 @@ class PyGameGUI:
environment_config=environment_config,
layout_config=layout,
seed=seed,
env_name=layout_path.stem
env_name=layout_path.stem,
).model_dump(mode="json")
# print(CreateEnvironmentConfig.model_validate_json(json_data=creation_json))
......@@ -1514,12 +1554,12 @@ class PyGameGUI:
"translations.level_name", text_kwargs={"level": self.level_info["name"]}
)
graph_width = self.window_width * 0.55
graph_width = self.window_width * 0.6
rows = 0
for rg in self.level_info["recipe_graphs"]:
rows += len(np.unique(np.array(list(rg["layout"].values()))[:, 1]))
row_height = self.window_height / 14
container_width = self.scroll_width * 0.9
container_width = self.window_width * 0.9
container_height = rows * row_height
icon_size = row_height * 0.9
......@@ -1543,8 +1583,11 @@ class PyGameGUI:
meal = re.sub(r"(?<!^)(?=[A-Z])", " ", meal)
positions = np.array(list(rg["layout"].values()))
unique_vals = np.unique(positions[:, 1])
height = row_height * len(unique_vals)
unique_x_vals = np.unique(positions[:, 0])
height = row_height * len(np.unique(positions[:, 1]))
graph_height = height * 0.9
graph_surface = pygame.Surface(
......@@ -1600,11 +1643,10 @@ class PyGameGUI:
container=container,
anchors={"centery": "centery", "right": "right"},
)
last_recipes_labels.append(container)
self.scroll_space_recipes.set_scrollable_area_dimensions(
(self.scroll_width * 0.95, container_height)
(self.window_width * 0.95, container_height)
)
def setup_tutorial(self):
......@@ -2097,8 +2139,7 @@ class PyGameGUI:
if event.type == pygame.JOYDEVICEREMOVED:
self.remove_joystick(event)
# Press enter key or controller start button instead of mouse button press
if (
if self.visualization_config["Gui"]["press_button_to_continue"] and (
event.type == pygame.JOYBUTTONDOWN
and any([joy.get_button(7) for joy in self.joysticks.values()])
or (event.type == pygame.KEYDOWN and event.key == pygame.K_RETURN)
......
cooperative_cuisine/pygame_2d_vis/images/plate_dispenser.png

9.46 KiB

cooperative_cuisine/pygame_2d_vis/images/wall.png

7.19 KiB

......@@ -13,7 +13,7 @@
"salad_recipe": "Rezept für Salat:",
"recipes_in_this_level": "Rezepte in diesem Level:",
"level_name": "%{level}",
"was_served": " wurde serviert",
"was_served": " wurde serviert.",
"waiting_for_players": "WARTE AUF ANDERE SPIELER",
"orders": "Bestellungen:",
"score": "Punktestand: %{score}",
......@@ -25,7 +25,7 @@
"completed_level": "Level beendet: %{level}",
"next_game": "Nächstes Level",
"finish_study": "Studie Beenden",
"thank_you": "Vielen Dank an der Teilnahme!",
"thank_you": "Vielen Dank für der Teilnahme an der Studie!",
"signal_supervisor": "Bitte gib dem Studienteilnehmer Bescheid, dass die Studie vorbei ist.",
"Tomato Soup": "Tomatensuppe:",
"Onion Soup": "Zwiebelsuppe:",
......@@ -34,6 +34,7 @@
"Fish And Chips": "Fish and Chips:",
"Chips": "Pommes",
"Salad": "Salat:",
"Fried Fish": "Backfisch:"
"Fried Fish": "Backfisch:",
"Burger With Chips": "Burger mit Pommes:"
}
}
\ No newline at end of file