Worlds and Universes

Universes are the top-level struct that you will be working with in the Arbiter Engine. They are tasked with letting Worlds join in and running those Worlds in parallel. By no means are you required to use Universes, but they will be useful for running multiple simulations at once or, in the future, they will allow for running Worlds that have different internal environments. For instance, one could have a World that consists of Agents acting on the Ethereum mainnet, another World that consists of Agents acting on Optimism, and finally a World that has an Arbiter Environment as the network analogue. Using these in tandem is a long-term goal of the Arbiter project.

Depending on your needs, you will either use the Universe if you want to run multiple Worlds in parallel or you will use the World if you only want to run a single simulation. The choice is yours.

struct Universe

The Universe struct looks like this:

pub struct Universe {
    worlds: Option<HashMap<String, World>>,
    world_tasks: Option<Vec<Result<World, JoinError>>>,
}

The Universe is a struct that wraps a mapping of Worlds where the key of the map is the World's ID. Also, the Universe manages the running of those Worlds in parallel by storing the running Worlds as tasks. In the future, more introspection and control will be added to the Universe to allow for debugging and managing the running Worlds.

The Universe::run_worlds currently iterates through the Worlds and starts them in concurrent tasks.

struct World

The World struct looks like this:

pub struct World {
    pub id: String,
    pub agents: Option<HashMap<String, Agent>>,
    pub environment: Environment,
    pub messager: Messager,
}

The World is a struct that has an ID, an Arbiter Environment, a mapping of Agents, and a Messager. The World is tasked with letting Agents join in, and when they do so, to connect them to the Environment with a client and Messager with the Agent's ID. Then the World stores the Agents in a map where the key is the Agent's ID.

The main methods to use with the world is World::add_agent which adds an agent to the World and World::run which will engage all of the Agent Behaviors.

In future development, the World will be generic over your choice of Provider that encapsulates the Ethereum-like execution environment you want to use (e.g., Ethereum mainnet, Optimism, or an Arbiter Environment).

Example

Let's first do a quick example where we take a World and add an Agent to it.

use arbiter_engine::{agent::Agent, world::World};
use crate::Replier;

fn setup_world(id: &str) -> World {
    let ping_replier = Replier::new("ping", "pong", 5, None);
    let pong_replier = Replier::new("pong", "ping", 5, Some("ping"));
    let agent = Agent::new("my_agent")
                    .with_behavior(ping_replier)
                    .with_behavior(pong_replier);
    let mut world = World::new(id);
    world.add_agent(agent);
}

async fn run() {
    let world = setup_world("my_world");
    world.run().await;
}

If you wanted to extend this to use a Universe, you would simply create a Universe and add the World to it.

use arbiter_engine::{agent::Agent, world::World};
use crate::Replier;

fn setup_world(id: &str) -> World {
    let ping_replier = Replier::new("ping", "pong", 5, None);
    let pong_replier = Replier::new("pong", "ping", 5, Some("ping"));
    let agent = Agent::new("my_agent")
                    .with_behavior(ping_replier)
                    .with_behavior(pong_replier);
    let mut world = World::new(id);
    world.add_agent(agent);
}

fn main() {
    let mut universe = Universe::new();
    universe.add_world(setup_world("my_world"));
    universe.add_world(setup_world("my_other_world"));
    universe.run_worlds().await;
}