"""
The gridworld module represents the physical environment simulation grid for
Ethical Gardeners.
This module defines the fundamental structures of the simulated environment
where agents (gardeners) interact with the world. The environment consists of:
1. A 2D grid of cells (:py:class:`Cell`) - Each cell represents a physical
location that can be of different types (:py:class:`CellType`). The cells
have a pollution level that evolves over time, depending on whether they
contain flowers or not.
2. Flowers (:py:class:`Flower`) - Plants that agents can grow in ground cells:
* Different types with unique growth patterns and properties
* Progress through growth stages over time
* Reduce pollution in their cell based on type and growth stage
* Can be harvested for monetary value when fully grown
* Return seeds when harvested that can be used to plant more flowers
3. Agents (:py:class:`.Agent`) - Gardeners that move through and interact with
the environment:
* Can move between cells
* Plant flowers using seeds from their inventory
* Harvest fully grown flowers for monetary value
The GridWorld provides methods to initialize the environment (from file,
randomly, or programmatically), place and manage agents and flowers,
update environmental conditions, and validate agent actions.
"""
from enum import Enum
import copy
import numpy as np
from ethicalgardeners.agent import Agent
from ethicalgardeners.constants import MIN_SEED_RETURNS, MAX_SEED_RETURNS
[docs]
class GridWorld:
"""
Represents the physical grid world environment for the Ethical Gardeners
simulation.
The GridWorld manages a 2D grid of cells. It handles the flowers and agents
and manages their placement within the environment. The grid can be
initialized from a file, randomly generated, or manually configured.
Attributes:
init_method (str): Type of initialization ('from_file', 'random',
'from_code')
init_config (dict): Configuration for initialization. If 'from_file',
this is the file path. If 'from_code', this is the grid
configuration dictionary. If 'random', the obstacle ratio and
number of agents.
width (int): The width of the grid in cells.
height (int): The height of the grid in cells.
min_pollution (float): The minimum level of pollution a cell can have
at any time. The cell pollution decreases over time when flowers
are planted, but cannot go lower than this minimum value.
max_pollution (float): The maximum level of pollution a cell can have
at any time. The cell pollution increases over time when no flowers
are planted, but cannot go higher than this maximum value.
pollution_increment (float): Amount by which pollution increases in
empty cells.
num_seeds_returned (int): Number of seeds returned when harvesting a
flower.
flowers_data (dict): Configuration data for different types of flowers.
collisions_on (bool): Whether agents can occupy the same cell
simultaneously.
grid (list): 2D array of Cell objects representing the environment.
agents (list): List of all Agent objects in the environment.
"""
[docs]
def __init__(self, init_method, init_config, width=10, height=10,
min_pollution=0, max_pollution=100, pollution_increment=1,
num_seeds_returned=1, collisions_on=True,
flowers_data: dict = None, random_generator=None, grid=None,
agents: list = None, flowers: list = None):
"""
Create a new grid world environment.
Args:
init_method (str): Type of initialization ('from_file', 'random',
'from_code')
init_config (dict): Configuration for initialization. If
'from_file', this is the file path. If 'from_code', this is the
grid configuration dictionary. If 'random', the obstacle ratio
and number of agents.
width (int, optional): The width of the grid in cells.
height (int, optional): The height of the grid in cells.
min_pollution (float, optional): Minimum allowed pollution level
for any cell.
max_pollution (float, optional): Maximum allowed pollution level
for any cell.
pollution_increment (float, optional): Amount by which pollution
increases in empty cells.
num_seeds_returned (int, optional): Number of seeds returned when
harvesting a flower. If -1, the system of seeds will be
disabled. If -2, a random number between
:py:const:`.MIN_SEED_RETURNS` and :py:const:`.MAX_SEED_RETURNS`
will be used. If -3, the number of seeds returned will be
randomly determined between :py:const:`.MIN_SEED_RETURNS` and
:py:const:`.MAX_SEED_RETURNS` each time a flower is harvested.
flowers_data (dict, optional): Configuration data for different
types of flowers.
collisions_on (bool, optional): Whether agents can occupy the same
cell simultaneously.
random_generator (:py:class:`numpy.random.RandomState`, optional):
Custom random generator instance for reproducibility. If None,
uses the default random
grid (list, optional): 2D array of Cell objects representing the
environment. If None, initializes an empty grid.
agents (list, optional): List of Agent objects to place in the
grid.
flowers (list, optional): List of tuples representing flowers to
place in the grid. Each tuple should be of the form (position,
flower_type, growth_stage)
"""
self.init_method = init_method
self.init_config = copy.deepcopy(init_config) # avoid the items from
# being mutated
self.width = width
self.height = height
self.min_pollution = min_pollution
self.max_pollution = max_pollution
self.pollution_increment = pollution_increment
self.collisions_on = collisions_on
if flowers_data is None:
flowers_data = {
0: {'price': 10, 'pollution_reduction': [0, 0, 0, 0, 5]},
1: {'price': 5, 'pollution_reduction': [0, 0, 1, 3]},
2: {'price': 2, 'pollution_reduction': [1]}
}
self.flowers_data = flowers_data
self.random_generator = random_generator if (
random_generator is not None) else np.random.RandomState()
if num_seeds_returned == -1:
self.num_seeds_returned = None # Seeds system disabled
elif num_seeds_returned == -2:
self.num_seeds_returned = self.random_generator.randint(
MIN_SEED_RETURNS,
MAX_SEED_RETURNS
)
else:
self.num_seeds_returned = num_seeds_returned
self.grid = grid if grid is not None else [[]]
self.agents = []
# Place agents in the grid
if agents is not None:
for agent in agents:
if not self.valid_position(agent.position):
raise ValueError(
f"Invalid position for agent: {agent.position}")
self.place_agent(agent)
# Place flowers in the grid and add them to the flowers dictionary
if flowers is not None:
for position, flower_type, growth_stage in flowers:
if not self.valid_position(position):
raise ValueError(
f"Invalid position for flower: {position}")
self.place_flower(position, flower_type, None, growth_stage)
[docs]
@classmethod
def init_from_file(cls, init_config, random_generator=None,
min_pollution=0, max_pollution=100,
pollution_increment=1, num_seeds_returned=1,
collisions_on=True):
"""
Initialize the grid from a file.
The file format supports:
- First line: width height
- Grid representation:
- G (ground)
- O (obstacle)
- FX_Y (ground with flower type X at growth stage Y)
- AX (ground with agent ID X)
- Agent definitions: ID,money,seeds
- Flowers_data definition: type,price,pollution_reduction
.. literalinclude:: /examples/grid_config.txt
:language: text
:caption: grid_config.txt
:name: grid_config
:encoding: utf-8
Args:
init_config (dict): Configuration dictionary with the key
"file_path" specifying the path to the initialization file.
random_generator (:py:class:`numpy.random.RandomState`, optional):
Custom random generator instance for reproducibility.
min_pollution (float, optional): Minimum allowed pollution level
for any cell.
max_pollution (float, optional): Maximum allowed pollution level
for any cell.
pollution_increment (float, optional): Amount by which pollution
increases in empty cells.
num_seeds_returned (int, optional): Number of seeds returned when
harvesting a flower.
collisions_on (bool, optional): Whether agents can occupy the same
cell simultaneously.
"""
with open(init_config["file_path"], 'r') as f:
lines = f.readlines()
# Read width and height from the first line
first_line = lines[0].strip().split()
width = int(first_line[0])
height = int(first_line[1])
# Initialize the grid with empty cells
grid = [[None for _ in range(width)] for _ in
range(height)]
# parse the grid
agents_to_create = {}
flowers_to_create = {}
for i in range(height):
cells = lines[i + 1].strip().split()
for j, cell_code in enumerate(cells):
if cell_code == 'G':
grid[i][j] = Cell(CellType.GROUND)
elif cell_code == 'O':
grid[i][j] = Cell(CellType.OBSTACLE)
elif cell_code.startswith('F'):
grid[i][j] = Cell(CellType.GROUND)
flower_info = cell_code[1:].split('_')
flower_type = int(flower_info[0])
growth_stage = int(flower_info[1])
flowers_to_create[(i, j)] = (flower_type, growth_stage)
elif cell_code.startswith('A'):
grid[i][j] = Cell(CellType.GROUND)
agent_id = int(cell_code[1:])
agents_to_create[agent_id] = (i, j)
# Create agents
agents = []
agent_def_start = height + 1
agent_def_lines = lines[agent_def_start:
agent_def_start + len(agents_to_create)]
for line in agent_def_lines:
agent_data = line.strip().split(',')
agent_id = int(agent_data[0])
position = agents_to_create[agent_id]
money = float(agent_data[1])
seed_counts = list(map(int, agent_data[2].split('|')))
seeds = {i: count for i, count in enumerate(seed_counts)}
agent = Agent(position, money, seeds)
agents.append(agent)
# Create flowers_data
flowers_data = {}
flower_def_start = height + 1 + len(agents_to_create)
flower_def_lines = lines[flower_def_start:]
for line in flower_def_lines:
flower_data = line.strip().split(',')
flower_type = int(flower_data[0])
price = int(flower_data[1])
pollution_reduction = list(map(float, flower_data[2].split('|')))
flowers_data[flower_type] = {
'price': price,
'pollution_reduction': pollution_reduction
}
# Place flowers with their growth stage
flowers = []
for position, (flower_type, growth_stage) in flowers_to_create.items():
flowers.append((position, flower_type, growth_stage))
return cls("from_file", init_config, width, height,
min_pollution, max_pollution, pollution_increment,
num_seeds_returned, collisions_on, flowers_data,
random_generator, grid, agents, flowers)
[docs]
@classmethod
def init_random(cls, init_config=None, width=10, height=10,
min_pollution=0, max_pollution=100, pollution_increment=1,
num_seeds_returned=1, collisions_on=True,
flowers_data: dict = None, random_generator=None):
"""
Initialize a random grid with obstacles and agents.
Args:
init_config (dict, optional): Configuration dictionary with the
keys "obstacles_ratio" (float between 0 and 1) and "nb_agent"
(int) specifying the ratio of obstacle cells and the number of
agents to place in the grid. Defaults to
{"obstacles_ratio": 0.2, "nb_agent": 1}
width (int, optional): Width of the grid
height (int, optional): Height of the grid
min_pollution (float, optional): Minimum allowed pollution level
for any cell.
max_pollution (float, optional): Maximum allowed pollution level
for any cell.
pollution_increment (float, optional): Amount by which pollution
increases in empty cells.
num_seeds_returned (int, optional): Number of seeds returned when
harvesting a flower.
collisions_on (bool, optional): Whether agents can occupy the same
cell simultaneously.
flowers_data (dict, optional): Configuration data for different
types of flowers.
random_generator (:py:class:`numpy.random.RandomState`, optional):
Custom random generator instance for reproducibility. If None,
uses the default random generator.
Raises:
ValueError: If there are not enough valid positions for the
specified number of agents after placing obstacles.
"""
if init_config is None:
init_config = {"obstacles_ratio": 0.2, "nb_agent": 1}
random_generator = random_generator if (
random_generator is not None) else np.random.RandomState()
# Initialize grid with ground cells
grid = [[Cell(CellType.GROUND) for _ in range(width)] for _ in
range(height)]
# Create a list of all possible positions
valid_positions = [(i, j) for i in range(height) for j in
range(width)]
# Place obstacles randomly
indices = np.arange(len(valid_positions)) # choice needs indices
num_obstacles = int(init_config["obstacles_ratio"] * width * height)
selected_indices = random_generator.choice(indices,
num_obstacles,
replace=False)
obstacle_positions = [valid_positions[i] for i in selected_indices]
for pos in obstacle_positions:
i, j = pos
grid[i][j] = Cell(CellType.OBSTACLE)
valid_positions.remove(pos)
if len(valid_positions) < init_config["nb_agent"]:
raise ValueError(
f"Not enough valid positions for {init_config['nb_agent']}"
f" agents")
indices = np.arange(len(valid_positions))
selected_indices = random_generator.choice(indices,
init_config["nb_agent"],
replace=False)
agent_positions = [valid_positions[i] for i in selected_indices]
agents = []
for i in range(init_config["nb_agent"]):
# Create agent with default values
agent = Agent(agent_positions[i])
agents.append(agent)
return cls("random", init_config, width, height,
min_pollution, max_pollution, pollution_increment,
num_seeds_returned, collisions_on, flowers_data,
random_generator, grid, agents)
[docs]
@classmethod
def init_from_code(cls, init_config=None, random_generator=None,
width=10, height=10, min_pollution=0, max_pollution=100,
pollution_increment=1, num_seeds_returned=1,
collisions_on=True):
"""
Initialize the grid directly from code using a configuration
dictionary.
This method allows programmatic grid initialization for testing and
debugging without having to create external files.
Args:
init_config (dict, optional): Configuration dictionary with the key
"grid_config" specifying the grid configuration. The grid
configuration dictionary should have the following structure:
.. code-block:: python
{
'width': int, # Width of the grid
'height': int, # Height of the grid
'cells': [ # List of special cells (other than GROUND)
{'position': (row, col), 'type': 'OBSTACLE'},
],
'min_pollution': float, # Minimum pollution level
'max_pollution': float, # Maximum pollution level
'pollution_increment': float, # Pollution increment
'num_seeds_returned': int, # Number of seeds returned
# when harvesting a flower
'collisions_on': bool, # Whether agents can occupy the
# same cell
'flowers_data': { # Optional: custom flower data
int: {'price': float,
'pollution_reduction': [float, ...]},
},
'agents': [ # List of agents to create (optional:
# money and seeds)
{'position': (row, col), 'money': float,
'seeds': {0:int, 1:int, ...}},
],
'flowers': [ # List of flowers to create (optional:
# growth stage)
{'position': (row, col), 'type': int,
'growth_stage': int},
]
}
width (int, optional): Width of the grid.
height (int, optional): Height of the grid.
min_pollution (float, optional): Minimum allowed pollution level
for any cell.
max_pollution (float, optional): Maximum allowed pollution level
for any cell.
pollution_increment (float, optional): Amount by which pollution
increases in empty cells.
num_seeds_returned (int, optional): Number of seeds returned when
harvesting a flower.
collisions_on (bool, optional): Whether agents can occupy the same
cell simultaneously.
random_generator (:py:class:`numpy.random.RandomState`, optional):
Custom random generator instance for reproducibility.
"""
grid_config = {}
if init_config is not None and "grid_config" in init_config:
grid_config = init_config["grid_config"]
# Set grid dimensions from the configuration
width = grid_config.get('width', width)
height = grid_config.get('height', height)
# Get pollution limits from the configuration or use defaults
min_pollution = grid_config.get('min_pollution',
min_pollution)
max_pollution = grid_config.get('max_pollution',
max_pollution)
# Get pollution increment from the configuration or use default
pollution_increment = grid_config.get('pollution_increment',
pollution_increment)
# Get number of seeds returned from the configuration or use default
num_seeds_returned = grid_config.get('num_seeds_returned',
num_seeds_returned)
# Get collisions setting from the configuration or use default
collisions_on = grid_config.get('collisions_on',
collisions_on)
# Get flowers data from the configuration or use default
flowers_data = grid_config.get('flowers_data',
None)
# Initialize grid with ground cells
grid = [[Cell(CellType.GROUND) for _ in range(width)] for _ in
range(height)]
# Place special cells (obstacles, ...) based on the configuration
for cell_info in grid_config.get('cells', []):
position = cell_info['position']
cell_type_str = cell_info['type']
# Convert string type to CellType enum
cell_type = CellType[cell_type_str.upper()]
grid[position[0]][position[1]] = Cell(cell_type)
# Create and place agents
agents = []
for agent_info in grid_config.get('agents', []):
position = agent_info['position']
money = agent_info.get('money', 0)
seeds = agent_info.get('seeds', {0: 10, 1: 10, 2: 10})
agent = Agent(position, money, seeds)
agents.append(agent)
# Create and place flowers
flowers = []
for flower_info in grid_config.get('flowers', []):
position = flower_info['position']
flower_type = flower_info['type']
growth_stage = flower_info.get('growth_stage', 0)
flowers.append((position, flower_type, growth_stage))
return cls("from_code", init_config, width=width,
height=height, min_pollution=min_pollution,
max_pollution=max_pollution,
pollution_increment=pollution_increment,
num_seeds_returned=num_seeds_returned,
collisions_on=collisions_on,
flowers_data=flowers_data, grid=grid,
agents=agents, flowers=flowers,
random_generator=random_generator)
[docs]
@classmethod
def create_from_config(cls, init_method: str, init_config=None,
random_generator=None, width=10, height=10,
min_pollution=0, max_pollution=100,
pollution_increment=1, num_seeds_returned=1,
collisions_on=True, flowers_data: dict = None):
"""
Create a GridWorld instance based on the specified initialization
method and configuration.
Args:
init_method (str): Type of initialization ('from_file', 'random',
'from_code')
init_config (dict): Configuration for initialization. If
'from_file', this is the file path. If 'from_code', this is the
grid configuration dictionary. If 'random', the obstacle ratio
and number of agents.
random_generator (:py:class:`numpy.random.RandomState`, optional):
Custom random generator instance for reproducibility.
width (int, optional): Width of the grid (used for "random"
initialization).
height (int, optional): Height of the grid (used for "random"
initialization).
min_pollution (float, optional): Minimum allowed pollution level
for any cell.
max_pollution (float, optional): Maximum allowed pollution level
for any cell.
pollution_increment (float, optional): Amount by which pollution
increases in empty cells.
num_seeds_returned (int, optional): Number of seeds returned when
harvesting a flower.
collisions_on (bool, optional): Whether agents can occupy the same
cell simultaneously.
flowers_data (dict, optional): Configuration data for different
types of flowers.
"""
if init_method == "from_file":
return cls.init_from_file(
init_config=init_config,
random_generator=random_generator,
min_pollution=min_pollution,
max_pollution=max_pollution,
pollution_increment=pollution_increment,
collisions_on=collisions_on,
num_seeds_returned=num_seeds_returned,
)
elif init_method == "from_code":
return cls.init_from_code(
init_config=init_config,
random_generator=random_generator,
min_pollution=min_pollution,
max_pollution=max_pollution,
pollution_increment=pollution_increment,
collisions_on=collisions_on,
num_seeds_returned=num_seeds_returned,
)
elif init_method == "random":
return cls.init_random(
init_config=init_config,
width=width,
height=height,
min_pollution=min_pollution,
max_pollution=max_pollution,
pollution_increment=pollution_increment,
collisions_on=collisions_on,
num_seeds_returned=num_seeds_returned,
random_generator=random_generator,
flowers_data=flowers_data
)
else:
# Default
return cls.init_random(
random_generator=random_generator,
min_pollution=min_pollution,
max_pollution=max_pollution,
pollution_increment=pollution_increment,
collisions_on=collisions_on,
num_seeds_returned=num_seeds_returned,
flowers_data=flowers_data
)
[docs]
def reset(self, random_generator=None):
"""
Reset the grid world to its initial configuration.
Warning: This method uses a special approach to reset the instance by
creating a new instance and copying its state with an access to
self.__dict__. It should work in most cases but take care if you
have a special case such as an attribute not being in __dict__.
Args:
random_generator (:py:class:`numpy.random.RandomState`, optional):
Custom random generator instance for reproducibility. If None,
uses the same random generator as the current instance.
"""
new = self.create_from_config(
init_method=self.init_method,
init_config=self.init_config,
random_generator=(random_generator
if random_generator is not None
else self.random_generator),
width=self.width,
height=self.height,
min_pollution=self.min_pollution,
max_pollution=self.max_pollution,
pollution_increment=self.pollution_increment,
num_seeds_returned=self.num_seeds_returned,
collisions_on=self.collisions_on,
flowers_data=self.flowers_data,
)
# Replace this object's state with the new one.
self.__dict__.update(new.__dict__)
return self
[docs]
def place_agent(self, agent: Agent):
"""
Place an agent in the grid at its current position.
Args:
agent (Agent): The agent to place in the grid.
Raises:
ValueError: If the agent's position is invalid or already occupied
and collisions are not allowed.
"""
if not self.valid_position(agent.position):
raise ValueError("Invalid position for agent.")
cell = self.get_cell(agent.position)
if cell.has_agent() and not self.collisions_on:
raise ValueError("Cannot place agent in an occupied cell without "
"collisions enabled.")
cell.agent = agent
self.agents.append(agent)
[docs]
def place_flower(self, position, flower_type: int, agent: Agent = None,
growth_stage=0):
"""
Place a flower in the grid at its specified position.
Args:
position (tuple): The (x, y) coordinates where the flower will be
planted.
flower_type (int): The type of flower to plant.
agent (Agent, optional): The agent planting the flower.
growth_stage (int, optional): The initial growth stage of the
flower (default is 0).
Raises:
ValueError: If the flower's position is invalid or if the cell
already contains a flower.
"""
if not self.valid_position(position):
raise ValueError("Invalid position for flower.")
cell = self.get_cell(position)
if cell.has_flower():
raise ValueError("Cannot place flower in a cell that already has "
"a flower.")
cell.flower = Flower(position, flower_type, self.flowers_data, agent,
growth_stage)
[docs]
def remove_flower(self, position):
"""
Removes a flower from the specified position in the grid.
Args:
position (tuple): The (x, y) coordinates of the flower to remove.
Raises:
ValueError: If there is no flower at the specified position.
"""
cell = self.get_cell(position)
if not cell.has_flower():
raise ValueError("Cannot remove flower from a cell that does not "
"have a flower.")
cell.flower = None
[docs]
def update_cell(self):
"""
Updates the pollution and flowers of all cells in the grid.
For each cell, if it contains a flower, pollution decreases by the
flower's pollution reduction value and make the flower grow. If it does
not contain a flower, pollution increases by the pollution increment
value.
"""
for row in self.grid:
for cell in row:
# Update pollution level
cell.update_pollution(self.min_pollution, self.max_pollution)
# If the cell has a flower, make it grow
if cell.has_flower():
cell.flower.grow()
[docs]
def valid_position(self, position):
"""
Checks if a position is valid for an agent to move to.
A position is valid if:
1. It is within the grid boundaries
2. It is not an obstacle cell
Args:
position (tuple): The (x, y) coordinates to check.
Returns:
bool: True if the position is valid, False otherwise.
"""
if 0 <= position[0] < self.height and 0 <= position[1] < self.width:
if self.get_cell(position).can_walk_on():
return True
else:
return False
else:
return False
[docs]
def valid_move(self, new_position):
"""
Checks if an agent can move to a new position based on the action.
A move is valid if:
1. The new position is valid.
2. If collisions are enabled, the new position is not occupied by
another agent.
Args:
new_position (tuple): The new (x, y) coordinates of the agent after
moving.
Returns:
bool: True if the move is valid, False otherwise.
"""
# Check if the cell is occupied by another agent
if not self.valid_position(new_position):
return False
if self.collisions_on:
if self.get_cell(new_position).has_agent():
return False
return True
[docs]
def get_cell(self, position):
"""
Gets the cell at the specified position.
Args:
position (tuple): The (x, y) coordinates of the cell to retrieve.
Returns:
Cell: The cell at the specified position.
"""
return self.grid[position[0]][position[1]]
[docs]
def copy(self):
"""
Create a deep copy of the GridWorld instance.
Returns:
GridWorld: A new instance of GridWorld with the same properties.
"""
return copy.deepcopy(self)
[docs]
class CellType(Enum):
"""
Enum representing the possible types of cells in the grid world.
Attributes:
GROUND: A normal cell where agents can walk, plant and harvest flowers.
OBSTACLE: An impassable cell that agents cannot traverse or interact
with.
"""
GROUND = 0
OBSTACLE = 1
[docs]
class Cell:
"""
Represents a single cell in the grid world.
It can be of different types (:py:class:`CellType`). Some types can
contain a flower (:py:class:`Flower`) and an agent (:py:class:`.Agent`).
It can have a pollution level that evolves over time to a speed defined by
:py:attr:`pollution_increment`.
Attributes:
cell_type (CellType): Type of the cell (ground, obstacle).
flower (Flower): The flower present in this cell, if any.
agent (Agent): The agent currently occupying this cell, if any.
pollution (float): Current pollution level of the cell, if applicable.
pollution_increment (float): Amount by which pollution increases each
step if no flower is in the cell.
"""
[docs]
def __init__(self, cell_type, pollution=50, pollution_increment=1):
"""
Create a new cell.
Args:
cell_type (CellType): The type of cell to create.
pollution (float, optional): Initial pollution level of the cell.
Defaults to 50 for ground cells, None for obstacles.
pollution_increment (float, optional): Amount by which pollution
increases each step if no flower is in the cell. Defaults to 1.
"""
self.cell_type = cell_type
self.flower = None
self.agent = None
if cell_type == CellType.GROUND:
self.pollution = pollution
elif cell_type == CellType.OBSTACLE:
self.pollution = None
self.pollution_increment = pollution_increment
[docs]
def update_pollution(self, min_pollution, max_pollution):
"""
Update the pollution level of the cell based on its current state.
For ground cells, if the cell contains a flower, its pollution
decreases by the flower's pollution reduction value, down to the
minimum pollution level. If the cell does not contain a flower, its
pollution increases by the pollution increment, up to the maximum
pollution level.
Args:
min_pollution (float): Minimum pollution level allowed.
max_pollution (float): Maximum pollution level allowed.
"""
if self.pollution is None:
return
if self.has_flower():
self.pollution = max(
self.pollution - self.flower.get_pollution_reduction(),
min_pollution
)
else:
self.pollution = min(
self.pollution + self.pollution_increment,
max_pollution
)
[docs]
def can_walk_on(self):
"""
Check if agents can walk on this cell.
Returns:
bool: True if agents can walk on this cell, False otherwise.
"""
return self.cell_type == CellType.GROUND
[docs]
def can_plant_on(self):
"""
Check if a flower can be planted in this cell.
Returns:
bool: True if a flower can be planted in this cell, False
otherwise.
"""
return self.cell_type == CellType.GROUND and not self.has_flower()
[docs]
def has_flower(self):
"""
Check if the cell contains a flower.
Returns:
bool: True if the cell contains a flower, False otherwise.
"""
return self.flower is not None
[docs]
def has_agent(self):
"""
Check if the cell is occupied by an agent.
Returns:
bool: True if the cell is occupied by an agent, False otherwise.
"""
return self.agent is not None
[docs]
class Flower:
"""
Represents a flower that can be planted and harvested in the environment.
Flowers grow through several stages and reduce pollution in their cell.
Different flower types have different growth patterns, prices, and
pollution reduction capabilities.
Attributes:
position (tuple): The (x, y) coordinates of the flower in the grid.
flower_type (int): The type of flower, determining its growth and
pollution reduction.
price (float): The monetary value of the flower when harvested.
pollution_reduction (list): List of pollution reduction values for each
growth stage.
num_growth_stage (int): Total number of growth stages for this flower.
current_growth_stage (int): Current growth stage of the flower,
starting at 0.
planted_by (Agent, optional): The agent who planted the flower. Can be
None if the flower was initially present in the environment.
"""
[docs]
def __init__(self, position, flower_type, flowers_data: dict,
agent: Agent = None, growth_stage=0):
"""
Create a new flower.
Args:
position (tuple): The (x, y) coordinates where the flower is
planted.
flower_type (int): The type of flower to create.
flowers_data (dict): Configuration data for flower types;
a dictionary mapping flower type IDs to a dictionary of
properties, containing ``keys`` and ``pollution_reduction``.
agent (Agent, optional): The agent who planted the flower.
growth_stage (int, optional): The number of growth stages for
this flower. Defaults to 0 (the initial stage).
"""
self.position = position
self.flower_type = flower_type
self.price = flowers_data[flower_type]['price']
self.pollution_reduction = (
flowers_data)[flower_type]["pollution_reduction"]
self.num_growth_stage = len(self.pollution_reduction) - 1
self.current_growth_stage = growth_stage
self.planted_by = agent
[docs]
def grow(self):
"""
Advance the flower to the next growth stage if not fully grown.
By default, the flower grows 1 stage at each time step, up to the
maximum stage defined for this flower type.
"""
if self.current_growth_stage < self.num_growth_stage:
self.current_growth_stage += 1
[docs]
def is_grown(self):
"""
Check if the flower has reached its final growth stage.
Returns:
bool: True if the flower is fully grown, False otherwise.
"""
return self.current_growth_stage == self.num_growth_stage
[docs]
def get_pollution_reduction(self):
"""
Return the current pollution reduction provided by the flower.
The pollution reduction depends on the current growth stage and the
flower type.
Returns:
float: The amount of pollution reduced by this flower at its
current stage.
"""
return self.pollution_reduction[self.current_growth_stage]