A Simple Simulation
Here we are going to build a simple simulation where we simulate Zeus sending lightning bolts and Poseidon creating storms, both potentially affecting the life of Odysseus
Imports¶
import asyncio
from enum import Enum
from hades import Event, Hades, NotificationResponse, Process, RandomProcess, SimulationStarted
Defining our Events¶
Let's begin by defining some events we want to exist. Lets have one for Zeus throwing lightning at a target, one for Poseidon causing a storm near a target, one for Athena Intervening to help someone, and one for Odysseus dying.
class LightningBoltThrown(Event):
target_id: str
class StormCreated(Event):
target_id: str
class OdysseusDied(Event):
pass
class AthenaIntervened(Event):
target_id: str
Adding the God Processes¶
Okay now we have our events we need some processes to actually do stuff with them. Let's start by defining some simple ones for Zeus and Poseidon to simply use their powers on Odysseus at intervals. To do this they can
react to the builtin SimulationStarted
event.
class Zeus(Process):
async def notify(self, event: Event):
match event:
case SimulationStarted(t=t):
for i in range(0, 100, 25):
self.add_event(LightningBoltThrown(t=t + i + 2, target_id="Odysseus"))
return NotificationResponse.ACK
return NotificationResponse.NO_ACK
class Poseidon(Process):
async def notify(self, event: Event):
match event:
case SimulationStarted(t=t):
for i in range(0, 100, 5):
self.add_event(StormCreated(t=t + i + 2, target_id="Odysseus"))
return NotificationResponse.ACK
return NotificationResponse.NO_ACK
Adding Odysseus' Process¶
Now lets do this for our hero Odysseus. We want to make it so that Odysseus will take a random amount of damage when he is the target of a LightningBoltThrown
or StormCreated
, if his health points are depleted we will set his state
to deceased using an Enum.
Finally if AthenaIntervened
his health will be restored and he will be SAFE
.
class Odysseus(RandomProcess):
def __init__(self, seed):
super().__init__(seed)
self.status = HeroLifeCycleStage.SAFE
self._health = 100
@property
def instance_identifier(self) -> str:
return "Odysseus"
def _handle_peril(self, t: int, max_damage: int, source: str):
self.status = HeroLifeCycleStage.IN_DANGER
print(f"odysseus is in danger from {source}!")
lost_hp = round(self.random.random() * max_damage)
self._health = max(self._health - lost_hp, 0)
print(f"odysseus' health dropped to {self._health}")
if self._health == 0:
print("odysseus died")
self.status = HeroLifeCycleStage.DECEASED
self.add_event(OdysseusDied(t=t))
async def notify(self, event: Event):
match event:
case LightningBoltThrown(t=t, target_id=target_id):
if self.status == HeroLifeCycleStage.DECEASED:
return NotificationResponse.ACK_BUT_IGNORED
self._handle_peril(t, 90, "Zeus' lightning bolt")
return NotificationResponse.ACK
case StormCreated(t=t, target_id=target_id):
if target_id != self.instance_identifier or self.status == HeroLifeCycleStage.DECEASED:
return NotificationResponse.ACK_BUT_IGNORED
self._handle_peril(t, 50, "Poseidon's storm")
return NotificationResponse.ACK
case AthenaIntervened(t=t, target_id=target_id):
print("but athena intervened saving and healing odysseus to 100")
self._health = 100
self.status = HeroLifeCycleStage.SAFE
return NotificationResponse.ACK
return NotificationResponse.NO_ACK
Adding Athena's Process¶
Last lets add the crucial Athena process. Let's have her do two things. Firstly, similar to Poseidon
and Zeus
lets make her have some predefined time to act. At t=3
say where she will, no matter what, intervene.
Secondly, whenever OddyseusDied
she will have a 50%
chance of intervening. Note that this will happen on the same timestep! See API Reference > Hades for more on how this works!
class GoddessAthena(RandomProcess):
async def notify(self, event: Event):
match event:
case SimulationStarted(t=t):
self.add_event(AthenaIntervened(t=t + 3, target_id="Odysseus"))
return NotificationResponse.ACK
case OdysseusDied(t=t):
if self.random.random() > 0.5:
self.add_event(AthenaIntervened(t=t, target_id="Odysseus"))
else:
print("athena was too late to save odysseus")
return NotificationResponse.ACK
return NotificationResponse.NO_ACK
Putting it all together¶
Finally we want to actually run these processes together
async def odyssey():
world = Hades()
world.register_process(Zeus())
world.register_process(Poseidon())
world.register_process(Odysseus("pomegranate"))
world.register_process(GoddessAthena("pomegranate"))
await world.run()
Note how we instantiate Odysseus
and Athena
with a random seed to ensure every time we run this we get the same result. They inherit from RandomProcess
to do this.
We could also vary this over multiple runs to have an idea of how long `Odysseus' adventure lasts on average.
Simulation Output¶
Running the above we get the following output:
odysseus is in danger from Zeus' lightning bolt!
odysseus' health dropped to 77
odysseus is in danger from Poseidon's storm!
odysseus' health dropped to 63
but athena intervened saving and healing odysseus to 100
odysseus is in danger from Poseidon's storm!
odysseus' health dropped to 78
odysseus is in danger from Poseidon's storm!
odysseus' health dropped to 65
odysseus is in danger from Poseidon's storm!
odysseus' health dropped to 42
odysseus is in danger from Poseidon's storm!
odysseus' health dropped to 42
odysseus is in danger from Zeus' lightning bolt!
odysseus' health dropped to 30
odysseus is in danger from Poseidon's storm!
odysseus' health dropped to 0
odysseus died
athena was too late to save odysseus