Skip to content

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.

class HeroLifeCycleStage(Enum):
    SAFE = 1
    IN_DANGER = 2
    DECEASED = 3

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