Source code for mesa.experimental.cell_space.cell_agent

"""Agents that understand how to exist in and move through cell spaces.

Provides specialized agent classes that handle cell occupation, movement, and
proper registration:
- CellAgent: Mobile agents that can move between cells
- FixedAgent: Immobile agents permanently fixed to cells
- Grid2DMovingAgent: Agents with grid-specific movement capabilities

These classes ensure consistent agent-cell relationships and proper state management
as agents move through the space. They can be used directly or as examples for
creating custom cell-aware agents.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Protocol

from mesa.agent import Agent

if TYPE_CHECKING:
    from mesa.experimental.cell_space import Cell


class HasCellProtocol(Protocol):
    """Protocol for discrete space cell holders."""

    cell: Cell


class HasCell:
    """Descriptor for cell movement behavior."""

    _mesa_cell: Cell | None = None

    @property
    def cell(self) -> Cell | None:  # noqa: D102
        return self._mesa_cell

    @cell.setter
    def cell(self, cell: Cell | None) -> None:
        # remove from current cell
        if self.cell is not None:
            self.cell.remove_agent(self)

        # update private attribute
        self._mesa_cell = cell

        # add to new cell
        if cell is not None:
            cell.add_agent(self)


class BasicMovement:
    """Mixin for moving agents in discrete space."""

    def move_to(self: HasCellProtocol, cell: Cell) -> None:
        """Move to a new cell."""
        self.cell = cell

    def move_relative(self: HasCellProtocol, direction: tuple[int, ...]):
        """Move to a cell relative to the current cell.

        Args:
            direction: The direction to move in.
        """
        new_cell = self.cell.connections.get(direction)
        if new_cell is not None:
            self.cell = new_cell
        else:
            raise ValueError(f"No cell in direction {direction}")


class FixedCell(HasCell):
    """Mixin for agents that are fixed to a cell."""

    @property
    def cell(self) -> Cell | None:  # noqa: D102
        return self._mesa_cell

    @cell.setter
    def cell(self, cell: Cell) -> None:
        if self.cell is not None:
            raise ValueError("Cannot move agent in FixedCell")
        self._mesa_cell = cell

        cell.add_agent(self)


[docs] class CellAgent(Agent, HasCell, BasicMovement): """Cell Agent is an extension of the Agent class and adds behavior for moving in discrete spaces. Attributes: cell (Cell): The cell the agent is currently in. """
[docs] def remove(self): """Remove the agent from the model.""" super().remove() self.cell = None # ensures that we are also removed from cell
[docs] class FixedAgent(Agent, FixedCell): """A patch in a 2D grid."""
[docs] def remove(self): """Remove the agent from the model.""" super().remove() # fixme we leave self._mesa_cell on the original value # so you cannot hijack remove() to move patches self.cell.remove_agent(self)
[docs] class Grid2DMovingAgent(CellAgent): """Mixin for moving agents in 2D grids.""" # fmt: off DIRECTION_MAP = { "n": (-1, 0), "north": (-1, 0), "up": (-1, 0), "s": (1, 0), "south": (1, 0), "down": (1, 0), "e": (0, 1), "east": (0, 1), "right": (0, 1), "w": (0, -1), "west": (0, -1), "left": (0, -1), "ne": (-1, 1), "northeast": (-1, 1), "upright": (-1, 1), "nw": (-1, -1), "northwest": (-1, -1), "upleft": (-1, -1), "se": (1, 1), "southeast": (1, 1), "downright": (1, 1), "sw": (1, -1), "southwest": (1, -1), "downleft": (1, -1) } # fmt: on
[docs] def move(self, direction: str, distance: int = 1): """Move the agent in a cardinal direction. Args: direction: The cardinal direction to move in. distance: The distance to move. """ direction = direction.lower() # Convert direction to lowercase if direction not in self.DIRECTION_MAP: raise ValueError(f"Invalid direction: {direction}") move_vector = self.DIRECTION_MAP[direction] for _ in range(distance): self.move_relative(move_vector)