diff --git a/lynx/common/actions/common_requirements.py b/lynx/common/actions/common_requirements.py index d20dfc0..9e0a82b 100644 --- a/lynx/common/actions/common_requirements.py +++ b/lynx/common/actions/common_requirements.py @@ -70,3 +70,6 @@ def any_object_on_square_has_all_given_tags(scene: Scene, position: Vector, give return True return False + def has_something_in_inventory(scene: 'Scene', object_id: int) -> bool: + object: Object = scene.get_object_by_id(object_id) + return bool(object.inventory) diff --git a/lynx/common/actions/drop.py b/lynx/common/actions/drop.py new file mode 100644 index 0000000..f7a3bd9 --- /dev/null +++ b/lynx/common/actions/drop.py @@ -0,0 +1,54 @@ +from dataclasses import dataclass + +from lynx.common.actions.action import Action +from lynx.common.actions.common_requirements import CommonRequirements +from lynx.common.actions.create_object import CreateObject +from lynx.common.actions.update_resources import UpdateResources +from lynx.common.object import Object +from lynx.common.vector import Vector + + +@dataclass +class Drop(Action): + """ + Action used to empty inventory of Agent. + If target_position is equal to drop_area_position, we should increase global points and resources. + If target_position is different than drop_area_position then we should create each object from inventory on chosen + square (target_position) + """ + object_id: int = -1 + target_position: Vector = Vector(0, 1) + + def drop_in_drop_area(self, scene: 'Scene', player_name: str, inventory: dict): + scene.update_resources_of_player(player_name, inventory) + update_resources_action = UpdateResources(player_name, inventory) + scene.add_to_pending_actions(update_resources_action.serialize()) + + def drop_in_overworld(self, scene: 'Scene', inventory: dict): + for objects_to_drop in inventory: + for object_to_drop in range(inventory[objects_to_drop]): + object_created = Object(id=scene.generate_id(), + name=objects_to_drop, + tags=['pushable', 'pickable'], + position=self.target_position) + create_action = CreateObject(object_created.serialize()) + scene.add_to_pending_actions(create_action.serialize()) + + + def apply(self, scene: 'Scene') -> None: + agent: Object = scene.get_object_by_id(self.object_id) + player_name = agent.owner + if self.target_position == scene.get_drop_area_of_a_player(player_name): + self.drop_in_drop_area(scene, player_name, agent.inventory) + else: + self.drop_in_overworld(scene, agent.inventory) + + agent.inventory = {} + + def satisfies_requirements(self, scene: 'Scene') -> bool: + agent: Object = scene.get_object_by_id(self.object_id) + + return CommonRequirements.is_in_range(scene, self.object_id, self.target_position, 1) \ + and (CommonRequirements.is_walkable(scene, self.target_position) + or scene.get_drop_area_of_a_player(agent.owner) == self.target_position) \ + and CommonRequirements.has_something_in_inventory(scene, self.object_id) diff --git a/lynx/common/actions/update_resources.py b/lynx/common/actions/update_resources.py new file mode 100644 index 0000000..e4bda01 --- /dev/null +++ b/lynx/common/actions/update_resources.py @@ -0,0 +1,20 @@ +from dataclasses import dataclass +from typing import Dict +from dataclasses import field + +from lynx.common.actions.action import Action + + +@dataclass +class UpdateResources(Action): + """ + Simple action for indicating that we should update resource view in the front-end. + """ + user_name: str = "" + points_updated: Dict[str, int] = field(default_factory=dict) + + def satisfies_requirements(self, scene: 'Scene') -> bool: + return True + + def apply(self, scene: 'Scene') -> None: + pass diff --git a/lynx/common/enitity.py b/lynx/common/enitity.py index 6b74b6e..da49629 100644 --- a/lynx/common/enitity.py +++ b/lynx/common/enitity.py @@ -32,6 +32,9 @@ def deserialize(cls, json_string: str) -> Entity: from lynx.common.actions.remove_object import RemoveObject from lynx.common.actions.print import Print from lynx.common.actions.take import Take + from lynx.common.actions.drop import Drop + from lynx.common.actions.update_resources import UpdateResources + from lynx.common.player import Player exported_entity = cls._Exported.deserialize(json_string) entity_type = locals()[exported_entity.type] diff --git a/lynx/common/player.py b/lynx/common/player.py new file mode 100644 index 0000000..df6b059 --- /dev/null +++ b/lynx/common/player.py @@ -0,0 +1,14 @@ +from typing import Dict, NoReturn, Optional + +from lynx.common.square import Square +from lynx.common.object import * +from lynx.common.serializable import Serializable +from lynx.common.vector import Vector +from lynx.common.serializable import Serializable + +@dataclass +class Player(Serializable): + player_id: str = field(default_factory=str) + player_resources: Dict[str, int] = field(default_factory=dict) + drop_area: Vector = field(default_factory=Vector) + diff --git a/lynx/common/scene.py b/lynx/common/scene.py index e391ba9..1b6769f 100644 --- a/lynx/common/scene.py +++ b/lynx/common/scene.py @@ -5,12 +5,13 @@ from lynx.common.serializable import Serializable from lynx.common.vector import Vector from lynx.common.actions.create_object import CreateObject +from lynx.common.player import Player import random @dataclass class Scene(Serializable): - players: List[str] = field(default_factory=list) + players: List[Player] = field(default_factory=list) entities: List[Entity] = field(default_factory=list) pending_actions: List[str] = field(default_factory=list) # Transformations which occur, during other transformations (e.g. chop -> Create logs) _square_position_map: Dict[Vector, Square] = field(default_factory=dict) @@ -66,11 +67,12 @@ def remove_object(self, object: Object) -> NoReturn: def add_to_pending_actions(self, action: str) -> NoReturn: self.pending_actions.append(action) - def is_player_new(self, player: str) -> bool: - return player not in self.players + def is_player_new(self, player_id: str) -> bool: + players_id = [player.player_id for player in self.players] + return player_id not in players_id def add_player(self, player: str) -> None: - self.players.append(player) + self.players.append(Player(player_id=player, player_resources={"Wood": 0, "Stone": 0}, drop_area=None)) def is_world_created(self) -> bool: return bool(self.entities) @@ -88,3 +90,22 @@ def generate_drop_area(self, player: str) -> None: drop_area = Object(name="DropArea", id=self.generate_id(), position=position, owner=player) create_drop_area = CreateObject(drop_area.serialize()) self.add_to_pending_actions(create_drop_area.serialize()) + player_object = self.get_player(player) + player_object.drop_area = position + + def get_player(self, player_id: str) -> Optional[Player]: + for player in self.players: + if player.player_id == player_id: + return player + return None + + def get_drop_area_of_a_player(self, player_id: str) -> Optional[Vector]: + for player in self.players: + if player.player_id == player_id: + return player.drop_area + return None + + def update_resources_of_player(self, player_id: str, inventory: Dict): + player = self.get_player(player_id) + for object_name, count in inventory.items(): + player.player_resources[object_name] += count diff --git a/tests/drop_test.py b/tests/drop_test.py new file mode 100644 index 0000000..ae9deb5 --- /dev/null +++ b/tests/drop_test.py @@ -0,0 +1,136 @@ +import random +from typing import NoReturn + +from lynx.common.actions.action import Action +from lynx.common.actions.drop import Drop +from lynx.common.object import Object +from lynx.common.player import Player +from lynx.common.scene import Scene +from lynx.common.vector import Vector +from lynx.common.actions.update_resources import UpdateResources + +class TestDropSerialization: + expected_serialization_drop = '{"type": "Drop", "attributes": {"object_id": 1, "target_position": {"x": 1, "y": 0}}}' + + def test_success_serialization(self) -> None: + serialized_drop = Drop(object_id=1, target_position=Vector(1, 0)).serialize() + + assert self.expected_serialization_drop == serialized_drop + + def test_success_deserialization(self) -> None: + expected_drop = Drop(object_id=1, target_position=Vector(1, 0)) + dummy_drop = Drop.deserialize(self.expected_serialization_drop) + + assert dummy_drop == expected_drop + + +class TestDropApply: + def test_drop_single_object_in_overworld_on_tile_sucessful(self) -> None: + random.seed(1222) + expected_scene = Scene(players=[Player(player_id="dummy", player_resources={"Wood": 0, "Stone": 0}, drop_area=Vector(5, 5))]) + expected_dummy_object = Object(id=1, name="dummy", owner="dummy", position=Vector(5, 5)) + expected_dummy_drop = Drop(target_position=Vector(5, 6), object_id=1) + expected_dummy_object2 = Object(id=expected_scene.generate_id(), name="Wood", position=Vector(5, 6), + tags=['pushable', 'pickable']) + expected_scene.add_entity(expected_dummy_object) + expected_scene.add_entity(expected_dummy_drop) + expected_scene.add_entity(expected_dummy_object2) + + random.seed(1222) + scene = Scene(players=[Player(player_id="dummy", player_resources={"Wood": 0, "Stone": 0}, drop_area=Vector(5, 5))]) + dummy_object = Object(id=1, name="dummy", owner="dummy", position=Vector(5, 5), inventory={"Wood": 1}) + dummy_drop = Drop(target_position=Vector(5, 6), object_id=1) + scene.add_entity(dummy_object) + scene.add_entity(dummy_drop) + + dummy_drop.apply(scene) + for action in scene.pending_actions: + Action.deserialize(action).apply(scene) + scene.pending_actions.clear() + + assert scene == expected_scene + + def test_drop_multiple_objects_in_overworld_on_tile_sucessful(self) -> None: + random.seed(1222) + expected_scene = Scene(players=[Player(player_id="dummy", player_resources={"Wood": 0, "Stone": 0}, drop_area=Vector(5, 5))]) + expected_dummy_object = Object(id=1, name="dummy", owner="dummy", position=Vector(5, 5)) + expected_dummy_drop = Drop(target_position=Vector(5, 6), object_id=1) + expected_dummy_object2 = Object(id=expected_scene.generate_id(), name="Wood", position=Vector(5, 6), + tags=['pushable', 'pickable']) + expected_dummy_object3 = Object(id=expected_scene.generate_id(), name="Wood", position=Vector(5, 6), + tags=['pushable', 'pickable']) + expected_dummy_object4 = Object(id=expected_scene.generate_id(), name="Stone", position=Vector(5, 6), + tags=['pushable', 'pickable']) + expected_scene.add_entity(expected_dummy_object) + expected_scene.add_entity(expected_dummy_drop) + expected_scene.add_entity(expected_dummy_object2) + expected_scene.add_entity(expected_dummy_object3) + expected_scene.add_entity(expected_dummy_object4) + + random.seed(1222) + scene = Scene(players=[Player(player_id="dummy", player_resources={"Wood": 0, "Stone": 0}, drop_area=Vector(5, 5))]) + dummy_object = Object(id=1, name="dummy", owner="dummy", position=Vector(5, 5), inventory={"Wood": 2, "Stone": 1}) + dummy_drop = Drop(target_position=Vector(5, 6), object_id=1) + scene.add_entity(dummy_object) + scene.add_entity(dummy_drop) + + dummy_drop.apply(scene) + for action in scene.pending_actions: + Action.deserialize(action).apply(scene) + scene.pending_actions.clear() + + assert scene == expected_scene + + def test_success_drop_to_drop_area_apply(self) -> None: + expected_scene = Scene(players=[Player(player_id="test", player_resources={"Wood": 2, "Stone": 1}, drop_area=Vector(5, 6))]) + expected_dummy_object = Object(id=1, name="dummy", owner="test", position=Vector(5, 5)) + expected_dummy_drop = Drop(target_position=Vector(5, 6), object_id=1) + expected_update_points = UpdateResources(user_name="test", points_updated = {"Wood": 2, "Stone": 1}) + expected_scene.add_entity(expected_dummy_object) + expected_scene.add_entity(expected_dummy_drop) + expected_scene.pending_actions.append(expected_update_points.serialize()) + + scene = Scene(players=[Player(player_id="test", player_resources={"Wood": 0, "Stone": 0}, drop_area=Vector(5, 6))]) + dummy_object = Object(id=1, name="dummy", owner="test", position=Vector(5, 5), inventory={"Wood": 2, "Stone": 1}) + dummy_drop = Drop(target_position=Vector(5, 6), object_id=1) + scene.add_entity(dummy_object) + scene.add_entity(dummy_drop) + + dummy_drop.apply(scene) + + assert scene == expected_scene + + +class TestDropRequirements: + + def test_all_requirements_satisified_positive(self) -> None: + scene = Scene(players=[Player(player_id="dummy", player_resources={"Wood": 0, "Stone": 0}, drop_area=Vector(5, 5))]) + dummy_object = Object(id=1, name="dummy", owner="dummy", position=Vector(5, 5), inventory={"Wood": 1}) + dummy_drop = Drop(target_position=Vector(5, 6), object_id=1) + scene.add_entity(Object(id=3, name="Grass", position=Vector(5, 6), tags=['walkable'])) + scene.add_entity(dummy_object) + assert dummy_drop.satisfies_requirements(scene) is True + + def test_requirements_agent_too_far_fail(self) -> None: + scene = Scene(players=[Player(player_id="dummy", player_resources={"Wood": 0, "Stone": 0}, drop_area=Vector(5, 5))]) + dummy_object = Object(id=1, name="dummy", owner="dummy", position=Vector(5, 5), inventory={"Wood": 1}) + dummy_drop = Drop(target_position=Vector(6, 6), object_id=1) + scene.add_entity(Object(id=3, name="Grass", position=Vector(6, 6), tags=['walkable'])) + scene.add_entity(dummy_object) + assert dummy_drop.satisfies_requirements(scene) is not True + + def test_requirements_empty_inventory_fail(self) -> None: + scene = Scene(players=[Player(player_id="dummy", player_resources={"Wood": 0, "Stone": 0}, drop_area=Vector(5, 5))]) + dummy_object = Object(id=1, name="dummy", owner="dummy", position=Vector(5, 5)) + dummy_drop = Drop(target_position=Vector(5, 6), object_id=1) + scene.add_entity(Object(id=3, name="Grass", position=Vector(5, 6), tags=['walkable'])) + scene.add_entity(dummy_object) + assert dummy_drop.satisfies_requirements(scene) is not True + + def test_requirements_no_walkable_tile_fail(self) -> None: + scene = Scene(players=[Player(player_id="dummy", player_resources={"Wood": 0, "Stone": 0}, drop_area=Vector(5, 5))]) + dummy_object = Object(id=1, name="dummy", owner="dummy", position=Vector(5, 5), inventory={"Wood": 1}) + dummy_drop = Drop(target_position=Vector(5, 6), object_id=1) + scene.add_entity(Object(id=3, name="Grass", position=Vector(5, 6))) + scene.add_entity(dummy_object) + assert dummy_drop.satisfies_requirements(scene) is not True diff --git a/tests/scene_test.py b/tests/scene_test.py index 69d8635..e567970 100644 --- a/tests/scene_test.py +++ b/tests/scene_test.py @@ -13,6 +13,7 @@ class TestSceneSerialization: '"tick": "", "on_death": "", "owner": "", "tags": [], "inventory": {}}}, ' \ '{"type": "Move", "attributes": {"object_id": 456, "direction": {"x": 1, "y": ' \ '0}}}], "pending_actions": []}' + def test_success(self) -> NoReturn: scene = Scene() diff --git a/tests/user_helper_functions_test.py b/tests/user_helper_functions_test.py index 081b3ec..d27b6a0 100644 --- a/tests/user_helper_functions_test.py +++ b/tests/user_helper_functions_test.py @@ -17,7 +17,6 @@ class TestUserHelperFunctions: scene.add_entity(dummy_object2) dummy_object3 = Object(id=4, name="diff_dummy", position=Vector(1, 1)) scene.add_entity(dummy_object3) - random.seed(12) def test_objects_around_success(self) -> NoReturn: assert get_objects_around(1, self.scene, 9) == [3, 4] @@ -50,6 +49,7 @@ def test_filter_objects_failure(self) -> NoReturn: assert filter_objects(self.scene, [1, 2, 3, 4], "empty") != [1] def test_random_direction_success(self) -> NoReturn: + random.seed(12) self.scene.add_entity(Object(id=10, name="Grass", position=Vector(-1, 0), tags=["walkable"])) self.scene.add_entity(Object(id=11, name="Grass", position=Vector(0, 1), tags=["walkable"])) assert random_direction(self.scene, 1) == Vector(-1, 0)