11from mesa .discrete_space import CellAgent , FixedAgent
2+ from mesa .experimental .mesa_signals import (
3+ Computable ,
4+ Computed ,
5+ ContinuousObservable ,
6+ HasObservables ,
7+ Observable ,
8+ )
29
310
4- class Animal (CellAgent ):
5- """The base animal class."""
11+ class Animal (CellAgent , HasObservables ):
12+ """The base animal class with reactive energy management."""
13+
14+ # Energy depletes continuously over time
15+ energy = ContinuousObservable (
16+ initial_value = 8.0 , rate_func = lambda value , elapsed , agent : - agent .metabolic_rate
17+ )
18+
19+ # Computed property: animal is hungry when energy is low
20+ is_hungry = Computable ()
21+
22+ # Computed property: animal can reproduce when energy is sufficient
23+ can_reproduce = Computable ()
624
725 def __init__ (
826 self , model , energy = 8 , p_reproduce = 0.04 , energy_from_food = 4 , cell = None
@@ -17,14 +35,40 @@ def __init__(
1735 cell: Cell in which the animal starts
1836 """
1937 super ().__init__ (model )
38+
39+ # Set base metabolic rate (energy loss per time unit when idle)
40+ self .metabolic_rate = 0.5
41+
42+ # Initialize energy (triggers continuous depletion)
2043 self .energy = energy
2144 self .p_reproduce = p_reproduce
2245 self .energy_from_food = energy_from_food
2346 self .cell = cell
2447
48+ # Set up computed properties
49+ self .is_hungry = Computed (lambda : self .energy < self .energy_from_food * 2 )
50+ self .can_reproduce = Computed (lambda : self .energy > self .energy_from_food * 4 )
51+
52+ # Register threshold: die when energy reaches zero
53+ self .add_threshold ("energy" , 0.0 , self ._on_energy_depleted )
54+
55+ # Register threshold: become critically hungry at 25% of starting energy
56+ self .add_threshold ("energy" , energy * 0.25 , self ._on_critical_hunger )
57+
58+ def _on_energy_depleted (self , signal ):
59+ """Called when energy crosses zero - animal dies."""
60+ if signal .direction == "down" : # Only trigger on downward crossing
61+ self .remove ()
62+
63+ def _on_critical_hunger (self , signal ):
64+ """Called when energy becomes critically low."""
65+ if signal .direction == "down" :
66+ # Increase metabolic efficiency when starving (survival mode)
67+ self .metabolic_rate *= 0.8
68+
2569 def spawn_offspring (self ):
2670 """Create offspring by splitting energy and creating new instance."""
27- self .energy /= 2
71+ self .energy /= 2 # This updates the continuous observable
2872 self .__class__ (
2973 self .model ,
3074 self .energy ,
@@ -35,33 +79,39 @@ def spawn_offspring(self):
3579
3680 def feed (self ):
3781 """Abstract method to be implemented by subclasses."""
82+ raise NotImplementedError
3883
3984 def step (self ):
4085 """Execute one step of the animal's behavior."""
41- # Move to random neighboring cell
86+ # Move to neighboring cell (uses more energy than standing still)
87+ self .metabolic_rate = 1.0 # Movement costs more energy
4288 self .move ()
4389
44- self .energy -= 1
45-
4690 # Try to feed
4791 self .feed ()
4892
49- # Handle death and reproduction
50- if self .energy < 0 :
51- self .remove ()
52- elif self .random .random () < self .p_reproduce :
93+ # Return to resting metabolic rate
94+ self .metabolic_rate = 0.5
95+
96+ # Reproduce if conditions are met (using computed property)
97+ if self .can_reproduce and self .random .random () < self .p_reproduce :
5398 self .spawn_offspring ()
5499
55100
56101class Sheep (Animal ):
57- """A sheep that walks around, reproduces (asexually) and gets eaten."""
102+ """A sheep that walks around, reproduces and gets eaten.
103+
104+ Sheep prefer cells with grass and avoid wolves. They gain energy by
105+ eating grass, which continuously depletes over time.
106+ """
58107
59108 def feed (self ):
60109 """If possible, eat grass at current location."""
61110 grass_patch = next (
62111 obj for obj in self .cell .agents if isinstance (obj , GrassPatch )
63112 )
64113 if grass_patch .fully_grown :
114+ # Eating gives instant energy boost
65115 self .energy += self .energy_from_food
66116 grass_patch .fully_grown = False
67117
@@ -70,64 +120,82 @@ def move(self):
70120 cells_without_wolves = self .cell .neighborhood .select (
71121 lambda cell : not any (isinstance (obj , Wolf ) for obj in cell .agents )
72122 )
73- # If all surrounding cells have wolves, stay put
123+
124+ # If all surrounding cells have wolves, stay put (fear overrides hunger)
74125 if len (cells_without_wolves ) == 0 :
75126 return
76127
77- # Among safe cells, prefer those with grown grass
78- cells_with_grass = cells_without_wolves .select (
79- lambda cell : any (
80- isinstance (obj , GrassPatch ) and obj .fully_grown for obj in cell .agents
128+ # If critically hungry, prioritize grass over safety
129+ if self .is_hungry : # Using computed property
130+ cells_with_grass = cells_without_wolves .select (
131+ lambda cell : any (
132+ isinstance (obj , GrassPatch ) and obj .fully_grown
133+ for obj in cell .agents
134+ )
81135 )
82- )
83- # Move to a cell with grass if available, otherwise move to any safe cell
84- target_cells = (
85- cells_with_grass if len (cells_with_grass ) > 0 else cells_without_wolves
86- )
136+ # Move to grass if available, otherwise any safe cell
137+ target_cells = (
138+ cells_with_grass if len (cells_with_grass ) > 0 else cells_without_wolves
139+ )
140+ else :
141+ # Not hungry - just avoid wolves
142+ target_cells = cells_without_wolves
143+
87144 self .cell = target_cells .select_random_cell ()
88145
89146
90147class Wolf (Animal ):
91- """A wolf that walks around, reproduces (asexually) and eats sheep."""
148+ """A wolf that walks around, reproduces and eats sheep.
149+
150+ Wolves are more efficient predators, with higher base energy and
151+ metabolic rate. They actively hunt sheep and gain substantial energy
152+ from successful kills.
153+ """
154+
155+ def __init__ (
156+ self , model , energy = 20 , p_reproduce = 0.05 , energy_from_food = 20 , cell = None
157+ ):
158+ """Initialize a wolf with higher energy needs than sheep."""
159+ super ().__init__ (model , energy , p_reproduce , energy_from_food , cell )
160+ # Wolves have higher metabolic rate (they're larger predators)
161+ self .metabolic_rate = 1.0
92162
93163 def feed (self ):
94164 """If possible, eat a sheep at current location."""
95165 sheep = [obj for obj in self .cell .agents if isinstance (obj , Sheep )]
96- if sheep : # If there are any sheep present
166+ if sheep : # Successful hunt
97167 sheep_to_eat = self .random .choice (sheep )
168+ # Eating gives instant energy boost
98169 self .energy += self .energy_from_food
99170 sheep_to_eat .remove ()
100171
101172 def move (self ):
102173 """Move to a neighboring cell, preferably one with sheep."""
103- cells_with_sheep = self .cell .neighborhood .select (
104- lambda cell : any (isinstance (obj , Sheep ) for obj in cell .agents )
105- )
106- target_cells = (
107- cells_with_sheep if len (cells_with_sheep ) > 0 else self .cell .neighborhood
108- )
109- self .cell = target_cells .select_random_cell ()
174+ # When hungry, actively hunt for sheep
175+ if self .is_hungry : # Using computed property
176+ cells_with_sheep = self .cell .neighborhood .select (
177+ lambda cell : any (isinstance (obj , Sheep ) for obj in cell .agents )
178+ )
179+ target_cells = (
180+ cells_with_sheep
181+ if len (cells_with_sheep ) > 0
182+ else self .cell .neighborhood
183+ )
184+ else :
185+ # When not hungry, wander randomly (conserve energy)
186+ target_cells = self .cell .neighborhood
110187
188+ self .cell = target_cells .select_random_cell ()
111189
112- class GrassPatch (FixedAgent ):
113- """A patch of grass that grows at a fixed rate and can be eaten by sheep."""
114190
115- @property
116- def fully_grown (self ):
117- """Whether the grass patch is fully grown."""
118- return self ._fully_grown
191+ class GrassPatch (FixedAgent , HasObservables ):
192+ """A patch of grass that grows at a fixed rate and can be eaten by sheep.
119193
120- @fully_grown .setter
121- def fully_grown (self , value : bool ) -> None :
122- """Set grass growth state and schedule regrowth if eaten."""
123- self ._fully_grown = value
194+ Grass growth is modeled as a continuous process with a fixed regrowth time.
195+ """
124196
125- if not value : # If grass was just eaten
126- self .model .simulator .schedule_event_relative (
127- setattr ,
128- self .grass_regrowth_time ,
129- function_args = [self , "fully_grown" , True ],
130- )
197+ # Observable: grass growth state
198+ fully_grown = Observable ()
131199
132200 def __init__ (self , model , countdown , grass_regrowth_time , cell ):
133201 """Create a new patch of grass.
@@ -139,12 +207,25 @@ def __init__(self, model, countdown, grass_regrowth_time, cell):
139207 cell: Cell to which this grass patch belongs
140208 """
141209 super ().__init__ (model )
142- self ._fully_grown = countdown == 0
210+
211+ self .fully_grown = countdown == 0
143212 self .grass_regrowth_time = grass_regrowth_time
144213 self .cell = cell
145214
215+ # Listen for when grass gets eaten, schedule regrowth
216+ self .observe ("fully_grown" , "change" , self ._on_growth_change )
217+
146218 # Schedule initial growth if not fully grown
147219 if not self .fully_grown :
220+ self .model .simulator .schedule_event_relative (self ._regrow , countdown )
221+
222+ def _on_growth_change (self , signal ):
223+ """React to grass being eaten - schedule regrowth."""
224+ if signal .new is False : # Grass was just eaten
148225 self .model .simulator .schedule_event_relative (
149- setattr , countdown , function_args = [ self , "fully_grown" , True ]
226+ self . _regrow , self . grass_regrowth_time
150227 )
228+
229+ def _regrow (self ):
230+ """Regrow the grass patch."""
231+ self .fully_grown = True
0 commit comments