diff --git a/cooperative_cuisine/counters.py b/cooperative_cuisine/counters.py index 01421b2137f238b8413eee3194a227c2c87e8576..cd84be94e982e3c6e49ecefd501dbe075be84237 100644 --- a/cooperative_cuisine/counters.py +++ b/cooperative_cuisine/counters.py @@ -684,10 +684,10 @@ class PlateDispenser(Counter): def progress(self, passed_time: timedelta, now: datetime): """Check if plates arrive from outside the kitchen and add a dirty plate accordingly""" - if self.next_plate_time < now: + if self.next_plate_time <= now: idx_delete = [] for i, times in enumerate(self.out_of_kitchen_timer): - if times < now: + if times <= now: self.hook(DIRTY_PLATE_ARRIVES, counter=self, times=times, now=now) idx_delete.append(i) log.debug("Add dirty plate") diff --git a/cooperative_cuisine/effects.py b/cooperative_cuisine/effects.py index 3688ce649ef10ddbddb1671709a75db41f5a1fe6..53989c0916dd9d8f2fe82b44ef2be4f971e1da29 100644 --- a/cooperative_cuisine/effects.py +++ b/cooperative_cuisine/effects.py @@ -163,9 +163,9 @@ class FireEffectManager(EffectManager): self.active_effects.append((effect, target)) # reset new effects self.new_effects = [] - if self.next_finished_timer < now: + if self.next_finished_timer <= now: for effect, target in self.active_effects: - if self.effect_to_timer[effect.uuid] < now: + if self.effect_to_timer[effect.uuid] <= now: if isinstance(target, Item): target = find_item_on_counters(target.uuid, self.counters) if target: diff --git a/cooperative_cuisine/info_msg.py b/cooperative_cuisine/info_msg.py index 3d6d95257a0e138282e32b8666a72627f12cb974..7899e9cee58554d40bda22669d467dba1df1deba 100644 --- a/cooperative_cuisine/info_msg.py +++ b/cooperative_cuisine/info_msg.py @@ -95,7 +95,7 @@ class InfoMsgManager(HookCallbackClass): for player_id, msgs in env.info_msgs_per_player.items(): delete_msgs = [] for idx, msg in enumerate(msgs): - if msg["end_time"] < env.env_time: + if msg["end_time"] <= env.env_time: delete_msgs.append(idx) for idx in reversed(delete_msgs): msgs.pop(idx) diff --git a/cooperative_cuisine/pygame_2d_vis/drawing.py b/cooperative_cuisine/pygame_2d_vis/drawing.py index f9a994625856c4ee3a2bee92e208c0df7b6a6d3c..0c67eaf1549e8e45510832cc5c9296591883715c 100644 --- a/cooperative_cuisine/pygame_2d_vis/drawing.py +++ b/cooperative_cuisine/pygame_2d_vis/drawing.py @@ -2,6 +2,7 @@ import argparse import colorsys import json import os +import sys from datetime import datetime, timedelta from pathlib import Path @@ -1002,7 +1003,21 @@ def generate_recipe_images(config: dict, folder_path: str | Path): pygame.image.save(screen, f"{folder_path}/{graph_dict['meal']}.png") -if __name__ == "__main__": +def main(args): + """ + + Runs the Cooperative Cuisine Image Generation process. + + This method takes command line arguments to specify the state file, visualization configuration file, and output file for the generated image. It then reads the visualization configuration + * file and state file, and calls the 'save_screenshot' and 'generate_recipe_images' methods to generate the image. + + Args: + -s, --state: A command line argument of type `argparse.FileType("r", encoding="UTF-8")`. Specifies the state file to use for image generation. If not provided, the default value is 'ROOT_DIR / "pygame_2d_vis" / "sample_state.json"'. + + -v, --visualization_config: A command line argument of type `argparse.FileType("r", encoding="UTF-8")`. Specifies the visualization configuration file to use for image generation. If not provided, the default value is 'ROOT_DIR / "pygame_2d_vis" / "visualization.yaml"'. + + -o, --output_file: A command line argument of type `str`. Specifies the output file path for the generated image. If not provided, the default value is 'ROOT_DIR / "generated" / "screenshot.jpg"'. + """ parser = argparse.ArgumentParser( prog="Cooperative Cuisine Image Generation", description="Generate images for a state in json.", @@ -1024,12 +1039,16 @@ if __name__ == "__main__": "-o", "--output_file", type=str, - default=ROOT_DIR / "pygame_2d_vis" / "generated" / "screenshot.jpg", + default=ROOT_DIR / "generated" / "screenshot.jpg", ) - args = parser.parse_args() + args = parser.parse_args(args) with open(args.visualization_config, "r") as f: viz_config = yaml.safe_load(f) with open(args.state, "r") as f: state = json.load(f) save_screenshot(state, viz_config, args.output_file) generate_recipe_images(viz_config, args.output_file.parent) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/cooperative_cuisine/scores.py b/cooperative_cuisine/scores.py index 5ae2c6ce83146b3d190f67e3e8236e3637a1b244..948d28f6d1e3ceb4685f9a2b17432481aaba0745 100644 --- a/cooperative_cuisine/scores.py +++ b/cooperative_cuisine/scores.py @@ -94,13 +94,13 @@ class ScoreViaHooks(HookCallbackClass): **kwargs: Additional keyword arguments to be passed to the parent class. """ super().__init__(name, env, **kwargs) - self.score_map = score_map + self.score_map: dict[str, float] = score_map """Mapping of hook references to scores.""" - self.static_score = static_score + self.static_score: float = static_score """The static score to be added if no other conditions are met.""" - self.kwarg_filter = kwarg_filter + self.kwarg_filter: dict[str, Any] = kwarg_filter """Filtering condition for keyword arguments.""" - self.score_on_specific_kwarg = score_on_specific_kwarg + self.score_on_specific_kwarg: str = score_on_specific_kwarg """The specific keyword argument to score on.""" def __call__(self, hook_ref: str, env: Environment, **kwargs): @@ -114,7 +114,7 @@ class ScoreViaHooks(HookCallbackClass): self.env.increment_score(self.static_score, info=hook_ref) elif self.score_map and hook_ref in self.score_map: if self.kwarg_filter: - if kwargs.items() <= self.kwarg_filter.items(): + if self.kwarg_filter.items() <= kwargs.items(): self.env.increment_score( self.score_map[hook_ref], info=f"{hook_ref} - {self.kwarg_filter}", diff --git a/tests/test_game_server.py b/tests/test_game_server.py index f072fed2dd5413ad016e64406146262c93723e53..84271ad77196e85aef512ce9994e0d8926130a0e 100644 --- a/tests/test_game_server.py +++ b/tests/test_game_server.py @@ -282,3 +282,10 @@ def test_websocket_wrong_inputs(create_env_config): finally: task.cancel() loop.close() + + +def test_root(): + with TestClient(app) as client: + res = client.get("/") + assert res.status_code == status.HTTP_200_OK + assert res.json() == {"Cooperative": "Cuisine"} diff --git a/tests/test_pygame.py b/tests/test_pygame.py index c44569519c406ec433d6b1ea1395109b86ca50be..9df43e0813184f20a159001c91684f149bc17fbb 100644 --- a/tests/test_pygame.py +++ b/tests/test_pygame.py @@ -1,7 +1,10 @@ +import json +import os + import yaml from cooperative_cuisine import ROOT_DIR -from cooperative_cuisine.pygame_2d_vis.drawing import calc_angle, Visualizer +from cooperative_cuisine.pygame_2d_vis.drawing import calc_angle, Visualizer, main from cooperative_cuisine.pygame_2d_vis.game_colors import RGB, WHITE, colors @@ -16,6 +19,11 @@ def test_calc_angle(): assert calc_angle([1.0, 1.0], [1.0, -1.0]) == -90.0 +def test_drawing(): + main([]) + assert os.path.exists(os.path.join(ROOT_DIR, "generated", "screenshot.jpg")) + + def test_visualizer(): with open(ROOT_DIR / "pygame_2d_vis" / "visualization.yaml", "r") as file: visualization_config = yaml.safe_load(file) @@ -25,3 +33,8 @@ def test_visualizer(): vis.create_player_colors(10) assert len(vis.player_colors) >= 10 assert len(set(vis.player_colors)) >= 10 + + with open(ROOT_DIR / "pygame_2d_vis" / "sample_state.json", "r") as file: + state = json.load(file) + image = vis.get_state_image(40, state) + assert image.shape == (480, 360, 3) diff --git a/tests/test_start.py b/tests/test_start.py index fec31349ac50c39f3d02019c66f1a7f802389a33..744bf6869590ad55b70c7dfb3cf0f3dd6e86af83 100644 --- a/tests/test_start.py +++ b/tests/test_start.py @@ -2,16 +2,32 @@ from datetime import timedelta import numpy as np import pytest +import yaml from cooperative_cuisine import ROOT_DIR from cooperative_cuisine.action import ActionType, InterActionData, Action -from cooperative_cuisine.counters import Counter, CuttingBoard +from cooperative_cuisine.counters import ( + Counter, + CuttingBoard, + CookingCounter, + ServingWindow, + Trashcan, +) +from cooperative_cuisine.effects import FireEffectManager from cooperative_cuisine.environment import ( Environment, ) from cooperative_cuisine.game_server import PlayerRequestType -from cooperative_cuisine.hooks import Hooks -from cooperative_cuisine.items import Item, ItemInfo, ItemType +from cooperative_cuisine.hooks import ( + Hooks, + SERVE_NOT_ORDERED_MEAL, + hooks_via_callback_class, + PLAYER_ADDED, + POST_STEP, +) +from cooperative_cuisine.info_msg import InfoMsgManager +from cooperative_cuisine.items import Item, ItemInfo, ItemType, Plate, CookingEquipment +from cooperative_cuisine.scores import ScoreViaHooks from cooperative_cuisine.server_results import ( PlayerInfo, CreateEnvResult, @@ -21,7 +37,7 @@ from cooperative_cuisine.state_representation import ( StateRepresentation, create_json_schema, ) -from cooperative_cuisine.utils import create_init_env_time +from cooperative_cuisine.utils import create_init_env_time, get_touching_counters layouts_folder = ROOT_DIR / "configs" / "layouts" environment_config_path = ROOT_DIR / "configs" / "environment_config.yaml" @@ -54,7 +70,6 @@ def layout_config(): with open(layout_path, "r") as file: layout = file.read() return layout - env.add_player("0") @pytest.fixture @@ -218,6 +233,7 @@ def test_processing(env_config, layout_config, item_info): }, ) env.counters.append(counter) + env.overwrite_counters(env.counters) tomato = Item(name="Tomato", item_info=None) env.add_player("1", np.array([2, 3])) @@ -308,3 +324,237 @@ def test_server_result_definition(): msg="123", player_hash="1234324", ) + + +def test_fire(env_config, layout_config, item_info): + env = Environment(env_config, layout_config, item_info, as_files=False) + env.add_player("0") + oven = None + for c in env.counters: + if ( + isinstance(c, CookingCounter) + and c.name == "Stove" + and c.occupied_by.name == "Pan" + ): + oven = c + break + assert oven is not None + + raw_patty = Item(name="RawPatty", item_info=env.item_info["RawPatty"]) + + assert oven.can_drop_off(raw_patty) + + oven.drop_off(raw_patty, "0") + assert isinstance(oven.occupied_by, CookingEquipment) + assert oven.occupied_by.content_list == [raw_patty] + + env.step(timedelta(seconds=env.item_info["CookedPatty"].seconds)) + assert oven.occupied_by.content_list[0].name == "CookedPatty" + env.step(timedelta(seconds=env.item_info["BurntCookedPatty"].seconds)) + assert oven.occupied_by.content_list[0].name == "BurntCookedPatty" + env.step(timedelta(seconds=env.item_info["Fire"].seconds)) + assert len(oven.occupied_by.active_effects) != 0 + assert oven.occupied_by.active_effects[0].name == "Fire" + + fire_manager = env.effect_manager["FireManager"] + assert isinstance(fire_manager, FireEffectManager) + env.step(fire_manager.next_finished_timer - env.env_time) + + touching_counters = get_touching_counters(oven, env.counters) + next_empty = None + connect_counter = None + for c in touching_counters: + if c.occupied_by: + assert len(c.occupied_by.active_effects) == 1 + else: + assert len(c.active_effects) == 1 + next_touching = get_touching_counters(c, env.counters) + for a in next_touching: + if a not in touching_counters and a.__class__.__name__ == "Counter": + a.occupied_by = None + next_empty = a + connect_counter = c + env.step(timedelta(seconds=0.01)) + assert next_empty is not None + next_empty.occupied_by = Item(name="Tomato", item_info=env.item_info["Tomato"]) + env.step( + fire_manager.effect_to_timer[connect_counter.active_effects[0].uuid] + - env.env_time + ) + assert len(next_empty.occupied_by.active_effects) == 1 + + fire_extinguisher = None + for c in env.counters: + if c.occupied_by and c.occupied_by.name == "Extinguisher": + fire_extinguisher = c.occupied_by + c.occupied_by = None + break + + assert fire_extinguisher is not None + env.players["0"].holding = fire_extinguisher + env.players["0"].pos = oven.pos + env.players["0"].pos[1] -= 1.0 + env.perform_action( + Action( + player="0", + action_type=ActionType.MOVEMENT, + action_data=np.array([0.0, -1.0]), + duration=0.1, + ) + ) + env.step(timedelta(seconds=0.1)) + + env.perform_action( + Action( + player="0", + action_type=ActionType.INTERACT, + action_data=InterActionData.START, + ) + ) + env.step(timedelta(seconds=env.item_info["Extinguisher"].seconds)) + env.step(timedelta(seconds=env.item_info["Extinguisher"].seconds)) + + assert len(oven.occupied_by.active_effects) == 0 + + +def test_score(env_config, layout_config, item_info): + def incr_score_callback(hook_ref, env: Environment, meal, meal_name, **kwargs): + assert isinstance(meal, Item) + assert isinstance(meal_name, str) + assert meal_name == "TomatoSoup" + env.increment_score(1_000, "Soup Soup") + + env = Environment(env_config, layout_config, item_info, as_files=False) + assert env.score == 0.0 + env.add_player("0") + env.register_callback_for_hook(SERVE_NOT_ORDERED_MEAL, incr_score_callback) + env.register_callback_for_hook( + SERVE_NOT_ORDERED_MEAL, + ScoreViaHooks( + name="123", + env=env, + score_on_specific_kwarg="meal_name", + score_map={"TomatoSoup": 2_000}, + ), + ) + env.register_callback_for_hook( + SERVE_NOT_ORDERED_MEAL, + ScoreViaHooks(name="124", env=env, score_map={SERVE_NOT_ORDERED_MEAL: 4_000}), + ) + env.register_callback_for_hook( + SERVE_NOT_ORDERED_MEAL, + ScoreViaHooks( + name="124", + env=env, + score_map={SERVE_NOT_ORDERED_MEAL: 8_000}, + kwarg_filter={"meal_name": "TomatoSoup"}, + ), + ) + env.register_callback_for_hook( + SERVE_NOT_ORDERED_MEAL, + ScoreViaHooks( + name="123", + env=env, + score_on_specific_kwarg="meal_name", + static_score=16_000, + score_map={}, + ), + ) + serving_window = None + for c in env.counters: + if isinstance(c, ServingWindow): + serving_window = c + break + assert serving_window is not None + env.order_manager.serving_not_ordered_meals = True + env.order_manager.open_orders = [] + plate = Plate( + transitions=env.counter_factory.filter_item_info( + by_item_type=ItemType.Meal, add_effects=True + ), + clean=True, + item_info=env.item_info["Plate"], + hook=env.hook, + ) + plate.content_list = [ + Item(name="TomatoSoup", item_info=env.item_info["TomatoSoup"]) + ] + assert serving_window.can_drop_off(plate) + + returned = serving_window.drop_off(plate, "0") + assert returned is None + + plates_prev = len(serving_window.plate_dispenser.occupied_by) + assert env.score >= 31_000 + assert len(serving_window.plate_dispenser.out_of_kitchen_timer) == 1 + env.step(serving_window.plate_dispenser.out_of_kitchen_timer[0] - env.env_time) + assert len(serving_window.plate_dispenser.out_of_kitchen_timer) == 0 + assert len(serving_window.plate_dispenser.occupied_by) == plates_prev + 1 + assert ( + serving_window.plate_dispenser.occupied_by[0].clean + != serving_window.plate_dispenser.plate_config.return_dirty + ) + + +def test_info_msgs(env_config, layout_config, item_info): + env_config_dict = yaml.load(env_config, Loader=yaml.Loader) + # TODO change after merge with 115 + env_config_dict["extra_setup_functions"]["dummy_msg"] = { + "func": hooks_via_callback_class, + "kwargs": { + "hooks": [PLAYER_ADDED], + "callback_class": InfoMsgManager, + "callback_class_kwargs": {"msg": "hello there"}, + }, + } + + env_config_dict["extra_setup_functions"]["dummy_msg_2"] = { + "func": hooks_via_callback_class, + "kwargs": { + "hooks": [POST_STEP], + "callback_class": InfoMsgManager, + "callback_class_kwargs": {"msg": "step step"}, + }, + } + + env_config = yaml.dump(env_config_dict) + + env = Environment(env_config, layout_config, item_info, as_files=False) + env.add_player("0") + assert env.info_msgs_per_player["0"][0]["msg"] == "hello there" + env.step(timedelta(seconds=0.1)) + assert env.info_msgs_per_player["0"][1]["msg"] == "step step" + env.step(env.info_msgs_per_player["0"][0]["end_time"] - env.env_time) + assert len(env.info_msgs_per_player["0"]) == 2 + assert env.info_msgs_per_player["0"][0]["msg"] == "step step" + + +def test_trashcan(env_config, layout_config, item_info): + env = Environment(env_config, layout_config, item_info, as_files=False) + env.add_player("0") + trash = None + for c in env.counters: + if isinstance(c, Trashcan): + trash = c + break + assert trash is not None + + item = Item(name="Tomato", item_info=env.item_info["Tomato"]) + assert trash.can_drop_off(item) + assert trash.drop_off(item, "0") is None + + plate = Plate( + transitions=env.counter_factory.filter_item_info( + by_item_type=ItemType.Meal, add_effects=True + ), + clean=True, + item_info=env.item_info["Plate"], + hook=env.hook, + ) + plate.content_list = [ + Item(name="TomatoSoup", item_info=env.item_info["TomatoSoup"]) + ] + + assert trash.can_drop_off(plate) + assert trash.drop_off(plate, "0") == plate + assert plate.content_list == []