diff --git a/mesa/agent.py b/mesa/agent.py index 2f0e842d382..3a70d83bf46 100644 --- a/mesa/agent.py +++ b/mesa/agent.py @@ -10,6 +10,7 @@ import contextlib import copy +import math import operator import warnings import weakref @@ -68,6 +69,8 @@ def remove(self) -> None: """Remove and delete the agent from the model.""" with contextlib.suppress(KeyError): self.model.agents_[type(self)].pop(self) + self.pos: Position | None + self.heading = 90 def step(self) -> None: """A single step of the agent.""" @@ -79,6 +82,73 @@ def advance(self) -> None: def random(self) -> Random: return self.model.random + def move_forward_or_backward(self, amount, sign): + """Does the calculation to find the agent's next move and is used within the forward and backward functions""" + new_x = ( + float(self.pos[0]) + sign * math.cos(self.heading * math.pi / 180) * amount + ) + new_y = ( + float(self.pos[1]) + sign * math.sin(self.heading * math.pi / -180) * amount + ) + next_pos = (new_x, new_y) + + try: + self.model.space.move_agent(self, next_pos) + except AttributeError as exc: + raise AttributeError("Agent's model does not define space") from exc + except Exception as exc: + warnings.warn( + f"agent.py (forward_backwards): could not move agent " + f"{self.unique_id} within self.model.space\n{exc}" + ) + + def move_forward(self, amount=1): + """Moves the agent forward by the amount given""" + self.move_forward_or_backward(amount, 1) + + def move_backward(self, amount=1): + """Moves the agent backwards from where its facing by the given amount""" + self.move_forward_or_backward(amount, -1) + + def turn_right(self, degree=90): + """Turns the agent right by the given degree""" + self.heading = (self.heading - degree) % 360 + + def turn_left(self, degree=90): + """Turns the agent left by the given degree""" + self.heading = (self.heading + degree) % 360 + + def distancexy(self, x, y): + """Gives you the distance of the agent and the given coordinate""" + + return math.dist(self.pos, (x, y)) + + def distance(self, another_agent): + """Gives you the distance between the agent and another agent""" + return self.distancexy(**(another_agent.pos)) + + def towardsxy(self, x, y): + """Calculates angle between a given coordinate and horizon as if the current position is the origin""" + dx = x - float(self.pos[0]) + dy = float(self.pos[1]) - y + if dx == 0: + return 90 if dy > 0 else 270 + else: + return math.degrees(math.atan2(dy, dx)) + + def towards(self, another_agent): + """Calculates angle between an agent and horizon as if the current position is the origin""" + return self.towardsxy(*another_agent.pos) + + def facexy(self, x, y): + """Makes agent face a given coordinate""" + self.heading = self.towardsxy(x, y) + + def face(self, another_agent): + """Makes agent face another agent""" + x, y = another_agent.pos + self.facexy(x, y) + class AgentSet(MutableSet, Sequence): """ diff --git a/tests/test_agent_spatial_methods.py b/tests/test_agent_spatial_methods.py new file mode 100644 index 00000000000..afce6ac1203 --- /dev/null +++ b/tests/test_agent_spatial_methods.py @@ -0,0 +1,72 @@ +import pytest + +from mesa.agent import Agent +from mesa.model import Model +from mesa.space import ContinuousSpace, MultiGrid + + +@pytest.fixture +def agent_in_space(): + model = Model() + model.space = ContinuousSpace(10, 10, torus=True) + agent = Agent(1, model) + agent.pos = (2, 1) + agent.heading = 0 + return agent + + +def test_move_forward(agent_in_space): + agent_in_space.heading = 90 + agent_in_space.move_forward(1) + assert agent_in_space.pos[0] == pytest.approx(2) + assert agent_in_space.pos[1] == pytest.approx(0) + + +def test_turn_right(agent_in_space): + agent_in_space.heading = 0 + agent_in_space.turn_right(60) + assert agent_in_space.heading == 300 + agent_in_space.move_forward(1) + assert agent_in_space.pos[0] == pytest.approx(2.5) + assert agent_in_space.pos[1] == pytest.approx(1.8660254) + + +def test_move_forward_toroid(agent_in_space): + "Verify that toroidal wrapping applies to move_forward" + + agent_in_space.move_forward(10.0) + assert agent_in_space.pos[0] == pytest.approx(2) + assert agent_in_space.pos[1] == pytest.approx(1) + + +def test_move_forward_raises_if_no_space(): + """move_forward only applies for models with ContinuousSpace""" + + model = Model() + model.grid = MultiGrid(10, 10, torus=True) + agent = Agent(1, model) + agent.pos = (2, 1) + with pytest.raises(Exception): + agent.move_forward(10.0) + + +def test_towards(agent_in_space): + agent2 = Agent(2, agent_in_space.model) + agent2.pos = (5, 1) + assert agent_in_space.towards(agent2) == pytest.approx(0) + agent2.pos = (2, 4) + assert agent_in_space.towards(agent2) == pytest.approx(270) + agent2.pos = (5, 4) + assert agent_in_space.towards(agent2) == pytest.approx(-45) + + +def test_facexy(agent_in_space): + agent_in_space.facexy(2, 5) + assert agent_in_space.heading == pytest.approx(270) + + +def test_face(agent_in_space): + agent2 = Agent(2, agent_in_space.model) + agent2.pos = (5, 1) + agent_in_space.face(agent2) + assert agent_in_space.heading == pytest.approx(0)