diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000000000000000000000000000000000000..58c84c7994feb962663602fbd7150f06c6f312b7 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,4 @@ +[report] +exclude_lines = + if TYPE_CHECKING: + if __name__ == .__main__.: \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 20c922cf496d3f850f66a511a0f29e4c35b6d89c..d7fbaa1bf993c21c9d51759ebe9e56487eaabe6a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,13 +3,14 @@ pytest: script: - apt-get update -qy - apt-get install -y python3-dev python3-pip graphviz graphviz-dev - - pip install pytest - - pip install . - - pytest --junitxml=report.xml + - pip install '.[test]' + - pytest --cov --cov-report term-missing --cov-report xml:./coverage.xml --junitxml=./test.xml --cov-config=.coveragerc + coverage: '/^TOTAL .+?(\d+%)$/' artifacts: - when: always reports: - junit: report.xml + coverage_report: + coverage_format: cobertura + path: coverage.xml pages: script: diff --git a/README.md b/README.md index 2798a4f0bf10be8d1b5f63dccb2ce0f29106497d..4573a8452a3005f43388f9e8f1441c754496c7e0 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,18 @@ +<div align="center"> + + + + +</div> + # Cooperative Cuisine Environment [Documentation](https://scs.pages.ub.uni-bielefeld.de/cocosy/overcooked-simulator) The overcooked-like cooperative cuisine environment for real-time human cooperative interactions and artificial agents. - -**The name ist still work in progress and we will probably change it.** +<div align="center"> +<img src="cooperative_cuisine/pygame_2d_vis/images/cooperative_cuisine.png" width="800"> +</div> ## Installation diff --git a/cooperative_cuisine/configs/layouts/basic.layout b/cooperative_cuisine/configs/layouts/basic.layout index 4b887651b95f07aaa0251f04279bd04c0936e995..060ea99b0826eaf1ac7dfba761db774513c455ba 100644 --- a/cooperative_cuisine/configs/layouts/basic.layout +++ b/cooperative_cuisine/configs/layouts/basic.layout @@ -1,5 +1,5 @@ #QU#FO#TNLB# -#__________M +@__________M |__________K $__________I #__A_____A_D diff --git a/cooperative_cuisine/pygame_2d_vis/images/cooperative_cuisine.png b/cooperative_cuisine/pygame_2d_vis/images/cooperative_cuisine.png new file mode 100644 index 0000000000000000000000000000000000000000..bfa9b99ed8ebe0eb063de05d5e5d60c91be86c77 Binary files /dev/null and b/cooperative_cuisine/pygame_2d_vis/images/cooperative_cuisine.png differ diff --git a/setup.py b/setup.py index 658851b4e072e83add01ebbf22717eb837a7801a..db8f170a0466a1aabca8db8554b369640b1dd150 100644 --- a/setup.py +++ b/setup.py @@ -30,9 +30,7 @@ requirements = [ "pydot>=2.0.0", ] -test_requirements = [ - "pytest>=3", -] +test_requirements = ["pytest>=3", "pytest-cov>=4.1"] setup( author="Annika Österdiekhoff, Dominik Battefeld, Fabian Heinrich, Florian Schröder", @@ -58,7 +56,6 @@ setup( name="cooperative_cuisine", packages=find_packages(include=["cooperative_cuisine", "cooperative_cuisine.*"]), test_suite="tests", - tests_require=test_requirements, url="https://gitlab.ub.uni-bielefeld.de/scs/cocosy/overcooked-simulator", version="0.1.0", zip_safe=False, @@ -68,6 +65,7 @@ setup( "stable-baselines3[extra]>=2.2.1", "opencv-python>=4.9", "wandb>=0.16.3", - ] + ], + "test": test_requirements, }, ) diff --git a/tests/test_cooking_equipment.py b/tests/test_cooking_equipment.py new file mode 100644 index 0000000000000000000000000000000000000000..5bc9fa3bb4dc16a8464d7a2284d2cdbd9b0a67a5 --- /dev/null +++ b/tests/test_cooking_equipment.py @@ -0,0 +1,186 @@ +import pytest + +from cooperative_cuisine.game_items import ItemInfo, CookingEquipment, Item, ItemType + + +def test_can_combine_single_other_item(): + """Test the 'can_combine' method with single other item""" + + item_info = ItemInfo(type=ItemType.Meal, name="Soup", seconds=5.0) + cooking_equipment = CookingEquipment( + transitions={}, name="Pot", item_info=item_info + ) + other_item = Item("Onion", ItemInfo(type=ItemType.Ingredient, name="Onion")) + + assert cooking_equipment.can_combine(other_item) == False + + +def test_can_combine_list_of_other_items(): + """Test the 'can_combine' method with list of other items""" + + item_info = ItemInfo(type=ItemType.Meal, name="Soup", seconds=5.0) + cooking_equipment = CookingEquipment( + transitions={}, name="Pot", item_info=item_info + ) + other_item = CookingEquipment( + name="Pan", + transitions={}, + item_info=ItemInfo(type=ItemType.Equipment, name="Pan"), + ) + + assert cooking_equipment.can_combine(other_item) == False + + +def test_can_combine_without_other_item(): + """Test the 'can_combine' method without other item""" + + item_info = ItemInfo(type=ItemType.Meal, name="Soup", seconds=5.0) + cooking_equipment = CookingEquipment( + transitions={}, name="Pot", item_info=item_info + ) + + assert cooking_equipment.can_combine(None) == False + + +def test_combine(): + """Test the 'combine' method""" + + item_info = ItemInfo(type=ItemType.Meal, name="Soup", seconds=5.0) + cooking_equipment = CookingEquipment( + transitions={}, name="Pot", item_info=item_info + ) + other_item = Item("Onion", ItemInfo(type=ItemType.Ingredient, name="Onion")) + + assert cooking_equipment.combine(other_item) is None + + +def test_progress(): + """Test the 'progress' method""" + + item_info = ItemInfo(type=ItemType.Meal, name="Soup", seconds=5.0) + cooking_equipment = CookingEquipment( + transitions={}, name="Pot", item_info=item_info + ) + result = Item(name="TestResult", item_info=None) + cooking_equipment.active_transition = { + "seconds": 5.0, + "result": result, + } + from datetime import datetime, timedelta + + cooking_equipment.progress(passed_time=timedelta(seconds=5.0), now=datetime.now()) + + assert cooking_equipment.content_list == [result] + + +def test_reset_content(): + """Test the 'reset_content' method""" + + item_info = ItemInfo(type=ItemType.Meal, name="Soup", seconds=5.0) + cooking_equipment = CookingEquipment( + transitions={}, name="Pot", item_info=item_info + ) + cooking_equipment.content_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + cooking_equipment.reset_content() + + assert cooking_equipment.content_list == [] + + +def test_release(): + """Test the 'release' method""" + + item_info = ItemInfo(type=ItemType.Meal, name="Soup", seconds=5.0) + cooking_equipment = CookingEquipment( + transitions={}, name="Pot", item_info=item_info + ) + + cooking_equipment.content_list = ["Content1", "Content2"] + + assert cooking_equipment.release() == ["Content1", "Content2"] + assert cooking_equipment.content_list == [] + + +def test_extra_repr_without_content(): + """Test the 'extra_repr' method without content""" + + item_info = ItemInfo(type=ItemType.Meal, name="Soup", seconds=5.0) + cooking_equipment = CookingEquipment( + transitions={}, name="Pot", item_info=item_info + ) + + assert cooking_equipment.extra_repr == "[], None" + + +def test_extra_repr_with_content(): + """Test the 'extra_repr' method with content""" + + item_info = ItemInfo(type=ItemType.Meal, name="Soup", seconds=5.0) + cooking_equipment = CookingEquipment( + transitions={}, name="Pot", item_info=item_info + ) + + item_1 = Item( + name="Tomato", item_info=ItemInfo(type=ItemType.Ingredient, name="Tomato") + ) + item_2 = Item( + name="Potato", item_info=ItemInfo(type=ItemType.Ingredient, name="Potato") + ) + cooking_equipment.content_list.extend([item_1, item_2]) + + assert cooking_equipment.extra_repr == "[Tomato(), Potato()], None" + + +def test_get_potential_meal_without_content(): + """Test the 'get_potential_meal' method without content""" + + item_info = ItemInfo(type=ItemType.Meal, name="Soup", seconds=5.0) + cooking_equipment = CookingEquipment( + transitions={}, name="Pot", item_info=item_info + ) + + assert cooking_equipment.get_potential_meal() is None + + +def test_get_potential_meal_with_content(): + """Test the 'get_potential_meal' method with content""" + + item_info = ItemInfo(type=ItemType.Meal, name="Soup", seconds=5.0) + cooking_equipment = CookingEquipment( + transitions={}, name="Pot", item_info=item_info + ) + + item_1 = Item( + name="Tomato", item_info=ItemInfo(type=ItemType.Ingredient, name="Tomato") + ) + cooking_equipment.content_list.append(item_1) + + assert cooking_equipment.get_potential_meal() == item_1 + + item_2 = Item( + name="TomatoSoup", item_info=ItemInfo(type=ItemType.Meal, name="TomatoSoup") + ) + cooking_equipment.content_ready = item_2 + assert cooking_equipment.get_potential_meal() == item_2 + + +@pytest.fixture +def cooking_equipment(): + item_info = ItemInfo(type=ItemType.Meal, name="Soup", seconds=5.0) + return CookingEquipment(transitions={}, name="Pot", item_info=item_info) + + +def test_reset(cooking_equipment): + """Test the 'reset' method""" + + cooking_equipment.active_transition = {"1": 2} + cooking_equipment.progress_percentage = 1.0 + cooking_equipment.progress_equipment = "Here" + + cooking_equipment.reset() + + assert cooking_equipment.progress_percentage == 0.0 + assert cooking_equipment.active_transition is None + assert cooking_equipment.progress_equipment is None + + +# TODO full transition test with combine, progress etc. diff --git a/tests/test_counter.py b/tests/test_counter.py new file mode 100644 index 0000000000000000000000000000000000000000..b0fb93a9bdd3f63d4d98f0377da8e9060fe3c88d --- /dev/null +++ b/tests/test_counter.py @@ -0,0 +1,101 @@ +import numpy as np + +from cooperative_cuisine.counters import ServingWindow, Dispenser +from cooperative_cuisine.game_items import Item, Plate, ItemInfo, ItemType +from cooperative_cuisine.hooks import Hooks +from cooperative_cuisine.utils import create_init_env_time + + +def test_serving_window(): + class DummyOrderManager: + def serve_meal(self, item, env_time, player) -> bool: + return (isinstance(item, str) and item == "Test123") or item.content_list[ + 0 + ].name == "TestMeal" + + class DummyPlateDispenser: + plate_received = False + + def update_plate_out_of_kitchen(self, env_time): + self.plate_received = True + + plate_dispenser = DummyPlateDispenser() + + serving_window = ServingWindow( + order_manager=DummyOrderManager(), + meals={"TestMeal", "TestMeal2"}, + env_time_func=create_init_env_time, + plate_dispenser=plate_dispenser, + pos=np.array([1.0, 1.0]), + hook=Hooks(None), + ) + + serving_window.drop_off(item="Test123") + assert ( + plate_dispenser.plate_received + ), "ServingWindow needs to update plate out of kitchen for ordered meal." + plate_dispenser.plate_received = False + plate = Plate(transitions={}, clean=True, item_info=None) + plate.content_list = [Item(name="TestMeal", item_info=None)] + assert serving_window.can_drop_off( + item=plate + ), "ServingWindow could drop off a known meal." + assert ( + serving_window.drop_off(item=plate) is None + ), "ServingWindow drop_off should return None for a served meal." + assert ( + plate_dispenser.plate_received + ), "ServingWindow needs to update plate out of kitchen for ordered meal." + plate_dispenser.plate_received = False + + plate.content_list = [Item(name="TestMeal2", item_info=None)] + assert serving_window.can_drop_off( + item=plate + ), "ServingWindow could drop off a known meal." + assert ( + serving_window.drop_off(item=plate) == plate + ), "ServingWindow should return the item for not ordered meals." + + assert ( + serving_window.pick_up() is None + ), "Player should not be able to pick something from the ServingWindow." + + +def test_dispenser(): + dispenser = Dispenser( + dispensing=ItemInfo( + type=ItemType.Ingredient, + name="MyIngredient", + seconds=0, + needs=["MySecondIngredient"], + equipment=None, + ), + pos=np.array([1.0, 1.0]), + hook=Hooks(None), + undo_dispenser_pickup=False, + ) + assert ( + dispenser.occupied_by.name == "MyIngredient" + ), "Initialized dispenser should be occupied by dispensing item" + assert ( + dispenser.pick_up().name == "MyIngredient" + ), "Picked up item should be the dispensing item" + assert ( + dispenser.occupied_by is not None + ), "After pickup a new occupied by should be generated" + assert ( + dispenser.occupied_by.name == "MyIngredient" + ), "After pick up a new occupied by item should be generated" + + assert not dispenser.can_drop_off( + dispenser.pick_up() + ), "Config undo_dispenser_pickup==False should stop the player to drop off picked up items" + + dispenser.undo_dispenser_pickup = True + assert dispenser.can_drop_off( + dispenser.pick_up() + ), "Config undo_dispenser_pickup==True should allow the player to drop off picked up items" + assert ( + dispenser.drop_off(dispenser.pick_up()) is None + ), "Config undo_dispenser_pickup==True should allow the player to drop off picked up items" + # check combine? diff --git a/tests/test_item.py b/tests/test_item.py new file mode 100644 index 0000000000000000000000000000000000000000..9b3cdb64deb08be2331a1570198dd14c6577b372 --- /dev/null +++ b/tests/test_item.py @@ -0,0 +1,64 @@ +import pytest + +from cooperative_cuisine.game_items import ItemInfo, Item, ItemType + + +@pytest.fixture +def items(): + item1_info = ItemInfo(type=ItemType.Ingredient, name="Tomato") + item1 = Item("Tomato", item1_info) + item2_info = ItemInfo(type=ItemType.Ingredient, name="Onion") + item2 = Item("Onion", item2_info) + item1_duplicate = Item("Tomato", item1_info) + + return item1, item1_info, item1_duplicate, item2, item2_info + + +def test_init(items): + item1, item1_info, _, _, _ = items + assert item1.name == "Tomato" + assert item1.item_info == item1_info + assert item1.progress_equipment is None + assert item1.progress_percentage == 0.0 + + +def test_repr(items): + item1, _, _, _, _ = items + assert str(item1) == "Tomato()" + + +def test_eq(items): + item1, _, item1_duplicate, item2, _ = items + assert item1 == item1_duplicate + assert item1 != item2 + + +def test_can_combine(items): + item1, _, _, item2, _ = items + assert not item1.can_combine(item2) + + +def test_combine(items): + item1, _, _, item2, _ = items + assert item1.combine(item2) is None + + +def test_progress(items): + item1, _, _, _, _ = items + item1.progress_equipment = "Oven" + item1.progress("Oven", 0.2) + assert item1.progress_percentage == 0.2 + item1.progress("Oven", 0.1) + assert item1.progress_percentage == pytest.approx(0.3) + item1.progress("NotOven", 0.2) + assert item1.progress_percentage == pytest.approx(0.3) + assert item1.progress_equipment == "Oven" + + +def test_reset(items): + item1, _, _, _, _ = items + item1.progress_equipment = "Oven" + item1.progress("Oven", 0.2) + item1.reset() + assert item1.progress_equipment is None + assert item1.progress_percentage == 0.0 diff --git a/tests/test_start.py b/tests/test_start.py index eb4746dfb41dc33db2a548ef5b675adfc46e9d7d..e69b1ac95c1590adff21abcaa9291f7352ddb8d6 100644 --- a/tests/test_start.py +++ b/tests/test_start.py @@ -12,7 +12,17 @@ from cooperative_cuisine.environment import ( InterActionData, ) from cooperative_cuisine.game_items import Item, ItemInfo, ItemType +from cooperative_cuisine.game_server import PlayerRequestType from cooperative_cuisine.hooks import Hooks +from cooperative_cuisine.server_results import ( + PlayerInfo, + CreateEnvResult, + PlayerRequestResult, +) +from cooperative_cuisine.state_representation import ( + StateRepresentation, + create_json_schema, +) from cooperative_cuisine.utils import create_init_env_time layouts_folder = ROOT_DIR / "configs" / "layouts" @@ -118,6 +128,10 @@ def test_player_movement_speed(env_config, layout_empty_config, item_info): np.linalg.norm(expected - env.players[player_name].pos), 0 ), "Performed movement do not move the player as expected." + assert StateRepresentation.model_validate_json( + json_data=env.get_json_state(player_id="1") + ), "json state does not match expected StateRepresentation." + def test_player_reach(env_config, layout_empty_config, item_info): env = Environment(env_config, layout_empty_config, item_info, as_files=False) @@ -281,3 +295,18 @@ def test_time_limit(): env.step(passed_time_2) assert env.game_ended, "Game has ended now." + + +def test_json_schema(): + assert isinstance(create_json_schema(), dict) + + +def test_server_result_definition(): + plater_info = PlayerInfo(client_id="123", player_hash="234567890", player_id="0") + CreateEnvResult(env_id="123344", player_info={"0": plater_info}, recipe_graphs=[]) + PlayerRequestResult( + request_type=PlayerRequestType.READY, + status=200, + msg="123", + player_hash="1234324", + )