├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .vscode ├── settings.json └── templates │ ├── py.lict │ └── rs.lict ├── AUTHORS.txt ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── images └── learn_wood_collected_over_epochs.png ├── npc-engine-core ├── AUTHORS.txt ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── benches │ └── mcts.rs ├── examples │ ├── capture │ │ ├── behavior │ │ │ ├── agent.rs │ │ │ ├── mod.rs │ │ │ └── world.rs │ │ ├── constants.rs │ │ ├── domain.rs │ │ ├── main.rs │ │ ├── map.rs │ │ ├── state.rs │ │ └── task │ │ │ ├── capturing.rs │ │ │ ├── mod.rs │ │ │ ├── moving.rs │ │ │ ├── pick.rs │ │ │ ├── shoot.rs │ │ │ └── world.rs │ ├── ecosystem │ │ ├── behavior │ │ │ ├── animal.rs │ │ │ ├── carnivore.rs │ │ │ ├── herbivore.rs │ │ │ ├── mod.rs │ │ │ └── world.rs │ │ ├── constants.rs │ │ ├── domain.rs │ │ ├── main.rs │ │ ├── map.rs │ │ ├── plot_ecosystem_stats.py │ │ ├── state.rs │ │ └── task │ │ │ ├── eat_grass.rs │ │ │ ├── eat_herbivore.rs │ │ │ ├── jump.rs │ │ │ ├── mod.rs │ │ │ ├── move.rs │ │ │ └── world.rs │ ├── learn │ │ ├── behavior.rs │ │ ├── constants.rs │ │ ├── domain.rs │ │ ├── estimator.rs │ │ ├── main.rs │ │ ├── plot.py │ │ ├── state.rs │ │ └── task │ │ │ ├── collect.rs │ │ │ ├── left.rs │ │ │ ├── mod.rs │ │ │ └── right.rs │ └── tic-tac-toe │ │ ├── board.rs │ │ ├── domain.rs │ │ ├── main.rs │ │ ├── move.rs │ │ └── player.rs ├── images │ └── learn_wood_collected_over_epochs.png ├── src │ ├── active_task.rs │ ├── behavior.rs │ ├── config.rs │ ├── context.rs │ ├── domain.rs │ ├── edge.rs │ ├── lib.rs │ ├── mcts.rs │ ├── node.rs │ ├── state_diff.rs │ ├── task.rs │ └── util.rs └── tests │ ├── branching_tests.rs │ ├── sanity_tests.rs │ ├── seeding_tests.rs │ └── value_tests.rs ├── npc-engine-utils ├── AUTHORS.txt ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── images │ └── learn_wood_collected_over_epochs.png └── src │ ├── coord2d.rs │ ├── direction.rs │ ├── executor.rs │ ├── functional.rs │ ├── global_domain.rs │ ├── graphs.rs │ ├── lib.rs │ ├── neuron.rs │ └── option_state_diff.rs └── scenario-lumberjacks ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── assets ├── ImpassableRock.png ├── OrangeDown.png ├── OrangeDownBarrier.png ├── OrangeDownChopping.png ├── OrangeLeft.png ├── OrangeLeftBarrier.png ├── OrangeLeftChopping.png ├── OrangeRight.png ├── OrangeRightBarrier.png ├── OrangeRightChopping.png ├── OrangeTop.png ├── OrangeTopBarrier.png ├── OrangeTopChopping.png ├── Tree1_3.png ├── Tree2_3.png ├── Tree3_3.png ├── TreeSapling.png ├── Well.png ├── WoodenBarrier.png ├── YellowDown.png ├── YellowDownBarrier.png ├── YellowDownChopping.png ├── YellowLeft.png ├── YellowLeftBarrier.png ├── YellowLeftChopping.png ├── YellowRight.png ├── YellowRightBarrier.png ├── YellowRightChopping.png ├── YellowTop.png ├── YellowTopBarrier.png └── YellowTopChopping.png ├── build.rs ├── config.schema.json ├── configs └── corridor.json ├── experiment.schema.json ├── experiments ├── barrier │ ├── base.json │ ├── experiment.json │ ├── map.png │ └── plot.json ├── base.json ├── competition-basic │ ├── base.json │ ├── experiment.json │ ├── map.png │ └── plot.json ├── config.schema.json ├── corridor.json ├── depth-choice │ ├── base.json │ ├── experiment.json │ ├── map.png │ └── plot.json ├── depth │ ├── base.json │ ├── experiment.json │ ├── map.png │ └── plot.json ├── experiment.schema.json ├── optimization │ ├── base.json │ ├── experiment.json │ ├── map.png │ └── plot.json ├── schema.json ├── specialization │ ├── base.json │ ├── experiment.json │ ├── map.png │ └── plot.json ├── tasks │ ├── base.json │ ├── experiment.json │ ├── map-1.png │ ├── map-3.png │ ├── map-5.png │ ├── map-7.png │ └── plot.json ├── teamwork-basic │ ├── base.json │ ├── experiment.json │ ├── map.png │ └── plot.json ├── visit-count-large │ ├── base.json │ ├── experiment.json │ ├── map.png │ └── plot.json └── visit-count-small │ ├── base.json │ ├── experiment.json │ ├── map.png │ └── plot.json ├── maps ├── corridor.png ├── corridor1.png ├── corridor2.png ├── corridor3.png ├── teamwork.png └── thief.png ├── schema.json └── src ├── behaviors ├── human.rs ├── lumberjack.rs └── mod.rs ├── bin ├── experiment.rs └── lumberjacks.rs ├── config.rs ├── fitnesses.rs ├── game.rs ├── graph.rs ├── heatmap.rs ├── hooks.rs ├── inventory.rs ├── lib.rs ├── lumberjacks_domain.rs ├── metrics ├── agency.rs ├── features.rs ├── fluctuation.rs ├── islands.rs ├── mod.rs └── performance.rs ├── screenshot.rs ├── serialization.rs ├── tasks ├── barrier.rs ├── chop.rs ├── mod.rs ├── move.rs ├── plant.rs ├── refill.rs ├── wait.rs └── water.rs ├── tilemap.rs ├── util.rs └── world.rs /.gitattributes: -------------------------------------------------------------------------------- 1 | *.png filter=lfs diff=lfs merge=lfs -text 2 | 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: CI 4 | 5 | jobs: 6 | check: 7 | name: Check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout sources 11 | uses: actions/checkout@v2 12 | with: 13 | lfs: 'true' 14 | 15 | - name: Install system dev dependencies 16 | run: sudo apt install -y libudev-dev libasound2-dev 17 | 18 | - name: Install stable toolchain 19 | uses: actions-rs/toolchain@v1 20 | with: 21 | profile: minimal 22 | toolchain: stable 23 | override: true 24 | 25 | - name: Run cargo check 26 | uses: actions-rs/cargo@v1 27 | with: 28 | command: check 29 | 30 | test: 31 | name: Test Suite 32 | runs-on: ubuntu-latest 33 | steps: 34 | - name: Checkout sources 35 | uses: actions/checkout@v2 36 | with: 37 | lfs: 'true' 38 | 39 | - name: Install system dev dependencies 40 | run: sudo apt install -y libudev-dev libasound2-dev 41 | 42 | - name: Install stable toolchain 43 | uses: actions-rs/toolchain@v1 44 | with: 45 | profile: minimal 46 | toolchain: stable 47 | override: true 48 | 49 | - name: Run cargo test 50 | uses: actions-rs/cargo@v1 51 | with: 52 | command: test 53 | args: --all-targets 54 | 55 | lints: 56 | name: Lints 57 | runs-on: ubuntu-latest 58 | steps: 59 | - name: Checkout sources 60 | uses: actions/checkout@v2 61 | with: 62 | lfs: 'true' 63 | 64 | - name: Install system dev dependencies 65 | run: sudo apt install -y libudev-dev libasound2-dev 66 | 67 | - name: Install stable toolchain 68 | uses: actions-rs/toolchain@v1 69 | with: 70 | profile: minimal 71 | toolchain: stable 72 | override: true 73 | components: rustfmt, clippy 74 | 75 | - name: Run cargo fmt 76 | uses: actions-rs/cargo@v1 77 | with: 78 | command: fmt 79 | args: --all -- --check 80 | 81 | - name: Run cargo clippy 82 | uses: actions-rs/cargo@v1 83 | with: 84 | command: clippy 85 | args: -- -D warnings 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "medkit", 4 | "nonmax", 5 | "RELU", 6 | "backpropagate", 7 | "backpropagation", 8 | "btreemap", 9 | "btreemaps", 10 | "chacha", 11 | "Colour", 12 | "dedup", 13 | "graphviz", 14 | "hasher", 15 | "hotspot", 16 | "hotspots", 17 | "mcts", 18 | "powi", 19 | "respawn", 20 | "rollout", 21 | "rollouts", 22 | "seedable", 23 | "Snapshotable", 24 | "Srgb" 25 | ], 26 | "license-header-manager.excludeFolders": [ 27 | "target" 28 | ], 29 | "license-header-manager.excludeExtensions": [ 30 | ".py" 31 | ], 32 | "license-header-manager.additionalCommentStyles": [{ 33 | "extension": ".rs", 34 | "commentStart": "/*", 35 | "commentMiddle": " * ", 36 | "commentEnd": " */" 37 | }] 38 | } 39 | -------------------------------------------------------------------------------- /.vscode/templates/py.lict: -------------------------------------------------------------------------------- 1 | SPDX-License-Identifier: Apache-2.0 OR MIT 2 | © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details -------------------------------------------------------------------------------- /.vscode/templates/rs.lict: -------------------------------------------------------------------------------- 1 | SPDX-License-Identifier: Apache-2.0 OR MIT 2 | © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details -------------------------------------------------------------------------------- /AUTHORS.txt: -------------------------------------------------------------------------------- 1 | Note: due to manual merge, not all authors (especially Sven Knobloch and 2 | David Enderlin) appear in the git log. Their contributions are listed below: 3 | 4 | * Sven Knobloch : core MCTS implementation 5 | * David Enderlin : multi-threaded planning and execution 6 | * Aydin Faraji : testing and bug fixing 7 | * Henry Raymond : documentation, supervision 8 | * Stéphane Magnenat : lead, supervision, refactoring, examples 9 | * Violaine Fayolle : lumberjack sprites -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "npc-engine-core", 4 | "npc-engine-utils", 5 | "scenario-lumberjacks" 6 | ] 7 | resolver = "2" 8 | 9 | [profile.bench] 10 | debug = true 11 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020-2022 NPC engine contributors. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /images/learn_wood_collected_over_epochs.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:50254c5fa3bcb630a7f6ba1df1ae9f0b8bc5beaec37ffc69dee7f42c45b2964f 3 | size 62712 4 | -------------------------------------------------------------------------------- /npc-engine-core/AUTHORS.txt: -------------------------------------------------------------------------------- 1 | ../AUTHORS.txt -------------------------------------------------------------------------------- /npc-engine-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "npc-engine-core" 3 | version = "0.1.0" 4 | authors = ["Sven Knobloch ", "David Enderlin ", "Aydin Faraji ", "Stéphane Magnenat "] 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | description = "The core of the NPC engine, providing a generic MCTS framework." 8 | repository = "https://github.com/ethz-gtc/npc-engine" 9 | homepage = "https://github.com/ethz-gtc/npc-engine" 10 | readme = "README.md" 11 | keywords = ["MCTS", "AI", "multi-agent", "simulation", "game"] 12 | categories = ["algorithms", "science", "simulation", "game-development"] 13 | rust-version = "1.62" 14 | 15 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 16 | 17 | [dependencies] 18 | downcast-rs = "1.2.0" 19 | serde = { version = "1", features = [ "derive" ] } 20 | rand = "0.8" 21 | rand_chacha = "0.3" 22 | log = "0.4" 23 | ordered-float = "3" 24 | dot = { version = "0.1", optional = true } 25 | palette = { version = "0.5", optional = true } 26 | rustc-hash = "2.1.1" 27 | 28 | [dev-dependencies] 29 | npc-engine-utils = { path = "../npc-engine-utils" } 30 | env_logger = "0.9.0" 31 | cached = "0.30" 32 | bounded-integer = { version = "0.5.1", features = [ "types" ] } 33 | regex = "1" 34 | nonmax = "0.5" 35 | lazy_static = "1.4" 36 | ansi_term = "0.12" 37 | clearscreen = "1.0.10" 38 | num-traits = { version = "0.2.1", default-features = false } 39 | criterion = "0.5" 40 | 41 | [features] 42 | default = [] 43 | graphviz = [ "dot", "palette" ] 44 | 45 | [[example]] 46 | name = "tic-tac-toe" 47 | required-features = ["graphviz"] 48 | 49 | [[bench]] 50 | name = "mcts" 51 | harness = false 52 | -------------------------------------------------------------------------------- /npc-engine-core/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /npc-engine-core/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /npc-engine-core/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /npc-engine-core/examples/capture/behavior/agent.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use npc_engine_core::{AgentId, Behavior, Context, IdleTask, Task}; 7 | use npc_engine_utils::OptionDiffDomain; 8 | 9 | use crate::{ 10 | constants::MAP, 11 | domain::CaptureDomain, 12 | task::{capturing::StartCapturing, moving::StartMoving, pick::Pick, shoot::Shoot}, 13 | }; 14 | 15 | use super::world::WORLD_AGENT_ID; 16 | 17 | pub struct AgentBehavior; 18 | impl Behavior for AgentBehavior { 19 | fn add_own_tasks( 20 | &self, 21 | ctx: Context, 22 | tasks: &mut Vec>>, 23 | ) { 24 | let state = CaptureDomain::get_cur_state(ctx.state_diff); 25 | let agent_state = state.agents.get(&ctx.agent); 26 | if let Some(agent_state) = agent_state { 27 | // already moving, cannot do anything else 28 | if agent_state.next_location.is_some() { 29 | return; 30 | } 31 | tasks.push(Box::new(IdleTask)); 32 | for to in MAP.neighbors(agent_state.cur_or_last_location) { 33 | let task = StartMoving { to }; 34 | tasks.push(Box::new(task)); 35 | } 36 | let other_agent = if ctx.agent.0 == 0 { 37 | AgentId(1) 38 | } else { 39 | AgentId(0) 40 | }; 41 | let other_tasks: Vec>> = 42 | vec![Box::new(Pick), Box::new(Shoot(other_agent))]; 43 | for task in other_tasks { 44 | if task.is_valid(ctx) { 45 | tasks.push(task); 46 | } 47 | } 48 | for capture_index in 0..MAP.capture_locations_count() { 49 | let task = StartCapturing(capture_index); 50 | if task.is_valid(ctx) { 51 | tasks.push(Box::new(task)); 52 | } 53 | } 54 | } 55 | } 56 | 57 | fn is_valid(&self, ctx: Context) -> bool { 58 | ctx.agent != WORLD_AGENT_ID 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /npc-engine-core/examples/capture/behavior/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | pub mod agent; 7 | pub mod world; 8 | -------------------------------------------------------------------------------- /npc-engine-core/examples/capture/behavior/world.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use npc_engine_core::{AgentId, Behavior, Context, Task}; 7 | 8 | use crate::{domain::CaptureDomain, task::world::WorldStep}; 9 | 10 | pub const WORLD_AGENT_ID: AgentId = AgentId(9); 11 | 12 | pub struct WorldBehavior; 13 | impl Behavior for WorldBehavior { 14 | fn add_own_tasks( 15 | &self, 16 | _ctx: Context, 17 | tasks: &mut Vec>>, 18 | ) { 19 | tasks.push(Box::new(WorldStep)); 20 | } 21 | 22 | fn is_valid(&self, ctx: Context) -> bool { 23 | ctx.agent == WORLD_AGENT_ID 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /npc-engine-core/examples/capture/constants.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use std::collections::HashMap; 7 | 8 | use npc_engine_core::TaskDuration; 9 | 10 | use crate::map::{Location, Map}; 11 | 12 | lazy_static! { 13 | pub static ref MAP: Map = { 14 | let links_data = [ 15 | (0, 1, 2), 16 | (0, 3, 2), 17 | (1, 2, 2), 18 | (1, 4, 1), 19 | (2, 5, 2), 20 | (3, 4, 1), 21 | (4, 5, 1), 22 | (3, 6, 2), 23 | (5, 6, 2) 24 | ]; 25 | let capture_locations = [ 26 | 0, 2, 6 27 | ]; 28 | let mut links = HashMap::>::new(); 29 | for (start, end, length) in links_data { 30 | let start = Location::new(start); 31 | let end = Location::new(end); 32 | // add bi-directional link 33 | links.entry(start) 34 | .or_default() 35 | .insert(end, length); 36 | links.entry(end) 37 | .or_default() 38 | .insert(start, length); 39 | } 40 | let capture_locations = capture_locations 41 | .map(Location::new 42 | ) 43 | .into(); 44 | Map { links, capture_locations } 45 | }; 46 | } 47 | 48 | pub const MAX_HP: u8 = 2; 49 | pub const MAX_AMMO: u8 = 4; 50 | pub const CAPTURE_DURATION: TaskDuration = 1; 51 | pub const RESPAWN_AMMO_DURATION: u8 = 2; 52 | pub const RESPAWN_MEDKIT_DURATION: u8 = 17; 53 | -------------------------------------------------------------------------------- /npc-engine-core/examples/capture/domain.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use std::{collections::BTreeSet, fmt}; 7 | 8 | use npc_engine_core::{AgentId, AgentValue, Behavior, Context, Domain, StateDiffRef}; 9 | use npc_engine_utils::OptionDiffDomain; 10 | use num_traits::Zero; 11 | 12 | use crate::{ 13 | behavior::{ 14 | agent::AgentBehavior, 15 | world::{WorldBehavior, WORLD_AGENT_ID}, 16 | }, 17 | constants::MAP, 18 | map::Location, 19 | state::{Diff, State}, 20 | }; 21 | 22 | pub enum DisplayAction { 23 | Wait, 24 | Pick, 25 | Shoot(AgentId), 26 | StartCapturing(Location), 27 | Capturing(Location), 28 | StartMoving(Location), 29 | Moving(Location), 30 | WorldStep, 31 | } 32 | 33 | impl Default for DisplayAction { 34 | fn default() -> Self { 35 | Self::Wait 36 | } 37 | } 38 | 39 | impl fmt::Debug for DisplayAction { 40 | fn fmt(&self, f: &'_ mut fmt::Formatter) -> fmt::Result { 41 | match &self { 42 | Self::Wait => f.write_str("Wait"), 43 | Self::Pick => f.write_str("Pick"), 44 | Self::Shoot(target) => f.write_fmt(format_args!("Shoot {:?}", target)), 45 | Self::StartCapturing(loc) => f.write_fmt(format_args!("StartCapturing {:?}", loc)), 46 | Self::Capturing(loc) => f.write_fmt(format_args!("Capturing {:?}", loc)), 47 | Self::StartMoving(loc) => f.write_fmt(format_args!("StartMoving {:?}", loc)), 48 | Self::Moving(loc) => f.write_fmt(format_args!("Moving {:?}", loc)), 49 | Self::WorldStep => f.write_str("WorldStep"), 50 | } 51 | } 52 | } 53 | 54 | pub struct CaptureDomain; 55 | 56 | impl Domain for CaptureDomain { 57 | type State = State; 58 | type Diff = Diff; 59 | type DisplayAction = DisplayAction; 60 | 61 | fn list_behaviors() -> &'static [&'static dyn Behavior] { 62 | &[&AgentBehavior, &WorldBehavior] 63 | } 64 | 65 | fn get_current_value(_tick: u64, state_diff: StateDiffRef, agent: AgentId) -> AgentValue { 66 | let state = Self::get_cur_state(state_diff); 67 | state 68 | .agents 69 | .get(&agent) 70 | .map_or(AgentValue::zero(), |agent_state| { 71 | AgentValue::from(agent_state.acc_capture) 72 | }) 73 | } 74 | 75 | fn update_visible_agents(_start_tick: u64, ctx: Context, agents: &mut BTreeSet) { 76 | let state = Self::get_cur_state(ctx.state_diff); 77 | agents.clear(); 78 | agents.extend(state.agents.keys()); 79 | agents.insert(WORLD_AGENT_ID); 80 | } 81 | 82 | fn get_state_description(state_diff: StateDiffRef) -> String { 83 | let state = Self::get_cur_state(state_diff); 84 | let mut s = format!( 85 | "World: ❤️ {} ({}), • {} ({}), ⚡: ", 86 | state.medkit, state.medkit_tick, state.ammo, state.ammo_tick 87 | ); 88 | s += &(0..MAP.capture_locations_count()) 89 | .map(|index| format!("{:?}", state.capture_points[index as usize])) 90 | .collect::>() 91 | .join(" "); 92 | for (id, state) in &state.agents { 93 | if let Some(target) = state.next_location { 94 | s += &format!( 95 | "\nA{} in {}-{}, ❤️ {}, • {}, ⚡{}", 96 | id.0, 97 | state.cur_or_last_location.get(), 98 | target.get(), 99 | state.hp, 100 | state.ammo, 101 | state.acc_capture 102 | ); 103 | } else { 104 | s += &format!( 105 | "\nA{} @ {}, ❤️ {}, • {}, ⚡{}", 106 | id.0, 107 | state.cur_or_last_location.get(), 108 | state.hp, 109 | state.ammo, 110 | state.acc_capture 111 | ); 112 | } 113 | } 114 | s 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /npc-engine-core/examples/capture/main.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use std::{collections::BTreeMap, iter}; 7 | 8 | use behavior::world::WORLD_AGENT_ID; 9 | use constants::MAX_HP; 10 | use domain::CaptureDomain; 11 | use map::Location; 12 | use npc_engine_core::{ 13 | graphviz, ActiveTask, ActiveTasks, AgentId, IdleTask, MCTSConfiguration, MCTS, 14 | }; 15 | use npc_engine_utils::{ 16 | plot_tree_in_tmp_with_task_name, run_simple_executor, ExecutorState, ExecutorStateLocal, 17 | }; 18 | use state::{AgentState, CapturePointState, State}; 19 | use task::world::WorldStep; 20 | 21 | #[macro_use] 22 | extern crate lazy_static; 23 | 24 | mod behavior; 25 | mod constants; 26 | mod domain; 27 | mod map; 28 | mod state; 29 | mod task; 30 | 31 | struct CaptureGameExecutorState; 32 | impl ExecutorStateLocal for CaptureGameExecutorState { 33 | fn create_initial_state(&self) -> State { 34 | let agent0_id = AgentId(0); 35 | let agent0_state = AgentState { 36 | acc_capture: 0, 37 | cur_or_last_location: Location::new(0), 38 | next_location: None, 39 | hp: MAX_HP, 40 | ammo: 0, //MAX_AMMO, 41 | }; 42 | let agent1_id = AgentId(1); 43 | let agent1_state = AgentState { 44 | acc_capture: 0, 45 | cur_or_last_location: Location::new(6), 46 | next_location: None, 47 | hp: MAX_HP, 48 | ammo: 0, //MAX_AMMO, 49 | }; 50 | State { 51 | agents: BTreeMap::from([(agent0_id, agent0_state), (agent1_id, agent1_state)]), 52 | capture_points: [ 53 | CapturePointState::Free, 54 | CapturePointState::Free, 55 | CapturePointState::Free, 56 | ], 57 | ammo: 1, 58 | ammo_tick: 0, 59 | medkit: 1, 60 | medkit_tick: 0, 61 | } 62 | } 63 | 64 | fn init_task_queue(&self, state: &State) -> ActiveTasks { 65 | state 66 | .agents 67 | .iter() 68 | .map(|(id, _)| ActiveTask::new_with_end(0, 0, *id, Box::new(IdleTask))) 69 | .chain(iter::once(ActiveTask::new_with_end( 70 | 0, 71 | 0, 72 | WORLD_AGENT_ID, 73 | Box::new(WorldStep), 74 | ))) 75 | .collect() 76 | } 77 | 78 | fn keep_agent(&self, _tick: u64, state: &State, agent: AgentId) -> bool { 79 | agent == WORLD_AGENT_ID || state.agents.contains_key(&agent) 80 | } 81 | } 82 | 83 | impl ExecutorState for CaptureGameExecutorState { 84 | fn post_mcts_run_hook( 85 | &mut self, 86 | mcts: &MCTS, 87 | last_active_task: &ActiveTask, 88 | ) { 89 | if let Err(e) = plot_tree_in_tmp_with_task_name(mcts, "capture_graphs", last_active_task) { 90 | println!("Cannot write search tree: {e}"); 91 | } 92 | } 93 | } 94 | 95 | fn main() { 96 | // These parameters control the MCTS algorithm. 97 | const CONFIG: MCTSConfiguration = MCTSConfiguration { 98 | allow_invalid_tasks: true, 99 | visits: 5000, 100 | depth: 50, 101 | exploration: 1.414, 102 | discount_hl: 17., 103 | seed: None, 104 | planning_task_duration: None, 105 | }; 106 | 107 | // Set the depth of graph output to 7. 108 | graphviz::set_graph_output_depth(7); 109 | 110 | // Configure the long to just write its content and enable the info level. 111 | use std::io::Write; 112 | env_logger::builder() 113 | .format(|buf, record| writeln!(buf, "{}", record.args())) 114 | .filter(None, log::LevelFilter::Info) 115 | .init(); 116 | 117 | // State of the execution. 118 | let mut executor_state = CaptureGameExecutorState; 119 | 120 | // Run the execution. 121 | run_simple_executor::(&CONFIG, &mut executor_state); 122 | } 123 | -------------------------------------------------------------------------------- /npc-engine-core/examples/capture/map.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use std::collections::HashMap; 7 | 8 | use nonmax::NonMaxU8; 9 | 10 | #[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)] 11 | pub struct Location(NonMaxU8); 12 | impl Location { 13 | pub fn new(location: u8) -> Self { 14 | Self(NonMaxU8::new(location).unwrap()) 15 | } 16 | pub fn get(&self) -> NonMaxU8 { 17 | self.0 18 | } 19 | } 20 | 21 | pub struct Map { 22 | pub links: HashMap>, 23 | pub capture_locations: Vec, 24 | } 25 | impl Map { 26 | pub fn ammo_location(&self) -> Location { 27 | Location::new(1) 28 | } 29 | pub fn medkit_location(&self) -> Location { 30 | Location::new(5) 31 | } 32 | pub fn path_len(&self, from: Location, to: Location) -> Option { 33 | self.links 34 | .get(&from) 35 | .and_then(|ends| ends.get(&to).copied()) 36 | } 37 | pub fn neighbors(&self, from: Location) -> Vec { 38 | self.links 39 | .get(&from) 40 | .map_or(Vec::new(), |ends| ends.keys().copied().collect()) 41 | } 42 | pub fn is_path(&self, from: Location, to: Location) -> bool { 43 | self.path_len(from, to).is_some() 44 | } 45 | pub fn capture_location(&self, index: u8) -> Location { 46 | self.capture_locations[index as usize] 47 | } 48 | pub fn capture_locations_count(&self) -> u8 { 49 | self.capture_locations.len() as u8 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /npc-engine-core/examples/capture/state.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use std::{collections::BTreeMap, fmt}; 7 | 8 | use npc_engine_core::AgentId; 9 | 10 | use crate::map::Location; 11 | 12 | #[derive(Clone, Hash, PartialEq, Eq, Debug)] 13 | pub struct AgentState { 14 | /// accumulated capture points for this agent 15 | pub acc_capture: u16, 16 | /// current or last location of the agent (if travelling), 17 | pub cur_or_last_location: Location, 18 | /// next location of the agent, if travelling, none otherwise 19 | pub next_location: Option, 20 | /// health point of the agent 21 | pub hp: u8, 22 | /// ammunition carried by the agent 23 | pub ammo: u8, 24 | } 25 | 26 | #[derive(Clone, Hash, PartialEq, Eq)] 27 | pub enum CapturePointState { 28 | Free, 29 | Capturing(AgentId), 30 | Captured(AgentId), 31 | } 32 | impl fmt::Debug for CapturePointState { 33 | fn fmt(&self, f: &'_ mut fmt::Formatter) -> fmt::Result { 34 | match &self { 35 | CapturePointState::Free => f.write_str("__"), 36 | CapturePointState::Capturing(agent) => f.write_fmt(format_args!("C{:?}", agent.0)), 37 | CapturePointState::Captured(agent) => f.write_fmt(format_args!("H{:?}", agent.0)), 38 | } 39 | } 40 | } 41 | 42 | #[derive(Clone, Hash, PartialEq, Eq, Debug)] 43 | pub struct State { 44 | /// active agents 45 | pub agents: BTreeMap, 46 | /// capture points 47 | pub capture_points: [CapturePointState; 3], 48 | /// ammo available at collection point 49 | pub ammo: u8, 50 | /// tick when ammo was collected 51 | pub ammo_tick: u8, 52 | /// medical kit available at collection point 53 | pub medkit: u8, 54 | /// tick when med kit was collected 55 | pub medkit_tick: u8, 56 | } 57 | 58 | pub type Diff = Option; // if Some, use this diff, otherwise use initial state 59 | -------------------------------------------------------------------------------- /npc-engine-core/examples/capture/task/capturing.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use npc_engine_core::{impl_task_boxed_methods, Context, ContextMut, Task, TaskDuration}; 7 | use npc_engine_utils::OptionDiffDomain; 8 | 9 | use crate::{ 10 | constants::{CAPTURE_DURATION, MAP}, 11 | domain::{CaptureDomain, DisplayAction}, 12 | state::CapturePointState, 13 | }; 14 | 15 | #[derive(Clone, Hash, PartialEq, Eq, Debug)] 16 | pub struct StartCapturing(pub u8); 17 | impl Task for StartCapturing { 18 | fn duration(&self, _ctx: Context) -> TaskDuration { 19 | // StartCapture is instantaneous 20 | 0 21 | } 22 | 23 | fn execute(&self, ctx: ContextMut) -> Option>> { 24 | let diff = CaptureDomain::get_cur_state_mut(ctx.state_diff); 25 | diff.capture_points[self.0 as usize] = CapturePointState::Capturing(ctx.agent); 26 | Some(Box::new(Capturing(self.0))) 27 | } 28 | 29 | fn display_action(&self) -> DisplayAction { 30 | DisplayAction::StartCapturing(MAP.capture_location(self.0)) 31 | } 32 | 33 | fn is_valid(&self, ctx: Context) -> bool { 34 | let state = CaptureDomain::get_cur_state(ctx.state_diff); 35 | // if the point is already captured, we cannot restart capturing 36 | if state.capture_points[self.0 as usize] == CapturePointState::Captured(ctx.agent) { 37 | return false; 38 | } 39 | let capture_location = MAP.capture_location(self.0); 40 | state.agents.get(&ctx.agent).map_or(false, |agent_state| 41 | // agent is at the right location and not moving 42 | agent_state.cur_or_last_location == capture_location && 43 | agent_state.next_location.is_none()) 44 | } 45 | 46 | impl_task_boxed_methods!(CaptureDomain); 47 | } 48 | 49 | #[derive(Clone, Hash, PartialEq, Eq, Debug)] 50 | pub struct Capturing(u8); 51 | impl Task for Capturing { 52 | fn duration(&self, _ctx: Context) -> TaskDuration { 53 | // Capturing takes some time 54 | CAPTURE_DURATION 55 | } 56 | 57 | fn execute(&self, ctx: ContextMut) -> Option>> { 58 | let diff = CaptureDomain::get_cur_state_mut(ctx.state_diff); 59 | diff.capture_points[self.0 as usize] = CapturePointState::Captured(ctx.agent); 60 | None 61 | } 62 | 63 | fn display_action(&self) -> DisplayAction { 64 | DisplayAction::Capturing(MAP.capture_location(self.0)) 65 | } 66 | 67 | fn is_valid(&self, ctx: Context) -> bool { 68 | let state = CaptureDomain::get_cur_state(ctx.state_diff); 69 | state.agents.get(&ctx.agent).is_some() 70 | && state.capture_points[self.0 as usize] == CapturePointState::Capturing(ctx.agent) 71 | // note: no need to check agent location, as this task is always a follow-up of StartCapturing 72 | } 73 | 74 | impl_task_boxed_methods!(CaptureDomain); 75 | } 76 | -------------------------------------------------------------------------------- /npc-engine-core/examples/capture/task/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | pub mod capturing; 7 | pub mod moving; 8 | pub mod pick; 9 | pub mod shoot; 10 | pub mod world; 11 | -------------------------------------------------------------------------------- /npc-engine-core/examples/capture/task/moving.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use npc_engine_core::{impl_task_boxed_methods, Context, ContextMut, Task, TaskDuration}; 7 | use npc_engine_utils::OptionDiffDomain; 8 | 9 | use crate::{ 10 | constants::MAP, 11 | domain::{CaptureDomain, DisplayAction}, 12 | map::Location, 13 | }; 14 | 15 | #[derive(Clone, Hash, PartialEq, Eq, Debug)] 16 | pub struct StartMoving { 17 | pub to: Location, 18 | } 19 | impl Task for StartMoving { 20 | fn duration(&self, __ctx: Context) -> TaskDuration { 21 | // Start moving is instantaneous 22 | 0 23 | } 24 | 25 | fn execute(&self, ctx: ContextMut) -> Option>> { 26 | let diff = CaptureDomain::get_cur_state_mut(ctx.state_diff); 27 | let agent_state = diff.agents.get_mut(&ctx.agent).unwrap(); 28 | let to = self.to; 29 | agent_state.next_location = Some(to); 30 | // After starting, the agent must complete the move. 31 | let from = agent_state.cur_or_last_location; 32 | Some(Box::new(Moving { from, to })) 33 | } 34 | 35 | fn display_action(&self) -> DisplayAction { 36 | DisplayAction::StartMoving(self.to) 37 | } 38 | 39 | fn is_valid(&self, ctx: Context) -> bool { 40 | let state = CaptureDomain::get_cur_state(ctx.state_diff); 41 | state.agents.get(&ctx.agent).map_or(false, |agent_state| { 42 | let location = agent_state.cur_or_last_location; 43 | // We must be at a location (i.e. not moving). 44 | agent_state.next_location.is_none() && 45 | // There must be a path to target. 46 | MAP.is_path(location, self.to) 47 | }) 48 | } 49 | 50 | impl_task_boxed_methods!(CaptureDomain); 51 | } 52 | 53 | #[derive(Clone, Hash, PartialEq, Eq, Debug)] 54 | pub struct Moving { 55 | from: Location, 56 | to: Location, 57 | } 58 | impl Task for Moving { 59 | fn duration(&self, _ctx: Context) -> TaskDuration { 60 | MAP.path_len(self.from, self.to).unwrap() 61 | } 62 | 63 | fn execute(&self, ctx: ContextMut) -> Option>> { 64 | let diff = CaptureDomain::get_cur_state_mut(ctx.state_diff); 65 | let agent_state = diff.agents.get_mut(&ctx.agent).unwrap(); 66 | agent_state.cur_or_last_location = self.to; 67 | agent_state.next_location = None; 68 | // The agent has completed the move, it is now idle. 69 | None 70 | } 71 | 72 | fn display_action(&self) -> DisplayAction { 73 | DisplayAction::Moving(self.to) 74 | } 75 | 76 | fn is_valid(&self, ctx: Context) -> bool { 77 | let state = CaptureDomain::get_cur_state(ctx.state_diff); 78 | // This is a follow-up of StartMoving, so as the map is static, we assume 79 | // that as long as the agent exists, the task is valid. 80 | state.agents.get(&ctx.agent).is_some() 81 | } 82 | 83 | impl_task_boxed_methods!(CaptureDomain); 84 | } 85 | -------------------------------------------------------------------------------- /npc-engine-core/examples/capture/task/pick.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use npc_engine_core::{impl_task_boxed_methods, Context, ContextMut, IdleTask, Task, TaskDuration}; 7 | use npc_engine_utils::OptionDiffDomain; 8 | 9 | use crate::{ 10 | constants::{MAP, MAX_AMMO, MAX_HP}, 11 | domain::{CaptureDomain, DisplayAction}, 12 | }; 13 | 14 | #[derive(Clone, Hash, PartialEq, Eq, Debug)] 15 | pub struct Pick; 16 | impl Task for Pick { 17 | fn duration(&self, _ctx: Context) -> TaskDuration { 18 | // Pick is instantaneous 19 | 0 20 | } 21 | 22 | fn weight(&self, _ctx: Context) -> f32 { 23 | 10.0 24 | } 25 | 26 | fn execute(&self, ctx: ContextMut) -> Option>> { 27 | let diff = CaptureDomain::get_cur_state_mut(ctx.state_diff); 28 | let agent_state = diff.agents.get_mut(&ctx.agent).unwrap(); 29 | let location = agent_state.cur_or_last_location; 30 | match location { 31 | _ if location == MAP.ammo_location() => { 32 | agent_state.ammo = (agent_state.ammo + 1).min(MAX_AMMO); 33 | diff.ammo = 0; 34 | diff.ammo_tick = (ctx.tick & 0xff) as u8; 35 | } 36 | _ if location == MAP.medkit_location() => { 37 | agent_state.hp = (agent_state.hp + 1).min(MAX_HP); 38 | diff.medkit = 0; 39 | diff.medkit_tick = (ctx.tick & 0xff) as u8; 40 | } 41 | _ => unimplemented!(), 42 | } 43 | // After Pick, the agent must wait one tick. 44 | Some(Box::new(IdleTask)) 45 | } 46 | 47 | fn display_action(&self) -> DisplayAction { 48 | DisplayAction::Pick 49 | } 50 | 51 | fn is_valid(&self, ctx: Context) -> bool { 52 | let state = CaptureDomain::get_cur_state(ctx.state_diff); 53 | state.agents.get(&ctx.agent).map_or(false, |agent_state| { 54 | // We cannot pick while moving. 55 | if agent_state.next_location.is_some() { 56 | false 57 | } else { 58 | // We must be at a location where there is something to pick. 59 | let location = agent_state.cur_or_last_location; 60 | (location == MAP.ammo_location() && state.ammo > 0) 61 | || (location == MAP.medkit_location() && state.medkit > 0) 62 | } 63 | }) 64 | } 65 | 66 | impl_task_boxed_methods!(CaptureDomain); 67 | } 68 | -------------------------------------------------------------------------------- /npc-engine-core/examples/capture/task/shoot.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use npc_engine_core::{ 7 | impl_task_boxed_methods, AgentId, Context, ContextMut, IdleTask, Task, TaskDuration, 8 | }; 9 | use npc_engine_utils::OptionDiffDomain; 10 | 11 | use crate::domain::{CaptureDomain, DisplayAction}; 12 | 13 | #[derive(Clone, Hash, PartialEq, Eq, Debug)] 14 | pub struct Shoot(pub AgentId); 15 | impl Task for Shoot { 16 | fn duration(&self, _ctx: Context) -> TaskDuration { 17 | // Shoot is instantaneous 18 | 0 19 | } 20 | 21 | fn weight(&self, _ctx: Context) -> f32 { 22 | 10.0 23 | } 24 | 25 | fn execute(&self, ctx: ContextMut) -> Option>> { 26 | let diff = CaptureDomain::get_cur_state_mut(ctx.state_diff); 27 | let agent_state = diff.agents.get_mut(&ctx.agent).unwrap(); 28 | agent_state.ammo -= 1; 29 | let target_state = diff.agents.get_mut(&self.0).unwrap(); 30 | if target_state.hp > 0 { 31 | target_state.hp -= 1; 32 | } 33 | if target_state.hp == 0 { 34 | diff.agents.remove(&self.0); 35 | } 36 | // After Shoot, the agent must wait one tick. 37 | Some(Box::new(IdleTask)) 38 | } 39 | 40 | fn display_action(&self) -> DisplayAction { 41 | DisplayAction::Shoot(self.0) 42 | } 43 | 44 | fn is_valid(&self, ctx: Context) -> bool { 45 | let state = CaptureDomain::get_cur_state(ctx.state_diff); 46 | state.agents.get(&ctx.agent).map_or(false, |agent_state| { 47 | // We must have ammo to shoot. 48 | // We cannot shoot while moving. 49 | if agent_state.ammo == 0 || agent_state.next_location.is_some() { 50 | false 51 | } else { 52 | let location = agent_state.cur_or_last_location; 53 | // Target must exist. 54 | let target = state.agents.get(&self.0); 55 | target.map_or(false, |target| { 56 | // Target must be on our location or its adjacent paths. 57 | target.cur_or_last_location == location 58 | || target 59 | .next_location 60 | .map_or(false, |next_location| next_location == location) 61 | }) 62 | } 63 | }) 64 | } 65 | 66 | impl_task_boxed_methods!(CaptureDomain); 67 | } 68 | -------------------------------------------------------------------------------- /npc-engine-core/examples/capture/task/world.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use npc_engine_core::{impl_task_boxed_methods, Context, ContextMut, Task, TaskDuration}; 7 | use npc_engine_utils::OptionDiffDomain; 8 | 9 | use crate::{ 10 | constants::{RESPAWN_AMMO_DURATION, RESPAWN_MEDKIT_DURATION}, 11 | domain::{CaptureDomain, DisplayAction}, 12 | state::CapturePointState, 13 | }; 14 | 15 | #[derive(Clone, Hash, PartialEq, Eq, Debug)] 16 | pub struct WorldStep; 17 | 18 | impl Task for WorldStep { 19 | fn duration(&self, _ctx: Context) -> TaskDuration { 20 | 1 21 | } 22 | 23 | fn execute(&self, ctx: ContextMut) -> Option>> { 24 | let diff = CaptureDomain::get_cur_state_mut(ctx.state_diff); 25 | // for each captured point, increment the score of the corresponding agent 26 | for capture_point in &diff.capture_points { 27 | if let CapturePointState::Captured(agent) = capture_point { 28 | if let Some(agent_state) = diff.agents.get_mut(agent) { 29 | agent_state.acc_capture += 1; 30 | } 31 | } 32 | } 33 | // respawn if timeout 34 | let now = (ctx.tick & 0xff) as u8; 35 | if respawn_timeout_ammo(now, diff.ammo_tick) { 36 | diff.ammo = 1; 37 | diff.ammo_tick = now; 38 | } 39 | if respawn_timeout_medkit(now, diff.medkit_tick) { 40 | diff.medkit = 1; 41 | diff.medkit_tick = now; 42 | } 43 | 44 | Some(Box::new(WorldStep)) 45 | } 46 | 47 | fn display_action(&self) -> DisplayAction { 48 | DisplayAction::WorldStep 49 | } 50 | 51 | fn is_valid(&self, _ctx: Context) -> bool { 52 | true 53 | } 54 | 55 | impl_task_boxed_methods!(CaptureDomain); 56 | } 57 | 58 | fn respawn_timeout_ammo(now: u8, before: u8) -> bool { 59 | now.wrapping_sub(before) > RESPAWN_AMMO_DURATION 60 | } 61 | fn respawn_timeout_medkit(now: u8, before: u8) -> bool { 62 | now.wrapping_sub(before) > RESPAWN_MEDKIT_DURATION 63 | } 64 | -------------------------------------------------------------------------------- /npc-engine-core/examples/ecosystem/behavior/animal.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use npc_engine_core::{Behavior, Context, IdleTask, Task}; 7 | use npc_engine_utils::DIRECTIONS; 8 | 9 | use crate::{domain::EcosystemDomain, state::Access, task::r#move::Move}; 10 | 11 | pub struct Animal; 12 | 13 | impl Behavior for Animal { 14 | fn add_own_tasks( 15 | &self, 16 | ctx: Context, 17 | tasks: &mut Vec>>, 18 | ) { 19 | for direction in DIRECTIONS { 20 | let task = Move(direction); 21 | if task.is_valid(ctx) { 22 | tasks.push(Box::new(task)); 23 | } 24 | } 25 | tasks.push(Box::new(IdleTask)); 26 | } 27 | 28 | fn is_valid(&self, ctx: Context) -> bool { 29 | ctx.state_diff 30 | .get_agent(ctx.agent) 31 | .filter(|agent_state| { 32 | // debug_assert!(agent_state.alive, "Behavior validity check called on a dead agent"); 33 | agent_state.alive() 34 | }) 35 | .is_some() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /npc-engine-core/examples/ecosystem/behavior/carnivore.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use npc_engine_core::{Behavior, Context, Task}; 7 | use npc_engine_utils::DIRECTIONS; 8 | 9 | use crate::{ 10 | domain::EcosystemDomain, 11 | state::{Access, AgentType}, 12 | task::{eat_herbivore::EatHerbivore, jump::Jump}, 13 | }; 14 | 15 | use super::{animal::Animal, world::WORLD_AGENT_ID}; 16 | 17 | pub struct Carnivore; 18 | 19 | impl Behavior for Carnivore { 20 | fn get_dependent_behaviors(&self) -> &'static [&'static dyn Behavior] { 21 | &[&Animal] 22 | } 23 | 24 | fn add_own_tasks( 25 | &self, 26 | ctx: Context, 27 | tasks: &mut Vec>>, 28 | ) { 29 | for direction in DIRECTIONS { 30 | let jump_task = Jump(direction); 31 | if jump_task.is_valid(ctx) { 32 | tasks.push(Box::new(jump_task)); 33 | } 34 | let eat_task = EatHerbivore(direction); 35 | if eat_task.is_valid(ctx) { 36 | tasks.push(Box::new(eat_task)); 37 | } 38 | } 39 | } 40 | 41 | fn is_valid(&self, ctx: Context) -> bool { 42 | if ctx.agent == WORLD_AGENT_ID { 43 | return false; 44 | } 45 | ctx.state_diff 46 | .get_agent(ctx.agent) 47 | .filter(|agent_state| { 48 | // debug_assert!(agent_state.alive, "Behavior validity check called on a dead agent"); 49 | agent_state.alive() && agent_state.ty == AgentType::Carnivore 50 | }) 51 | .is_some() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /npc-engine-core/examples/ecosystem/behavior/herbivore.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use npc_engine_core::{Behavior, Context, Task}; 7 | 8 | use crate::{ 9 | domain::EcosystemDomain, 10 | state::{Access, AgentType}, 11 | task::eat_grass::EatGrass, 12 | }; 13 | 14 | use super::{animal::Animal, world::WORLD_AGENT_ID}; 15 | 16 | pub struct Herbivore; 17 | 18 | impl Behavior for Herbivore { 19 | fn get_dependent_behaviors(&self) -> &'static [&'static dyn Behavior] { 20 | &[&Animal] 21 | } 22 | 23 | fn add_own_tasks( 24 | &self, 25 | ctx: Context, 26 | tasks: &mut Vec>>, 27 | ) { 28 | let eat_task = EatGrass; 29 | if eat_task.is_valid(ctx) { 30 | tasks.push(Box::new(eat_task)); 31 | } 32 | } 33 | 34 | fn is_valid(&self, ctx: Context) -> bool { 35 | if ctx.agent == WORLD_AGENT_ID { 36 | return false; 37 | } 38 | ctx.state_diff 39 | .get_agent(ctx.agent) 40 | .filter(|agent_state| { 41 | // debug_assert!(agent_state.alive, "Behavior validity check called on a dead agent"); 42 | agent_state.alive() && agent_state.ty == AgentType::Herbivore 43 | }) 44 | .is_some() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /npc-engine-core/examples/ecosystem/behavior/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | pub mod animal; 7 | pub mod carnivore; 8 | pub mod herbivore; 9 | pub mod world; 10 | -------------------------------------------------------------------------------- /npc-engine-core/examples/ecosystem/behavior/world.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use npc_engine_core::{AgentId, Behavior, Context, Task}; 7 | 8 | use crate::{domain::EcosystemDomain, task::world::WorldStep}; 9 | 10 | pub const WORLD_AGENT_ID: AgentId = AgentId(u32::MAX); 11 | 12 | pub struct WorldBehavior; 13 | impl Behavior for WorldBehavior { 14 | fn add_own_tasks( 15 | &self, 16 | _ctx: Context, 17 | tasks: &mut Vec>>, 18 | ) { 19 | tasks.push(Box::new(WorldStep)); 20 | } 21 | 22 | fn is_valid(&self, ctx: Context) -> bool { 23 | ctx.agent == WORLD_AGENT_ID 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /npc-engine-core/examples/ecosystem/constants.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | // This file contains constants that can be tuned in order to adjust simulation. 7 | 8 | use std::time::Duration; 9 | 10 | use npc_engine_core::TaskDuration; 11 | use npc_engine_utils::Coord2D; 12 | 13 | // map generation 14 | pub const MAP_SIZE: Coord2D = Coord2D::new(40, 20); 15 | pub const OBSTACLE_RANDOM_COUNT: usize = 14; 16 | pub const OBSTACLE_HOTSPOT_COUNT: usize = 6; 17 | pub const PLANT_RANDOM_COUNT: usize = 50; 18 | pub const PLANT_HOTSPOT_COUNT: usize = 77; 19 | pub const HERBIVORE_COUNT: usize = 40; 20 | pub const CARNIVORE_COUNT: usize = 8; 21 | 22 | // local state derivation 23 | pub const MAP_RADIUS: i32 = 8; 24 | pub const AGENTS_RADIUS_HERBIVORE: i32 = 3; 25 | pub const AGENTS_RADIUS_CARNIVORE: i32 = 6; 26 | pub const MAX_AGENTS_ATTENTION: usize = 3; // the number of closest agents an agent considers 27 | 28 | // food parameter 29 | pub const HERBIVORE_MAX_FOOD: u32 = 6; 30 | pub const CARNIVORE_MAX_FOOD: u32 = 16; 31 | 32 | // reproduction parameters 33 | pub const HERBIVORE_REPRODUCTION_PERIOD: u64 = 5; 34 | pub const HERBIVORE_REPRODUCTION_CYCLE_DURATION: u64 = 35 | WORLD_TASK_DURATION * HERBIVORE_REPRODUCTION_PERIOD; 36 | pub const HERBIVORE_MIN_FOOD_FOR_REPRODUCTION: u32 = 4; 37 | pub const HERBIVORE_FOOD_GIVEN_TO_BABY: u32 = 2; 38 | pub const CARNIVORE_REPRODUCTION_PERIOD: u64 = 7; 39 | pub const CARNIVORE_REPRODUCTION_CYCLE_DURATION: u64 = 40 | WORLD_TASK_DURATION * CARNIVORE_REPRODUCTION_PERIOD; 41 | pub const CARNIVORE_MIN_FOOD_FOR_REPRODUCTION: u32 = 12; 42 | pub const CARNIVORE_FOOD_GIVEN_TO_BABY: u32 = 5; 43 | 44 | // world task parameter 45 | pub const WORLD_TASK_DURATION: TaskDuration = 10; // each worl task duraction, the food is reduced by one 46 | pub const WORLD_GRASS_REGROW_PERIOD: u64 = 7; 47 | pub const WORLD_GRASS_GROW_CYCLE_DURATION: u64 = WORLD_TASK_DURATION * WORLD_GRASS_REGROW_PERIOD; 48 | pub const WORLD_GRASS_EXPAND_PERIOD: u64 = 13; 49 | pub const WORLD_GRASS_EXPAND_CYCLE_DURATION: u64 = WORLD_TASK_DURATION * WORLD_GRASS_EXPAND_PERIOD; 50 | 51 | // death 52 | pub const TOMB_DURATION: u64 = 40; // number of tick a tombstone is shown after death 53 | 54 | // planning parameters 55 | pub const PLANNING_DURATION: u64 = 3; 56 | pub const PLANNING_VISITS: u32 = 1000; 57 | pub const PLANNING_MINIMUM_VISITS: u32 = 50; 58 | pub const PLANNING_DEPTH: u32 = 50; 59 | pub const PLANNING_DISCOUNT_HL: f32 = PLANNING_DEPTH as f32 / 3.0; 60 | pub const PLANNING_EXPLORATION: f32 = 1.414; 61 | 62 | // task weights (idle task has weight 1) 63 | pub const EAT_GRASS_WEIGHT: f32 = 5.0; 64 | pub const EAT_HERBIVORE_WEIGHT: f32 = 20.0; 65 | pub const JUMP_WEIGHT: f32 = 10.0; 66 | pub const MOVE_WEIGHT: f32 = 5.0; 67 | 68 | // execution paramters 69 | pub const EXECUTION_STEP_DURATION: Duration = Duration::from_millis(40); 70 | -------------------------------------------------------------------------------- /npc-engine-core/examples/ecosystem/plot_ecosystem_stats.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | d = np.genfromtxt('stats.txt', delimiter=',') 5 | 6 | fig, (ax1, ax2) = plt.subplots(2) 7 | 8 | ax1.plot(d[:,[1,2]]) 9 | ax1.set_ylabel('animal count') 10 | ax1.legend(['herbivore', 'carnivore']) 11 | 12 | ax2.plot(d[:,[0,3]]) 13 | ax2.set_ylabel('grass and visit count') 14 | ax2.legend(['grass', 'visits']) 15 | 16 | plt.show() 17 | -------------------------------------------------------------------------------- /npc-engine-core/examples/ecosystem/task/eat_grass.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use npc_engine_core::{impl_task_boxed_methods, Context, ContextMut, Task, TaskDuration}; 7 | 8 | use crate::{ 9 | constants::*, 10 | domain::{DisplayAction, EcosystemDomain}, 11 | map::Tile, 12 | state::{Access, AccessMut}, 13 | }; 14 | 15 | #[derive(Hash, Clone, Eq, PartialEq, Debug)] 16 | pub struct EatGrass; 17 | 18 | impl Task for EatGrass { 19 | fn weight(&self, _ctx: Context) -> f32 { 20 | EAT_GRASS_WEIGHT 21 | } 22 | 23 | fn duration(&self, _ctx: Context) -> TaskDuration { 24 | 0 25 | } 26 | 27 | fn execute( 28 | &self, 29 | mut ctx: ContextMut, 30 | ) -> Option>> { 31 | let agent_state = ctx.state_diff.get_agent_mut(ctx.agent).unwrap(); 32 | agent_state.food = HERBIVORE_MAX_FOOD; 33 | let agent_pos = agent_state.position; 34 | let growth = ctx.state_diff.get_grass(agent_pos).unwrap(); 35 | ctx.state_diff.set_tile(agent_pos, Tile::Grass(growth - 1)); 36 | None 37 | } 38 | 39 | fn is_valid(&self, ctx: Context) -> bool { 40 | let agent_state = ctx.state_diff.get_agent(ctx.agent).unwrap(); 41 | debug_assert!( 42 | agent_state.alive(), 43 | "Task validity check called on a dead agent" 44 | ); 45 | if !agent_state.alive() { 46 | return false; 47 | } 48 | if !agent_state.food < HERBIVORE_MAX_FOOD { 49 | return false; 50 | } 51 | ctx.state_diff 52 | .get_grass(agent_state.position) 53 | .filter(|growth| *growth > 0) 54 | .is_some() 55 | } 56 | 57 | fn display_action(&self) -> DisplayAction { 58 | DisplayAction::EatGrass 59 | } 60 | 61 | impl_task_boxed_methods!(EcosystemDomain); 62 | } 63 | -------------------------------------------------------------------------------- /npc-engine-core/examples/ecosystem/task/eat_herbivore.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | // use std::fmt::{self, Formatter}; 7 | 8 | use npc_engine_core::{impl_task_boxed_methods, Context, ContextMut, Task, TaskDuration}; 9 | use npc_engine_utils::Direction; 10 | 11 | use crate::{ 12 | constants::*, 13 | domain::{DisplayAction, EcosystemDomain}, 14 | map::DirConv, 15 | state::{Access, AccessMut, AgentType}, 16 | }; 17 | 18 | #[derive(Hash, Clone, Eq, PartialEq, Debug)] 19 | pub struct EatHerbivore(pub Direction); 20 | 21 | impl Task for EatHerbivore { 22 | fn weight(&self, _ctx: Context) -> f32 { 23 | EAT_HERBIVORE_WEIGHT 24 | } 25 | 26 | fn duration(&self, _ctx: Context) -> TaskDuration { 27 | 0 28 | } 29 | 30 | fn execute( 31 | &self, 32 | mut ctx: ContextMut, 33 | ) -> Option>> { 34 | let agent_state = ctx.state_diff.get_agent(ctx.agent).unwrap(); 35 | // try next to position 36 | let passage_pos = DirConv::apply(self.0, agent_state.position); 37 | let prey_state = ctx.state_diff.get_agent_at_mut(passage_pos); 38 | if let Some((_, prey_state)) = prey_state { 39 | if prey_state.ty == AgentType::Herbivore && prey_state.alive() { 40 | prey_state.kill(ctx.tick); 41 | let agent_state = ctx.state_diff.get_agent_mut(ctx.agent).unwrap(); 42 | agent_state.position = passage_pos; 43 | agent_state.food = CARNIVORE_MAX_FOOD; 44 | return None; 45 | } 46 | } 47 | // if not, the prey is one further away 48 | let target_pos = DirConv::apply(self.0, passage_pos); 49 | let prey_state = ctx.state_diff.get_agent_at_mut(target_pos).unwrap(); 50 | prey_state.1.kill(ctx.tick); 51 | let agent_state = ctx.state_diff.get_agent_mut(ctx.agent).unwrap(); 52 | agent_state.position = target_pos; 53 | agent_state.food = CARNIVORE_MAX_FOOD; 54 | None 55 | } 56 | 57 | fn is_valid(&self, ctx: Context) -> bool { 58 | let agent_state = ctx.state_diff.get_agent(ctx.agent).unwrap(); 59 | debug_assert!( 60 | agent_state.alive(), 61 | "Task validity check called on a dead agent" 62 | ); 63 | if !agent_state.alive() { 64 | return false; 65 | } 66 | if !agent_state.food < CARNIVORE_MAX_FOOD { 67 | return false; 68 | } 69 | let is_herbivore = |position| { 70 | ctx.state_diff.is_tile_passable(position) 71 | && ctx 72 | .state_diff 73 | .get_agent_at(position) 74 | .map(|(_, agent_state)| { 75 | agent_state.ty == AgentType::Herbivore && agent_state.alive() 76 | }) 77 | .unwrap_or(false) 78 | }; 79 | let passage_pos = DirConv::apply(self.0, agent_state.position); 80 | let target_pos = DirConv::apply(self.0, passage_pos); 81 | is_herbivore(passage_pos) 82 | || (ctx.state_diff.is_tile_passable(passage_pos) && is_herbivore(target_pos)) 83 | } 84 | 85 | fn display_action(&self) -> DisplayAction { 86 | DisplayAction::EatHerbivore(self.0) 87 | } 88 | 89 | impl_task_boxed_methods!(EcosystemDomain); 90 | } 91 | -------------------------------------------------------------------------------- /npc-engine-core/examples/ecosystem/task/jump.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | // use std::fmt::{self, Formatter}; 7 | 8 | use npc_engine_core::{impl_task_boxed_methods, Context, ContextMut, Task, TaskDuration}; 9 | use npc_engine_utils::Direction; 10 | 11 | use crate::{ 12 | constants::JUMP_WEIGHT, 13 | domain::{DisplayAction, EcosystemDomain}, 14 | map::DirConv, 15 | state::{Access, AccessMut}, 16 | }; 17 | 18 | #[derive(Hash, Clone, Eq, PartialEq, Debug)] 19 | pub struct Jump(pub Direction); 20 | 21 | impl Task for Jump { 22 | fn weight(&self, _ctx: Context) -> f32 { 23 | JUMP_WEIGHT 24 | } 25 | 26 | fn duration(&self, _ctx: Context) -> TaskDuration { 27 | 0 28 | } 29 | 30 | fn execute( 31 | &self, 32 | mut ctx: ContextMut, 33 | ) -> Option>> { 34 | let agent_state = ctx.state_diff.get_agent_mut(ctx.agent).unwrap(); 35 | let passage_pos = DirConv::apply(self.0, agent_state.position); 36 | let target_pos = DirConv::apply(self.0, passage_pos); 37 | agent_state.position = target_pos; 38 | agent_state.food -= 1; 39 | None 40 | } 41 | 42 | fn is_valid(&self, ctx: Context) -> bool { 43 | let agent_state = ctx.state_diff.get_agent(ctx.agent).unwrap(); 44 | debug_assert!( 45 | agent_state.alive(), 46 | "Task validity check called on a dead agent" 47 | ); 48 | if !agent_state.alive() || agent_state.food <= 1 { 49 | return false; 50 | } 51 | 52 | let passage_pos = DirConv::apply(self.0, agent_state.position); 53 | let target_pos = DirConv::apply(self.0, passage_pos); 54 | ctx.state_diff.is_tile_passable(passage_pos) && ctx.state_diff.is_position_free(target_pos) 55 | } 56 | 57 | fn display_action(&self) -> DisplayAction { 58 | DisplayAction::Jump(self.0) 59 | } 60 | 61 | impl_task_boxed_methods!(EcosystemDomain); 62 | } 63 | -------------------------------------------------------------------------------- /npc-engine-core/examples/ecosystem/task/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | pub mod eat_grass; 7 | pub mod eat_herbivore; 8 | pub mod jump; 9 | pub mod r#move; 10 | pub mod world; 11 | -------------------------------------------------------------------------------- /npc-engine-core/examples/ecosystem/task/move.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | // use std::fmt::{self, Formatter}; 7 | 8 | use npc_engine_core::{impl_task_boxed_methods, Context, ContextMut, Task, TaskDuration}; 9 | use npc_engine_utils::Direction; 10 | 11 | use crate::{ 12 | constants::MOVE_WEIGHT, 13 | domain::{DisplayAction, EcosystemDomain}, 14 | map::DirConv, 15 | state::{Access, AccessMut}, 16 | }; 17 | 18 | #[derive(Hash, Clone, Eq, PartialEq, Debug)] 19 | pub struct Move(pub Direction); 20 | 21 | impl Task for Move { 22 | fn weight(&self, _ctx: Context) -> f32 { 23 | MOVE_WEIGHT 24 | } 25 | 26 | fn duration(&self, _ctx: Context) -> TaskDuration { 27 | 0 28 | } 29 | 30 | fn execute( 31 | &self, 32 | mut ctx: ContextMut, 33 | ) -> Option>> { 34 | let agent_state = ctx.state_diff.get_agent_mut(ctx.agent).unwrap(); 35 | let target_pos = DirConv::apply(self.0, agent_state.position); 36 | agent_state.position = target_pos; 37 | None 38 | } 39 | 40 | fn is_valid(&self, ctx: Context) -> bool { 41 | let agent_state = ctx.state_diff.get_agent(ctx.agent).unwrap(); 42 | debug_assert!( 43 | agent_state.alive(), 44 | "Task validity check called on a dead agent" 45 | ); 46 | if !agent_state.alive() { 47 | return false; 48 | } 49 | let target_pos = DirConv::apply(self.0, agent_state.position); 50 | ctx.state_diff.is_position_free(target_pos) 51 | } 52 | 53 | fn display_action(&self) -> DisplayAction { 54 | DisplayAction::Move(self.0) 55 | } 56 | 57 | impl_task_boxed_methods!(EcosystemDomain); 58 | } 59 | -------------------------------------------------------------------------------- /npc-engine-core/examples/learn/behavior.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use npc_engine_core::{Behavior, Context, IdleTask, Task}; 7 | 8 | use crate::{ 9 | domain::LearnDomain, 10 | task::{collect::Collect, left::Left, right::Right}, 11 | }; 12 | 13 | pub struct DefaultBehaviour; 14 | impl Behavior for DefaultBehaviour { 15 | fn add_own_tasks( 16 | &self, 17 | ctx: Context, 18 | tasks: &mut Vec>>, 19 | ) { 20 | tasks.push(Box::new(IdleTask)); 21 | let possible_tasks: [Box>; 3] = 22 | [Box::new(Collect), Box::new(Left), Box::new(Right)]; 23 | for task in &possible_tasks { 24 | if task.is_valid(ctx) { 25 | tasks.push(task.clone()); 26 | } 27 | } 28 | } 29 | 30 | fn is_valid(&self, _ctx: Context) -> bool { 31 | true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /npc-engine-core/examples/learn/constants.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | pub const TOTAL_WOOD: usize = 20; 7 | pub const TICKS_PER_ROUND: u64 = 50; 8 | -------------------------------------------------------------------------------- /npc-engine-core/examples/learn/domain.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use npc_engine_core::{AgentId, AgentValue, Behavior, Context, Domain, StateDiffRef}; 7 | use npc_engine_utils::OptionDiffDomain; 8 | 9 | use crate::{behavior::DefaultBehaviour, state::State}; 10 | 11 | pub type Diff = Option; // if Some, use this diff, otherwise use initial state 12 | 13 | #[derive(Debug)] 14 | pub enum DisplayAction { 15 | Wait, 16 | Collect, 17 | Left, 18 | Right, 19 | } 20 | 21 | impl Default for DisplayAction { 22 | fn default() -> Self { 23 | Self::Wait 24 | } 25 | } 26 | 27 | pub struct LearnDomain; 28 | 29 | impl Domain for LearnDomain { 30 | type State = State; 31 | type Diff = Diff; 32 | type DisplayAction = DisplayAction; 33 | 34 | fn list_behaviors() -> &'static [&'static dyn Behavior] { 35 | &[&DefaultBehaviour] 36 | } 37 | 38 | fn get_current_value( 39 | _tick: u64, 40 | state_diff: StateDiffRef, 41 | _agent: AgentId, 42 | ) -> AgentValue { 43 | let state = Self::get_cur_state(state_diff); 44 | AgentValue::from(state.wood_count) 45 | } 46 | 47 | fn update_visible_agents( 48 | _start_tick: u64, 49 | ctx: Context, 50 | agents: &mut std::collections::BTreeSet, 51 | ) { 52 | agents.insert(ctx.agent); 53 | } 54 | 55 | fn get_state_description(state_diff: StateDiffRef) -> String { 56 | let state = Self::get_cur_state(state_diff); 57 | let map = state 58 | .map 59 | .iter() 60 | .map(|count| match count { 61 | 0 => " ", 62 | 1 => "🌱", 63 | 2 => "🌿", 64 | _ => "🌳", 65 | }) 66 | .collect::(); 67 | format!("@{:2} 🪵{:2} [{map}]", state.agent_pos, state.wood_count) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /npc-engine-core/examples/learn/estimator.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use std::collections::BTreeMap; 7 | 8 | use npc_engine_core::{AgentId, MCTSConfiguration, StateDiffRef, StateValueEstimator}; 9 | use npc_engine_utils::{NeuralNetwork, Neuron, OptionDiffDomain}; 10 | 11 | use crate::{domain::LearnDomain, state::State}; 12 | 13 | #[derive(Clone)] 14 | pub struct NNStateValueEstimator(pub NeuralNetwork<5, 2>); 15 | impl Default for NNStateValueEstimator { 16 | fn default() -> Self { 17 | Self(NeuralNetwork { 18 | hidden_layer: [ 19 | Neuron::random_with_range(0.1), 20 | Neuron::random_with_range(0.1), 21 | ], 22 | output_layer: Neuron::random_with_range(0.1), 23 | }) 24 | } 25 | } 26 | impl StateValueEstimator for NNStateValueEstimator { 27 | fn estimate( 28 | &mut self, 29 | _rnd: &mut rand_chacha::ChaCha8Rng, 30 | _config: &MCTSConfiguration, 31 | initial_state: &State, 32 | _start_tick: u64, 33 | node: &npc_engine_core::Node, 34 | _edges: &npc_engine_core::Edges, 35 | _depth: u32, 36 | ) -> Option> { 37 | let state = LearnDomain::get_cur_state(StateDiffRef::new(initial_state, node.diff())); 38 | let value = self.0.output(&state.local_view()); 39 | Some(BTreeMap::from([(AgentId(0), value)])) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /npc-engine-core/examples/learn/main.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use constants::{TICKS_PER_ROUND, TOTAL_WOOD}; 7 | use domain::LearnDomain; 8 | use estimator::NNStateValueEstimator; 9 | use npc_engine_core::{ 10 | graphviz, ActiveTask, ActiveTasks, AgentId, IdleTask, MCTSConfiguration, StateValueEstimator, 11 | MCTS, 12 | }; 13 | use npc_engine_utils::{run_simple_executor, ExecutorState, ExecutorStateLocal}; 14 | use rand::{thread_rng, Rng}; 15 | use state::State; 16 | 17 | mod behavior; 18 | mod constants; 19 | mod domain; 20 | mod estimator; 21 | mod state; 22 | mod task; 23 | 24 | #[derive(Default)] 25 | struct LearnExecutorState { 26 | estimator: NNStateValueEstimator, 27 | planned_values: Vec<([f32; 5], f32)>, 28 | } 29 | 30 | impl LearnExecutorState { 31 | pub fn wood_collected(&self) -> f32 { 32 | TOTAL_WOOD as f32 - self.planned_values.last().unwrap().0.iter().sum::() 33 | } 34 | 35 | pub fn train_and_clear_data(&mut self) { 36 | self.estimator.0.train(self.planned_values.iter(), 0.001); 37 | self.planned_values.clear(); 38 | } 39 | } 40 | 41 | impl ExecutorStateLocal for LearnExecutorState { 42 | fn create_initial_state(&self) -> State { 43 | let mut rng = thread_rng(); 44 | let mut map = [0; 14]; 45 | for _tree in 0..TOTAL_WOOD { 46 | let mut pos = rng.gen_range(0..14); 47 | while map[pos] >= 3 { 48 | pos = rng.gen_range(0..14); 49 | } 50 | map[pos] += 1; 51 | } 52 | State { 53 | map, 54 | wood_count: 0, 55 | agent_pos: rng.gen_range(0..14), 56 | } 57 | } 58 | 59 | fn init_task_queue(&self, _: &State) -> ActiveTasks { 60 | vec![ActiveTask::new_with_end( 61 | 0, 62 | 0, 63 | AgentId(0), 64 | Box::new(IdleTask), 65 | )] 66 | .into_iter() 67 | .collect() 68 | } 69 | 70 | fn keep_agent(&self, tick: u64, _state: &State, _agent: AgentId) -> bool { 71 | tick < TICKS_PER_ROUND 72 | } 73 | } 74 | 75 | impl ExecutorState for LearnExecutorState { 76 | fn create_state_value_estimator(&self) -> Box + Send> { 77 | Box::new(self.estimator.clone()) 78 | } 79 | 80 | fn post_mcts_run_hook( 81 | &mut self, 82 | mcts: &MCTS, 83 | _last_active_task: &ActiveTask, 84 | ) { 85 | self.planned_values.push(( 86 | mcts.initial_state().local_view(), 87 | mcts.q_value_at_root(AgentId(0)), 88 | )); 89 | } 90 | } 91 | 92 | #[allow(dead_code)] 93 | fn enable_map_display() { 94 | use std::io::Write; 95 | env_logger::builder() 96 | .format(|buf, record| writeln!(buf, "{}", record.args())) 97 | .filter(None, log::LevelFilter::Info) 98 | .init(); 99 | } 100 | 101 | fn main() { 102 | // These parameters control the MCTS algorithm. 103 | const CONFIG: MCTSConfiguration = MCTSConfiguration { 104 | allow_invalid_tasks: true, 105 | visits: 20, 106 | depth: TICKS_PER_ROUND as u32, 107 | exploration: 1.414, 108 | discount_hl: TICKS_PER_ROUND as f32 / 3., 109 | seed: None, 110 | planning_task_duration: None, 111 | }; 112 | 113 | // Set the depth of graph output to 4. 114 | graphviz::set_graph_output_depth(4); 115 | 116 | // Uncomment these if you want to see the world being harvested 117 | // enable_map_display(); 118 | 119 | // State of the execution, including the estimator. 120 | let mut executor_state = LearnExecutorState::default(); 121 | 122 | // We run multiple executions, after each, we train the estimator. 123 | for _epoch in 0..600 { 124 | run_simple_executor::(&CONFIG, &mut executor_state); 125 | let wood_collected = executor_state.wood_collected(); 126 | println!("{wood_collected}"); 127 | executor_state.train_and_clear_data(); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /npc-engine-core/examples/learn/plot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | SPDX-License-Identifier: Apache-2.0 OR MIT 5 | © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 6 | """ 7 | 8 | import sys 9 | import os 10 | import numpy as np 11 | import scipy.ndimage as ndimage 12 | import matplotlib.pyplot as plt 13 | 14 | print('Compiling') 15 | os.system('cargo build --release --example learn') 16 | Xs = [] 17 | for run in range(20): 18 | print(f'Running simulation {run}') 19 | os.system('target/release/examples/learn > output.csv') 20 | with open('output.csv') as f: 21 | Xs.append([float(x) for x in f.readlines()]) 22 | os.system('rm -f output.csv') 23 | X = np.average(Xs, axis = 0) 24 | avg_X = ndimage.uniform_filter1d(X, 100) 25 | plot_to_file = len(sys.argv) > 1 26 | if plot_to_file: 27 | plt.rcParams["figure.figsize"] = (3.3, 1.6) 28 | plt.plot(X) 29 | plt.plot(avg_X) 30 | plt.xlabel('epoch') 31 | plt.ylabel('wood collected') 32 | if plot_to_file: 33 | plt.savefig(sys.argv[1], bbox_inches='tight') 34 | else: 35 | plt.show() -------------------------------------------------------------------------------- /npc-engine-core/examples/learn/state.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | #[derive(Clone, Hash, PartialEq, Eq, Debug)] 7 | pub struct State { 8 | pub map: [u8; 14], 9 | pub wood_count: u8, 10 | pub agent_pos: u8, 11 | } 12 | impl State { 13 | // The number of trees as seen by the agent: 14 | // [sum very left, just left, cur pos, just right, sum very right] 15 | pub fn local_view(&self) -> [f32; 5] { 16 | let pos = self.agent_pos as usize; 17 | let len = self.map.len(); 18 | let left_left = if pos > 1 { 19 | let sum: u8 = self.map.iter().take(pos - 1).sum(); 20 | sum as f32 21 | } else { 22 | 0. 23 | }; 24 | let left = if pos > 0 { 25 | self.map[pos - 1] as f32 26 | } else { 27 | 0. 28 | }; 29 | let mid = self.map[pos] as f32; 30 | let right = if pos < len - 1 { 31 | self.map[pos + 1] as f32 32 | } else { 33 | 0. 34 | }; 35 | let right_right = if pos < len - 2 { 36 | let sum: u8 = self.map.iter().skip(pos + 2).sum(); 37 | sum as f32 38 | } else { 39 | 0. 40 | }; 41 | [left_left, left, mid, right, right_right] 42 | } 43 | } 44 | 45 | #[cfg(test)] 46 | mod tests { 47 | use super::*; 48 | 49 | #[test] 50 | fn state_local_view() { 51 | let mut state = State { 52 | map: [1, 3, 2, 1, 3, 2, 1, 0, 1, 3, 2, 0, 1, 3], 53 | wood_count: 0, 54 | agent_pos: 0, 55 | }; 56 | assert_eq!(state.local_view(), [0., 0., 1., 3., 19.]); 57 | state.agent_pos = 1; 58 | assert_eq!(state.local_view(), [0., 1., 3., 2., 17.]); 59 | state.agent_pos = 3; 60 | assert_eq!(state.local_view(), [4., 2., 1., 3., 13.]); 61 | state.agent_pos = 12; 62 | assert_eq!(state.local_view(), [19., 0., 1., 3., 0.]); 63 | state.agent_pos = 13; 64 | assert_eq!(state.local_view(), [19., 1., 3., 0., 0.]); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /npc-engine-core/examples/learn/task/collect.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use npc_engine_core::{impl_task_boxed_methods, Context, ContextMut, Task, TaskDuration}; 7 | use npc_engine_utils::OptionDiffDomain; 8 | 9 | use crate::domain::{DisplayAction, LearnDomain}; 10 | 11 | #[derive(Clone, Hash, PartialEq, Eq, Debug)] 12 | pub struct Collect; 13 | impl Task for Collect { 14 | fn duration(&self, _ctx: Context) -> TaskDuration { 15 | 1 16 | } 17 | 18 | fn execute(&self, ctx: ContextMut) -> Option>> { 19 | let state = LearnDomain::get_cur_state_mut(ctx.state_diff); 20 | debug_assert!(state.map[state.agent_pos as usize] > 0); 21 | state.map[state.agent_pos as usize] -= 1; 22 | state.wood_count += 1; 23 | None 24 | } 25 | 26 | fn is_valid(&self, ctx: Context) -> bool { 27 | let state = LearnDomain::get_cur_state(ctx.state_diff); 28 | state.map[state.agent_pos as usize] > 0 29 | } 30 | 31 | fn display_action(&self) -> DisplayAction { 32 | DisplayAction::Collect 33 | } 34 | 35 | impl_task_boxed_methods!(LearnDomain); 36 | } 37 | -------------------------------------------------------------------------------- /npc-engine-core/examples/learn/task/left.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use npc_engine_core::{impl_task_boxed_methods, Context, ContextMut, Task, TaskDuration}; 7 | use npc_engine_utils::OptionDiffDomain; 8 | 9 | use crate::domain::{DisplayAction, LearnDomain}; 10 | 11 | #[derive(Clone, Hash, PartialEq, Eq, Debug)] 12 | pub struct Left; 13 | impl Task for Left { 14 | fn duration(&self, _ctx: Context) -> TaskDuration { 15 | 1 16 | } 17 | 18 | fn execute(&self, ctx: ContextMut) -> Option>> { 19 | let state = LearnDomain::get_cur_state_mut(ctx.state_diff); 20 | debug_assert!(state.agent_pos > 0); 21 | state.agent_pos -= 1; 22 | None 23 | } 24 | 25 | fn is_valid(&self, ctx: Context) -> bool { 26 | let state = LearnDomain::get_cur_state(ctx.state_diff); 27 | state.agent_pos > 0 28 | } 29 | 30 | fn display_action(&self) -> DisplayAction { 31 | DisplayAction::Left 32 | } 33 | 34 | impl_task_boxed_methods!(LearnDomain); 35 | } 36 | -------------------------------------------------------------------------------- /npc-engine-core/examples/learn/task/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | pub mod collect; 7 | pub mod left; 8 | pub mod right; 9 | -------------------------------------------------------------------------------- /npc-engine-core/examples/learn/task/right.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use npc_engine_core::{impl_task_boxed_methods, Context, ContextMut, Task, TaskDuration}; 7 | use npc_engine_utils::OptionDiffDomain; 8 | 9 | use crate::domain::{DisplayAction, LearnDomain}; 10 | 11 | #[derive(Clone, Hash, PartialEq, Eq, Debug)] 12 | pub struct Right; 13 | impl Task for Right { 14 | fn duration(&self, _ctx: Context) -> TaskDuration { 15 | 1 16 | } 17 | 18 | fn execute(&self, ctx: ContextMut) -> Option>> { 19 | let state = LearnDomain::get_cur_state_mut(ctx.state_diff); 20 | debug_assert!((state.agent_pos as usize) < state.map.len() - 1); 21 | state.agent_pos += 1; 22 | None 23 | } 24 | 25 | fn is_valid(&self, ctx: Context) -> bool { 26 | let state = LearnDomain::get_cur_state(ctx.state_diff); 27 | (state.agent_pos as usize) < state.map.len() - 1 28 | } 29 | 30 | fn display_action(&self) -> DisplayAction { 31 | DisplayAction::Right 32 | } 33 | 34 | impl_task_boxed_methods!(LearnDomain); 35 | } 36 | -------------------------------------------------------------------------------- /npc-engine-core/examples/tic-tac-toe/domain.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use std::{collections::BTreeSet, fmt}; 7 | 8 | use npc_engine_core::{AgentId, AgentValue, Behavior, Context, Domain, StateDiffRef}; 9 | 10 | use crate::{ 11 | board::{Board, CellArray2D, Diff, State}, 12 | r#move::{Move, MoveBehavior}, 13 | }; 14 | 15 | // Option, so that the idle placeholder action is Wait 16 | #[derive(Default)] 17 | pub struct DisplayAction(pub Option); 18 | 19 | impl fmt::Debug for DisplayAction { 20 | fn fmt(&self, f: &'_ mut fmt::Formatter) -> fmt::Result { 21 | match &self.0 { 22 | None => f.write_str("Wait"), 23 | Some(m) => f.write_fmt(format_args!("Move({}, {})", m.x, m.y)), 24 | } 25 | } 26 | } 27 | 28 | pub struct TicTacToe; 29 | 30 | // TODO: once const NotNan::new() is stabilized, switch to that 31 | // SAFETY: 0.0, 1.0, -1.0 are not NaN 32 | const VALUE_UNDECIDED: AgentValue = unsafe { AgentValue::new_unchecked(0.) }; 33 | const VALUE_WIN: AgentValue = unsafe { AgentValue::new_unchecked(1.) }; 34 | const VALUE_LOOSE: AgentValue = unsafe { AgentValue::new_unchecked(-1.) }; 35 | 36 | impl Domain for TicTacToe { 37 | type State = State; 38 | type Diff = Diff; 39 | type DisplayAction = DisplayAction; 40 | 41 | fn list_behaviors() -> &'static [&'static dyn Behavior] { 42 | &[&MoveBehavior] 43 | } 44 | 45 | fn get_current_value(_tick: u64, state_diff: StateDiffRef, agent: AgentId) -> AgentValue { 46 | let state = *state_diff.initial_state | state_diff.diff.unwrap_or(0); 47 | match state.winner() { 48 | None => VALUE_UNDECIDED, 49 | Some(player) => { 50 | if player.to_agent() == agent { 51 | VALUE_WIN 52 | } else { 53 | VALUE_LOOSE 54 | } 55 | } 56 | } 57 | } 58 | 59 | fn update_visible_agents( 60 | _start_tick: u64, 61 | _ctx: Context, 62 | agents: &mut BTreeSet, 63 | ) { 64 | agents.insert(AgentId(0)); 65 | agents.insert(AgentId(1)); 66 | } 67 | 68 | fn get_state_description(state_diff: StateDiffRef) -> String { 69 | let state = *state_diff.initial_state | state_diff.diff.unwrap_or(0); 70 | state.description() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /npc-engine-core/examples/tic-tac-toe/move.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use std::fmt; 7 | 8 | use npc_engine_core::{ 9 | impl_task_boxed_methods, Behavior, Context, ContextMut, IdleTask, Task, TaskDuration, 10 | }; 11 | 12 | use crate::{ 13 | board::{Board, Cell, CellArray2D, CellCoord, C_RANGE}, 14 | domain::{DisplayAction, TicTacToe}, 15 | player::Player, 16 | }; 17 | 18 | #[derive(Clone, Hash, PartialEq, Eq)] 19 | pub struct Move { 20 | pub x: CellCoord, 21 | pub y: CellCoord, 22 | } 23 | impl std::fmt::Display for Move { 24 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 25 | write!(f, "{} {}", self.x, self.y) 26 | } 27 | } 28 | 29 | impl Task for Move { 30 | fn duration(&self, _ctx: Context) -> TaskDuration { 31 | // Moves affect the board instantly 32 | 0 33 | } 34 | 35 | fn execute(&self, ctx: ContextMut) -> Option>> { 36 | let diff = if let Some(diff) = ctx.state_diff.diff { 37 | diff 38 | } else { 39 | *ctx.state_diff.diff = Some(0); 40 | &mut *ctx.state_diff.diff.as_mut().unwrap() 41 | }; 42 | diff.set(self.x, self.y, Cell::Player(Player::from_agent(ctx.agent))); 43 | assert!(ctx.state_diff.diff.is_some()); 44 | // After every move, one has to wait one's next turn 45 | Some(Box::new(IdleTask)) 46 | } 47 | 48 | fn display_action(&self) -> DisplayAction { 49 | DisplayAction(Some(self.clone())) 50 | } 51 | 52 | fn is_valid(&self, ctx: Context) -> bool { 53 | let state = *ctx.state_diff.initial_state | ctx.state_diff.diff.unwrap_or(0); 54 | state.winner().is_none() && state.get(self.x, self.y) == Cell::Empty 55 | } 56 | 57 | impl_task_boxed_methods!(TicTacToe); 58 | } 59 | 60 | impl fmt::Debug for Move { 61 | fn fmt(&self, f: &'_ mut fmt::Formatter) -> fmt::Result { 62 | f.debug_struct("Move") 63 | .field("x", &self.x.get()) 64 | .field("y", &self.y.get()) 65 | .finish() 66 | } 67 | } 68 | 69 | pub struct MoveBehavior; 70 | impl Behavior for MoveBehavior { 71 | fn add_own_tasks(&self, ctx: Context, tasks: &mut Vec>>) { 72 | // if the game is already ended, no move are valid 73 | let board = *ctx.state_diff.initial_state | ctx.state_diff.diff.unwrap_or(0); 74 | if board.winner().is_some() { 75 | return; 76 | } 77 | for x in C_RANGE { 78 | for y in C_RANGE { 79 | let task = Move { x, y }; 80 | if task.is_valid(ctx) { 81 | tasks.push(Box::new(task)); 82 | } 83 | } 84 | } 85 | } 86 | 87 | fn is_valid(&self, _ctx: Context) -> bool { 88 | true 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /npc-engine-core/examples/tic-tac-toe/player.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use std::fmt; 7 | 8 | use npc_engine_core::AgentId; 9 | 10 | #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] 11 | pub enum Player { 12 | O, 13 | X, 14 | } 15 | 16 | impl Player { 17 | pub fn from_agent(agent: AgentId) -> Self { 18 | match agent { 19 | AgentId(0) => Player::O, 20 | AgentId(1) => Player::X, 21 | AgentId(id) => panic!("Invalid AgentId {id}"), 22 | } 23 | } 24 | 25 | pub fn to_agent(self) -> AgentId { 26 | match self { 27 | Player::O => AgentId(0), 28 | Player::X => AgentId(1), 29 | } 30 | } 31 | } 32 | 33 | impl fmt::Display for Player { 34 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 35 | match self { 36 | Player::O => write!(f, "O"), 37 | Player::X => write!(f, "X"), 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /npc-engine-core/images/learn_wood_collected_over_epochs.png: -------------------------------------------------------------------------------- 1 | ../../images/learn_wood_collected_over_epochs.png -------------------------------------------------------------------------------- /npc-engine-core/src/active_task.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use crate::{AgentId, Context, Domain, IdleTask, Task}; 7 | use std::cmp::Ordering; 8 | use std::collections::BTreeSet; 9 | use std::hash::{Hash, Hasher}; 10 | use std::{fmt, mem}; 11 | 12 | /// A task associated to an agent and that is being processed by the planner. 13 | pub struct ActiveTask { 14 | /// the start tick of this task 15 | pub start: u64, 16 | /// the end tick of this task 17 | pub end: u64, 18 | /// the agent executing this task 19 | pub agent: AgentId, 20 | /// the actual task 21 | pub task: Box>, 22 | } 23 | /// A set of active tasks. 24 | /// 25 | /// These tasks are sorted by end time and then by agent, due to the implementation of `cmp` by `ActiveTask`. 26 | pub type ActiveTasks = BTreeSet>; 27 | 28 | impl ActiveTask { 29 | /// Creates a new active task, computes the end from task and the state_diff. 30 | pub fn new(task: Box>, ctx: Context) -> Self { 31 | let end = ctx.tick + task.duration(ctx); 32 | Self::new_with_end(ctx.tick, end, ctx.agent, task) 33 | } 34 | /// Creates a new active task with a specified end. 35 | pub fn new_with_end(start: u64, end: u64, agent: AgentId, task: Box>) -> Self { 36 | Self { 37 | start, 38 | end, 39 | agent, 40 | task, 41 | } 42 | } 43 | /// Creates a new idle task for agent at a given tick, make sure that it will 44 | /// execute in the future considering that we are currently processing active_agent. 45 | pub fn new_idle(tick: u64, agent: AgentId, active_agent: AgentId) -> Self { 46 | Self { 47 | start: tick, 48 | // Make sure the idle tasks of added agents will not be 49 | // executed before the active agent. 50 | end: if agent < active_agent { tick + 1 } else { tick }, 51 | agent, 52 | task: Box::new(IdleTask), 53 | } 54 | } 55 | /// The memory footprint of this struct. 56 | pub fn size(&self, task_size: fn(&dyn Task) -> usize) -> usize { 57 | let mut size = 0; 58 | size += mem::size_of::(); 59 | size += task_size(&*self.task); 60 | size 61 | } 62 | } 63 | 64 | /// Returns the task associated to a given agent from an active task set. 65 | pub(crate) fn get_task_for_agent( 66 | set: &ActiveTasks, 67 | agent: AgentId, 68 | ) -> Option<&ActiveTask> { 69 | set.iter().find(|&task| task.agent == agent) 70 | } 71 | 72 | impl fmt::Debug for ActiveTask { 73 | fn fmt(&self, f: &'_ mut fmt::Formatter) -> fmt::Result { 74 | f.debug_struct("ActiveTask") 75 | .field("end", &self.end) 76 | .field("agent", &self.agent) 77 | .field("task", &self.task) 78 | .finish() 79 | } 80 | } 81 | 82 | impl std::fmt::Display for ActiveTask { 83 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 84 | write!(f, "{} {:?} ends T{}", self.agent, self.task, self.end) 85 | } 86 | } 87 | 88 | impl Hash for ActiveTask { 89 | fn hash(&self, state: &mut H) { 90 | self.end.hash(state); 91 | self.agent.hash(state); 92 | self.task.hash(state); 93 | } 94 | } 95 | 96 | impl Clone for ActiveTask { 97 | fn clone(&self) -> Self { 98 | ActiveTask { 99 | start: self.start, 100 | end: self.end, 101 | agent: self.agent, 102 | task: self.task.box_clone(), 103 | } 104 | } 105 | } 106 | 107 | impl Ord for ActiveTask { 108 | /// Active tasks are ordered first by time of ending, then by agent id 109 | fn cmp(&self, other: &Self) -> Ordering { 110 | self.end.cmp(&other.end).then(self.agent.cmp(&other.agent)) 111 | } 112 | } 113 | 114 | impl PartialOrd for ActiveTask { 115 | fn partial_cmp(&self, other: &Self) -> Option { 116 | Some(self.cmp(other)) 117 | } 118 | } 119 | 120 | impl Eq for ActiveTask {} 121 | 122 | impl PartialEq for ActiveTask { 123 | fn eq(&self, other: &Self) -> bool { 124 | self.end == other.end && self.agent == other.agent && self.task.box_eq(&other.task) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /npc-engine-core/src/behavior.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use crate::{Context, Domain, Task}; 7 | 8 | /// A possibly-recursive set of possible tasks. 9 | /// 10 | /// You need to implement at least two methods: [is_valid](Self::is_valid) and [add_own_tasks](Self::add_own_tasks). 11 | pub trait Behavior: 'static { 12 | /// Returns if the behavior is valid for the given agent in the given world state. 13 | fn is_valid(&self, ctx: Context) -> bool; 14 | 15 | /// Collects valid tasks for the given agent in the given world state. 16 | #[allow(unused)] 17 | fn add_own_tasks(&self, ctx: Context, tasks: &mut Vec>>); 18 | 19 | /// Returns dependent behaviors. 20 | fn get_dependent_behaviors(&self) -> &'static [&'static dyn Behavior] { 21 | &[] 22 | } 23 | 24 | /// Helper method to recursively collect all valid tasks for the given agent in the given world state. 25 | /// 26 | /// It will not do anything if the behavior is invalid at that point. 27 | fn add_tasks(&self, ctx: Context, tasks: &mut Vec>>) { 28 | if !self.is_valid(ctx) { 29 | return; 30 | } 31 | self.add_own_tasks(ctx, tasks); 32 | self.get_dependent_behaviors() 33 | .iter() 34 | .for_each(|behavior| behavior.add_tasks(ctx, tasks)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /npc-engine-core/src/config.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use std::num::NonZeroU64; 7 | 8 | /// A functor that returns whether the planner must do an early stop. 9 | pub type EarlyStopCondition = dyn Fn(u32) -> bool + Send; 10 | 11 | /// The configuration of an MCTS instance. 12 | #[derive(Clone, Debug, Default)] 13 | pub struct MCTSConfiguration { 14 | /// if true, invalid tasks do not abort expansion or rollout, but trigger re-planning 15 | pub allow_invalid_tasks: bool, 16 | /// maximum number of visits per run 17 | pub visits: u32, 18 | /// maximum tree depth per run in tick 19 | pub depth: u32, 20 | /// exploration factor to use in UCT to balance exploration and exploitation 21 | pub exploration: f32, 22 | /// the discount factor for later reward, in half life (per agent's turn or tick) 23 | pub discount_hl: f32, 24 | /// if not `None`, the duration of the planning task 25 | pub planning_task_duration: Option, 26 | /// optionally, a user-given seed 27 | pub seed: Option, 28 | } 29 | -------------------------------------------------------------------------------- /npc-engine-core/src/context.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use crate::{AgentId, Domain, StateDiffRef, StateDiffRefMut}; 7 | 8 | /// The context of a search node 9 | pub struct Context<'a, D: Domain> { 10 | /// tick at this node 11 | pub tick: u64, 12 | /// world state at this node 13 | pub state_diff: StateDiffRef<'a, D>, 14 | /// agent for this node 15 | pub agent: AgentId, 16 | } 17 | impl Copy for Context<'_, D> {} 18 | impl Clone for Context<'_, D> { 19 | fn clone(&self) -> Self { 20 | *self 21 | } 22 | } 23 | impl<'a, D: Domain> Context<'a, D> { 24 | /// Creates a new Context from its components. 25 | pub fn new(tick: u64, state_diff: StateDiffRef<'a, D>, agent: AgentId) -> Self { 26 | Self { 27 | tick, 28 | state_diff, 29 | agent, 30 | } 31 | } 32 | /// Builds directly from an initial_state and diff . 33 | pub fn with_state_and_diff( 34 | tick: u64, 35 | initial_state: &'a D::State, 36 | diff: &'a D::Diff, 37 | agent: AgentId, 38 | ) -> Self { 39 | Self { 40 | tick, 41 | state_diff: StateDiffRef::new(initial_state, diff), 42 | agent, 43 | } 44 | } 45 | /// Replaces the tick and agent, keep the state_diff. 46 | pub fn replace_tick_and_agent(self, tick: u64, agent: AgentId) -> Self { 47 | Self { 48 | tick, 49 | state_diff: self.state_diff, 50 | agent, 51 | } 52 | } 53 | /// Drops the state_diff and returns (tick, agent) 54 | pub fn drop_state_diff(self) -> (u64, AgentId) { 55 | (self.tick, self.agent) 56 | } 57 | } 58 | 59 | /// The context of a search node, mutable version 60 | pub struct ContextMut<'a, D: Domain> { 61 | /// tick at this node 62 | pub tick: u64, 63 | /// world state at this node 64 | pub state_diff: StateDiffRefMut<'a, D>, 65 | /// agent for this node 66 | pub agent: AgentId, 67 | } 68 | impl<'a, D: Domain> ContextMut<'a, D> { 69 | /// Creates a new ContextMut from its components. 70 | pub fn new(tick: u64, state_diff: StateDiffRefMut<'a, D>, agent: AgentId) -> Self { 71 | Self { 72 | tick, 73 | state_diff, 74 | agent, 75 | } 76 | } 77 | /// Builds directly from an initial_state and diff. 78 | pub fn with_state_and_diff( 79 | tick: u64, 80 | initial_state: &'a D::State, 81 | diff: &'a mut D::Diff, 82 | agent: AgentId, 83 | ) -> Self { 84 | Self { 85 | tick, 86 | state_diff: StateDiffRefMut::new(initial_state, diff), 87 | agent, 88 | } 89 | } 90 | /// Builds directly from an initial_state and diff, with rest as a tuple. 91 | pub fn with_rest_and_state_and_diff( 92 | rest: (u64, AgentId), 93 | initial_state: &'a D::State, 94 | diff: &'a mut D::Diff, 95 | ) -> Self { 96 | Self { 97 | tick: rest.0, 98 | state_diff: StateDiffRefMut::new(initial_state, diff), 99 | agent: rest.1, 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /npc-engine-core/src/domain.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use std::collections::{BTreeMap, BTreeSet}; 7 | use std::hash::Hash; 8 | 9 | use ordered_float::NotNan; 10 | use rand_chacha::ChaCha8Rng; 11 | 12 | use crate::{ 13 | AgentId, Behavior, Context, Edges, IdleTask, MCTSConfiguration, Node, StateDiffRef, Task, 14 | }; 15 | 16 | /// The "current" value an agent has in a given state. 17 | pub type AgentValue = NotNan; 18 | 19 | /// A domain on which the MCTS planner can plan. 20 | pub trait Domain: Sized + 'static { 21 | /// The state the MCTS plans on. 22 | type State: std::fmt::Debug + Sized; 23 | /// A compact set of changes towards a `State` that are accumulated throughout planning. 24 | type Diff: std::fmt::Debug + Default + Clone + Hash + Eq; 25 | /// A representation of a display action that can be fetched from a task. 26 | /// We need Default trait for creating the DisplayAction for the idle placeholder task. 27 | type DisplayAction: std::fmt::Debug + Default; 28 | 29 | /// Returns all behaviors available for this domain. 30 | fn list_behaviors() -> &'static [&'static dyn Behavior]; 31 | 32 | /// Gets the current value of the given agent in the given tick and world state. 33 | fn get_current_value(tick: u64, state_diff: StateDiffRef, agent: AgentId) -> AgentValue; 34 | 35 | /// Updates the list of agents which are in the horizon of the given agent in the given tick and world state. 36 | fn update_visible_agents(start_tick: u64, ctx: Context, agents: &mut BTreeSet); 37 | 38 | /// Gets all possible valid tasks for a given agent in a given tick and world state. 39 | fn get_tasks(ctx: Context) -> Vec>> { 40 | let mut actions = Vec::new(); 41 | Self::list_behaviors() 42 | .iter() 43 | .for_each(|behavior| behavior.add_tasks(ctx, &mut actions)); 44 | 45 | actions.dedup(); 46 | actions 47 | } 48 | 49 | /// Gets a textual description of the given world state. 50 | /// This will be used by the graph tool to show in each node, and the log tool to dump the state. 51 | fn get_state_description(_state_diff: StateDiffRef) -> String { 52 | String::new() 53 | } 54 | 55 | /// Gets the new agents present in a diff but not in a state. 56 | fn get_new_agents(_state_diff: StateDiffRef) -> Vec { 57 | vec![] 58 | } 59 | 60 | /// Gets the display actions for idle task. 61 | fn display_action_task_idle() -> Self::DisplayAction { 62 | Default::default() 63 | } 64 | 65 | /// Gets the display actions for planning task. 66 | fn display_action_task_planning() -> Self::DisplayAction { 67 | Default::default() 68 | } 69 | } 70 | 71 | /// An estimator of state-value function. 72 | pub trait StateValueEstimator: Send { 73 | /// Takes the state of an explored node and returns the estimated expected (discounted) values. 74 | /// 75 | /// Returns None if the passed node has no unexpanded edge. 76 | #[allow(clippy::too_many_arguments)] 77 | fn estimate( 78 | &mut self, 79 | rnd: &mut ChaCha8Rng, 80 | config: &MCTSConfiguration, 81 | initial_state: &D::State, 82 | start_tick: u64, 83 | node: &Node, 84 | edges: &Edges, 85 | depth: u32, 86 | ) -> Option>; 87 | } 88 | 89 | /// Domains who want to use planning tasks must implement this. 90 | pub trait DomainWithPlanningTask: Domain { 91 | /// A fallback task, in case, during planning, the world evolved in a different direction than what the MCTS tree explored. 92 | fn fallback_task(_agent: AgentId) -> Box> { 93 | Box::new(IdleTask) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /npc-engine-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | //! This is the core of the NPC engine, containing the [MCTS] algorithm implementation and related abstractions. 7 | //! 8 | //! We provide several [examples](https://github.com/ethz-gtc/npc-engine/tree/main/npc-engine-core/examples) 9 | //! as introductions on how to use the planner. 10 | //! A good place to start is [tic-tac-toe](https://github.com/ethz-gtc/npc-engine/tree/main/npc-engine-core/examples/tic-tac-toe). 11 | //! 12 | //! The core of the planner is the [MCTS] struct, which holds the state of the planner. 13 | //! It has two constructors, a simplified one, [new](MCTS::new), and a complete one, [new_with_tasks](MCTS::new_with_tasks). 14 | //! Once constructed, the [run](MCTS::run) method performs the search and returns the best task. 15 | //! After a search, the resulting tree can be inspected, starting from the [root node](MCTS::root_node). 16 | //! 17 | //! The planner's search parameters are described by the [MCTSConfiguration] struct. 18 | //! 19 | //! The [MCTS] struct is generic over a [Domain], which you have to implement to describe your own planning domain. 20 | //! You need to implement at least these three methods: 21 | //! * [list_behaviors](Domain::list_behaviors) returns the possible actions employing a hierarchical [Behavior] abstraction. 22 | //! * [get_current_value](Domain::get_current_value) returns the instantaneous (not discounted) value of an agent in a given state. 23 | //! * [update_visible_agents](Domain::update_visible_agents) lists all agents visible from a given agent in a given state. 24 | //! 25 | //! The `graphviz` feature enables to output the search tree in the Graphviz's dot format using the [plot_mcts_tree](graphviz::plot_mcts_tree) function. 26 | //! 27 | //! Additional features and utilites such as execution loops are available in the [`npc-engine-utils`](https://crates.io/crates/npc-engine-utils/) crate. 28 | //! You might want to use them in your project as they make the planner significantly simpler to use. 29 | //! Most [examples](https://github.com/ethz-gtc/npc-engine/tree/main/npc-engine-core/examples) use them. 30 | 31 | mod active_task; 32 | mod behavior; 33 | mod config; 34 | mod context; 35 | mod domain; 36 | mod edge; 37 | mod mcts; 38 | mod node; 39 | mod state_diff; 40 | mod task; 41 | mod util; 42 | 43 | pub use active_task::*; 44 | pub use behavior::*; 45 | pub use config::*; 46 | pub use context::*; 47 | pub use domain::*; 48 | pub use edge::*; 49 | pub use mcts::*; 50 | pub use node::*; 51 | pub use state_diff::*; 52 | pub use task::*; 53 | use util::*; 54 | 55 | /// The identifier of an agent, essentially a u32. 56 | #[derive( 57 | Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize, 58 | )] 59 | #[serde(rename_all = "kebab-case")] 60 | pub struct AgentId( 61 | /// The internal identifier 62 | pub u32, 63 | ); 64 | impl std::fmt::Display for AgentId { 65 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 66 | write!(f, "A{}", self.0) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /npc-engine-core/src/state_diff.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use std::{fmt, mem, ops::Deref}; 7 | 8 | use crate::Domain; 9 | 10 | /// A joint reference to an initial state and a difference to this state. 11 | pub struct StateDiffRef<'a, D: Domain> { 12 | /// Initial state 13 | pub initial_state: &'a D::State, 14 | /// Difference to the initial state 15 | pub diff: &'a D::Diff, 16 | } 17 | impl Copy for StateDiffRef<'_, D> {} 18 | impl Clone for StateDiffRef<'_, D> { 19 | fn clone(&self) -> Self { 20 | *self 21 | } 22 | } 23 | impl<'a, D: Domain> StateDiffRef<'a, D> { 24 | pub fn new(initial_state: &'a D::State, diff: &'a D::Diff) -> Self { 25 | StateDiffRef { 26 | initial_state, 27 | diff, 28 | } 29 | } 30 | } 31 | 32 | impl fmt::Debug for StateDiffRef<'_, D> 33 | where 34 | D::State: fmt::Debug, 35 | D::Diff: fmt::Debug, 36 | { 37 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 38 | f.debug_struct("SnapshotDiffRef") 39 | .field("Snapshot", self.initial_state) 40 | .field("Diff", self.diff) 41 | .finish() 42 | } 43 | } 44 | 45 | /// A joint reference to an initial state and a difference to this state, where the difference is mutable. 46 | pub struct StateDiffRefMut<'a, D: Domain> { 47 | /// Initial state 48 | pub initial_state: &'a D::State, 49 | /// Difference (mutable) to the initial state 50 | pub diff: &'a mut D::Diff, 51 | } 52 | impl<'a, D: Domain> StateDiffRefMut<'a, D> { 53 | pub fn new(initial_state: &'a D::State, diff: &'a mut D::Diff) -> Self { 54 | StateDiffRefMut { 55 | initial_state, 56 | diff, 57 | } 58 | } 59 | } 60 | 61 | impl<'a, D: Domain> Deref for StateDiffRefMut<'a, D> { 62 | type Target = StateDiffRef<'a, D>; 63 | 64 | fn deref(&self) -> &Self::Target { 65 | // Safety: StateDiffRef and StateDiffRefMut have the same memory layout 66 | // and casting from mutable to immutable is always safe 67 | unsafe { mem::transmute(self) } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /npc-engine-core/src/util.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | #[allow(deprecated)] 7 | use std::hash::BuildHasher; 8 | 9 | use rustc_hash::FxHasher; 10 | 11 | /// A seed for seeded hash maps and sets. 12 | const SEED: u64 = 6364136223846793005; 13 | 14 | /// An helper struct to carry a seed to HashMaps and HashSets. 15 | #[derive(Copy, Clone, Debug)] 16 | pub(crate) struct SeededRandomState { 17 | seed: u64, 18 | } 19 | 20 | impl Default for SeededRandomState { 21 | fn default() -> Self { 22 | SeededRandomState { seed: SEED } 23 | } 24 | } 25 | 26 | #[allow(deprecated)] 27 | impl BuildHasher for SeededRandomState { 28 | type Hasher = FxHasher; 29 | 30 | fn build_hasher(&self) -> Self::Hasher { 31 | FxHasher::with_seed(self.seed as usize) 32 | } 33 | } 34 | 35 | /// An `HashMap` with a defined seed. 36 | pub(crate) type SeededHashMap = std::collections::HashMap; 37 | /// An `HashSet` with a defined seed. 38 | #[cfg(feature = "graphviz")] 39 | pub(crate) type SeededHashSet = std::collections::HashSet; 40 | -------------------------------------------------------------------------------- /npc-engine-core/tests/branching_tests.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use std::{collections::BTreeSet, fmt, hash::Hash, ops::Range}; 7 | 8 | use npc_engine_core::{ 9 | impl_task_boxed_methods, AgentId, AgentValue, Behavior, Context, ContextMut, Domain, 10 | MCTSConfiguration, StateDiffRef, Task, TaskDuration, MCTS, 11 | }; 12 | struct TestEngine; 13 | 14 | #[derive(Debug)] 15 | struct State(u16); 16 | 17 | #[derive(Debug, Default, Eq, Hash, Clone, PartialEq)] 18 | struct Diff(u16); 19 | 20 | #[derive(Debug, Default)] 21 | struct DisplayAction; 22 | impl fmt::Display for DisplayAction { 23 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 24 | write!(f, "") 25 | } 26 | } 27 | 28 | impl Domain for TestEngine { 29 | type State = State; 30 | type Diff = Diff; 31 | type DisplayAction = DisplayAction; 32 | 33 | fn list_behaviors() -> &'static [&'static dyn Behavior] { 34 | &[&TestBehaviorA, &TestBehaviorB] 35 | } 36 | 37 | fn get_current_value( 38 | _tick: u64, 39 | state_diff: StateDiffRef, 40 | _agent: AgentId, 41 | ) -> AgentValue { 42 | (state_diff.initial_state.0 + state_diff.diff.0).into() 43 | } 44 | 45 | fn update_visible_agents( 46 | _start_tick: u64, 47 | ctx: Context, 48 | agents: &mut BTreeSet, 49 | ) { 50 | agents.insert(ctx.agent); 51 | } 52 | } 53 | 54 | #[derive(Copy, Clone, Debug)] 55 | struct TestBehaviorA; 56 | 57 | impl Behavior for TestBehaviorA { 58 | fn add_own_tasks(&self, _ctx: Context, tasks: &mut Vec>>) { 59 | tasks.push(Box::new(TestTask(true)) as _); 60 | } 61 | 62 | fn is_valid(&self, _ctx: Context) -> bool { 63 | true 64 | } 65 | } 66 | 67 | #[derive(Copy, Clone, Debug)] 68 | struct TestBehaviorB; 69 | 70 | impl Behavior for TestBehaviorB { 71 | fn add_own_tasks(&self, _ctx: Context, tasks: &mut Vec>>) { 72 | tasks.push(Box::new(TestTask(false)) as _); 73 | } 74 | 75 | fn is_valid(&self, _ctx: Context) -> bool { 76 | true 77 | } 78 | } 79 | 80 | #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] 81 | struct TestTask(bool); 82 | 83 | impl Task for TestTask { 84 | fn weight(&self, _ctx: Context) -> f32 { 85 | 1. 86 | } 87 | 88 | fn duration(&self, _ctx: Context) -> TaskDuration { 89 | 1 90 | } 91 | 92 | fn is_valid(&self, _ctx: Context) -> bool { 93 | true 94 | } 95 | 96 | fn execute(&self, ctx: ContextMut) -> Option>> { 97 | ctx.state_diff.diff.0 += 1; 98 | None 99 | } 100 | 101 | fn display_action(&self) -> ::DisplayAction { 102 | DisplayAction 103 | } 104 | 105 | impl_task_boxed_methods!(TestEngine); 106 | } 107 | 108 | const EPSILON: f32 = 0.001; 109 | 110 | #[test] 111 | fn ucb() { 112 | const CONFIG: MCTSConfiguration = MCTSConfiguration { 113 | allow_invalid_tasks: false, 114 | visits: 10, 115 | depth: 1, 116 | exploration: 1.414, 117 | discount_hl: 15., 118 | seed: None, 119 | planning_task_duration: None, 120 | }; 121 | env_logger::init(); 122 | let agent = AgentId(0); 123 | 124 | let state = State(Default::default()); 125 | let mut mcts = MCTS::::new(state, agent, CONFIG); 126 | 127 | let task = mcts.run().unwrap(); 128 | assert!(task.downcast_ref::().is_some()); 129 | assert_eq!((CONFIG.depth * 2 + 1) as usize, mcts.node_count()); 130 | 131 | let node = mcts.root_node(); 132 | let edges = mcts.get_edges(&node).unwrap(); 133 | let root_visits = edges.child_visits(); 134 | 135 | let edge_a = edges 136 | .get_edge(&(Box::new(TestTask(true)) as Box>)) 137 | .unwrap(); 138 | let edge_a = edge_a.lock().unwrap(); 139 | let edge_b = edges 140 | .get_edge(&(Box::new(TestTask(false)) as Box>)) 141 | .unwrap(); 142 | let edge_b = edge_b.lock().unwrap(); 143 | 144 | assert!( 145 | (edge_a.uct( 146 | AgentId(0), 147 | root_visits, 148 | CONFIG.exploration, 149 | Range { 150 | start: AgentValue::new(0.0).unwrap(), 151 | end: AgentValue::new(1.0).unwrap() 152 | } 153 | ) - 1.9597) 154 | .abs() 155 | < EPSILON 156 | ); 157 | assert!( 158 | (edge_b.uct( 159 | AgentId(0), 160 | root_visits, 161 | CONFIG.exploration, 162 | Range { 163 | start: AgentValue::new(0.0).unwrap(), 164 | end: AgentValue::new(1.0).unwrap() 165 | } 166 | ) - 1.9597) 167 | .abs() 168 | < EPSILON 169 | ); 170 | } 171 | -------------------------------------------------------------------------------- /npc-engine-core/tests/seeding_tests.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use std::{collections::BTreeSet, fmt, hash::Hash}; 7 | 8 | use npc_engine_core::{ 9 | impl_task_boxed_methods, AgentId, AgentValue, Behavior, Context, ContextMut, Domain, 10 | MCTSConfiguration, StateDiffRef, Task, TaskDuration, MCTS, 11 | }; 12 | use rand::{thread_rng, RngCore}; 13 | 14 | struct TestEngine; 15 | 16 | #[derive(Debug, Clone, Copy)] 17 | struct State(u16); 18 | 19 | #[derive(Debug, Default, Eq, Hash, Clone, PartialEq)] 20 | struct Diff(u16); 21 | 22 | #[derive(Debug, Default)] 23 | struct DisplayAction; 24 | impl fmt::Display for DisplayAction { 25 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 26 | write!(f, "") 27 | } 28 | } 29 | 30 | impl Domain for TestEngine { 31 | type State = State; 32 | type Diff = Diff; 33 | type DisplayAction = DisplayAction; 34 | 35 | fn list_behaviors() -> &'static [&'static dyn Behavior] { 36 | &[&TestBehavior] 37 | } 38 | 39 | fn get_current_value( 40 | _tick: u64, 41 | state_diff: StateDiffRef, 42 | _agent: AgentId, 43 | ) -> AgentValue { 44 | (state_diff.initial_state.0 + state_diff.diff.0).into() 45 | } 46 | 47 | fn update_visible_agents( 48 | _start_tick: u64, 49 | ctx: Context, 50 | agents: &mut BTreeSet, 51 | ) { 52 | agents.insert(ctx.agent); 53 | } 54 | } 55 | 56 | #[derive(Copy, Clone, Debug)] 57 | struct TestBehavior; 58 | 59 | impl Behavior for TestBehavior { 60 | fn add_own_tasks(&self, _ctx: Context, tasks: &mut Vec>>) { 61 | for i in 0..10 { 62 | tasks.push(Box::new(TestTask(i)) as _); 63 | } 64 | } 65 | 66 | fn is_valid(&self, _ctx: Context) -> bool { 67 | true 68 | } 69 | } 70 | 71 | #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] 72 | struct TestTask(u16); 73 | 74 | impl Task for TestTask { 75 | fn weight(&self, _ctx: Context) -> f32 { 76 | 1. 77 | } 78 | 79 | fn duration(&self, _ctx: Context) -> TaskDuration { 80 | 1 81 | } 82 | 83 | fn is_valid(&self, _ctx: Context) -> bool { 84 | true 85 | } 86 | 87 | fn execute(&self, ctx: ContextMut) -> Option>> { 88 | ctx.state_diff.diff.0 += self.0.min(1); 89 | None 90 | } 91 | 92 | fn display_action(&self) -> ::DisplayAction { 93 | DisplayAction 94 | } 95 | 96 | impl_task_boxed_methods!(TestEngine); 97 | } 98 | 99 | #[test] 100 | fn seed() { 101 | env_logger::init(); 102 | let agent = AgentId(0); 103 | for _ in 0..5 { 104 | let seed = thread_rng().next_u64(); 105 | let config = MCTSConfiguration { 106 | allow_invalid_tasks: false, 107 | visits: 1000, 108 | depth: 10, 109 | exploration: 1.414, 110 | discount_hl: 15., 111 | seed: Some(seed), 112 | planning_task_duration: None, 113 | }; 114 | let state = State(Default::default()); 115 | let mut mcts = MCTS::::new(state, agent, config.clone()); 116 | 117 | let result = mcts.run(); 118 | 119 | for _ in 0..10 { 120 | let mut mcts = MCTS::::new(state, agent, config.clone()); 121 | 122 | assert!(result == mcts.run()); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /npc-engine-core/tests/value_tests.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use std::{collections::BTreeSet, fmt, hash::Hash}; 7 | 8 | use npc_engine_core::{ 9 | impl_task_boxed_methods, AgentId, AgentValue, Behavior, Context, ContextMut, Domain, 10 | MCTSConfiguration, StateDiffRef, Task, TaskDuration, MCTS, 11 | }; 12 | 13 | pub(crate) struct TestEngine; 14 | 15 | #[derive(Debug)] 16 | pub(crate) struct State(u16); 17 | 18 | #[derive(Debug, Default, Eq, Hash, Clone, PartialEq)] 19 | pub(crate) struct Diff(u16); 20 | 21 | #[derive(Debug, Default)] 22 | struct DisplayAction; 23 | impl fmt::Display for DisplayAction { 24 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 25 | write!(f, "") 26 | } 27 | } 28 | 29 | impl Domain for TestEngine { 30 | type State = State; 31 | type Diff = Diff; 32 | type DisplayAction = DisplayAction; 33 | 34 | fn list_behaviors() -> &'static [&'static dyn Behavior] { 35 | &[&TestBehavior] 36 | } 37 | 38 | fn get_current_value( 39 | _tick: u64, 40 | state_diff: StateDiffRef, 41 | _agent: AgentId, 42 | ) -> AgentValue { 43 | (state_diff.initial_state.0 + state_diff.diff.0).into() 44 | } 45 | 46 | fn update_visible_agents(_start_tick: u64, ctx: Context, agents: &mut BTreeSet) { 47 | agents.insert(ctx.agent); 48 | } 49 | } 50 | 51 | #[derive(Copy, Clone, Debug)] 52 | struct TestBehavior; 53 | 54 | impl Behavior for TestBehavior { 55 | fn add_own_tasks(&self, _ctx: Context, tasks: &mut Vec>>) { 56 | tasks.push(Box::new(TestTask) as _); 57 | } 58 | 59 | fn is_valid(&self, _ctx: Context) -> bool { 60 | true 61 | } 62 | } 63 | 64 | #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] 65 | struct TestTask; 66 | 67 | impl Task for TestTask { 68 | fn weight(&self, _ctx: Context) -> f32 { 69 | 1. 70 | } 71 | 72 | fn duration(&self, _ctx: Context) -> TaskDuration { 73 | 1 74 | } 75 | 76 | fn is_valid(&self, _ctx: Context) -> bool { 77 | true 78 | } 79 | 80 | fn execute(&self, ctx: ContextMut) -> Option>> { 81 | ctx.state_diff.diff.0 += 1; 82 | None 83 | } 84 | 85 | fn display_action(&self) -> ::DisplayAction { 86 | DisplayAction 87 | } 88 | 89 | impl_task_boxed_methods!(TestEngine); 90 | } 91 | 92 | const EPSILON: f32 = 0.001; 93 | 94 | #[test] 95 | fn linear_bellman() { 96 | const CONFIG: MCTSConfiguration = MCTSConfiguration { 97 | allow_invalid_tasks: false, 98 | visits: 10_000, 99 | depth: 5, 100 | exploration: 1.414, 101 | discount_hl: 15., 102 | seed: None, 103 | planning_task_duration: None, 104 | }; 105 | env_logger::init(); 106 | let agent = AgentId(0); 107 | 108 | let world = State(0); 109 | let mut mcts = MCTS::::new(world, agent, CONFIG); 110 | 111 | fn expected_value(discount: f32, depth: u32) -> f32 { 112 | let discount = |delta| 2f64.powf((-(delta as f64)) / (discount as f64)) as f32; 113 | let mut value = 0.; 114 | 115 | for _ in 0..depth { 116 | value = 1. + discount(1) * value; 117 | } 118 | 119 | value 120 | } 121 | 122 | let task = mcts.run().unwrap(); 123 | assert!(task.downcast_ref::().is_some()); 124 | // Check length is depth with root 125 | assert_eq!((CONFIG.depth + 1) as usize, mcts.node_count()); 126 | 127 | let mut node = mcts.root_node(); 128 | 129 | { 130 | assert_eq!(Diff(0), *node.diff()); 131 | } 132 | 133 | for i in 1..CONFIG.depth { 134 | let edges = mcts.get_edges(&node).unwrap(); 135 | assert_eq!(edges.expanded_count(), 1); 136 | let edge_rc = edges 137 | .get_edge(&(Box::new(TestTask) as Box>)) 138 | .unwrap(); 139 | let edge = edge_rc.lock().unwrap(); 140 | 141 | node = edge.child(); 142 | 143 | assert_eq!(Diff(i as u16), *node.diff()); 144 | assert_eq!((CONFIG.visits - i + 1) as usize, edge.visits()); 145 | assert!( 146 | (expected_value(CONFIG.discount_hl, CONFIG.depth - i + 1) - edge.q_value(agent)).abs() 147 | < EPSILON 148 | ); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /npc-engine-utils/AUTHORS.txt: -------------------------------------------------------------------------------- 1 | ../AUTHORS.txt -------------------------------------------------------------------------------- /npc-engine-utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "npc-engine-utils" 3 | version = "0.1.0" 4 | authors = ["Sven Knobloch ", "David Enderlin ", "Stéphane Magnenat "] 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | description = "The utility module of the NPC engine, providing re-usable support code" 8 | repository = "https://github.com/ethz-gtc/npc-engine" 9 | homepage = "https://github.com/ethz-gtc/npc-engine" 10 | readme = "README.md" 11 | keywords = ["MCTS", "AI", "multi-agent", "simulation", "game"] 12 | categories = ["algorithms", "science", "simulation", "game-development"] 13 | rust-version = "1.62" 14 | 15 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 16 | 17 | [dependencies] 18 | npc-engine-core = { version = "0.1", path = "../npc-engine-core", features = ["graphviz"] } 19 | log = "0.4" 20 | ansi_term = "0.12" 21 | rand = "0.8" 22 | serde = { version = "1", features = [ "derive" ] } 23 | 24 | [dev-dependencies] 25 | env_logger = "0.9.0" -------------------------------------------------------------------------------- /npc-engine-utils/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /npc-engine-utils/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /npc-engine-utils/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /npc-engine-utils/images/learn_wood_collected_over_epochs.png: -------------------------------------------------------------------------------- 1 | ../../images/learn_wood_collected_over_epochs.png -------------------------------------------------------------------------------- /npc-engine-utils/src/direction.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use serde::Serialize; 7 | use std::{fmt, marker::PhantomData}; 8 | 9 | use crate::Coord2D; 10 | 11 | /// A helper trait that tells whether up and down are positive or negative. 12 | pub trait YUpDown { 13 | /// Returns 1 or -1 depending on the up direction. 14 | fn up() -> i32; 15 | /// Returns 1 or -1 depending on the down direction. 16 | fn down() -> i32; 17 | } 18 | 19 | /// Up is positive. 20 | pub struct YUp; 21 | impl YUpDown for YUp { 22 | fn up() -> i32 { 23 | 1 24 | } 25 | fn down() -> i32 { 26 | -1 27 | } 28 | } 29 | 30 | /// Down is positive. 31 | pub struct YDown; 32 | impl YUpDown for YDown { 33 | fn up() -> i32 { 34 | -1 35 | } 36 | fn down() -> i32 { 37 | 1 38 | } 39 | } 40 | 41 | /// A direction type. 42 | /// 43 | /// Directions can be applied to [Coord2D] through the help of a [DirectionConverter], 44 | /// which decides whether up is positive or negative y. 45 | #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize)] 46 | #[serde(rename_all = "kebab-case")] 47 | pub enum Direction { 48 | Up, 49 | Down, 50 | Left, 51 | Right, 52 | } 53 | 54 | impl fmt::Display for Direction { 55 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 56 | match self { 57 | Direction::Up => write!(f, "Up"), 58 | Direction::Down => write!(f, "Down"), 59 | Direction::Left => write!(f, "Left"), 60 | Direction::Right => write!(f, "Right"), 61 | } 62 | } 63 | } 64 | 65 | /// A helper struct to apply direction to coordinates. 66 | /// 67 | /// If used as DirectionConverter<[YUp]>, y will be positive up, 68 | /// and if used as DirectionConverter<[YDown]>, y will be negative up. 69 | pub struct DirectionConverter { 70 | _phantom: PhantomData, 71 | } 72 | impl DirectionConverter { 73 | /// Moves `coord` by `direction`. 74 | pub fn apply(direction: Direction, coord: Coord2D) -> Coord2D { 75 | let Coord2D { x, y } = coord; 76 | let (x, y) = match direction { 77 | Direction::Up => (x, y + YDir::up()), 78 | Direction::Down => (x, y + YDir::down()), 79 | Direction::Left => (x - 1, y), 80 | Direction::Right => (x + 1, y), 81 | }; 82 | Coord2D::new(x, y) 83 | } 84 | 85 | /// Gets the direction between `start` and `end`, panics if they are not adjacent. 86 | pub fn from(start: Coord2D, end: Coord2D) -> Direction { 87 | let dx = end.x - start.x; 88 | let dy = end.y - start.y; 89 | match (dx, dy) { 90 | (1, _) => Direction::Right, 91 | (-1, _) => Direction::Left, 92 | (_, 1) => match YDir::up() == 1 { 93 | true => Direction::Up, 94 | false => Direction::Down, 95 | }, 96 | (_, -1) => match YDir::down() == -1 { 97 | true => Direction::Down, 98 | false => Direction::Up, 99 | }, 100 | _ => panic!("start and end positions are not next to each others with 4-connectivity"), 101 | } 102 | } 103 | } 104 | 105 | /// All directions. 106 | pub const DIRECTIONS: [Direction; 4] = [ 107 | Direction::Up, 108 | Direction::Right, 109 | Direction::Down, 110 | Direction::Left, 111 | ]; 112 | /// Apply direction to coordinates with up being positive. 113 | pub type DirectionConverterYUp = DirectionConverter; 114 | /// Apply direction to coordinates with up being negative. 115 | pub type DirectionConverterYDown = DirectionConverter; 116 | -------------------------------------------------------------------------------- /npc-engine-utils/src/functional.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | /// Returns a reference to the first element of a tuple reference. 7 | pub fn keep_first(tuple: &(A, B)) -> &A { 8 | &tuple.0 9 | } 10 | 11 | /// Returns a reference to the second element of a tuple reference. 12 | pub fn keep_second(tuple: &(A, B)) -> &B { 13 | &tuple.1 14 | } 15 | 16 | /// Returns a mutable reference to the first element of a mutable tuple reference. 17 | pub fn keep_first_mut(tuple: &mut (A, B)) -> &mut A { 18 | &mut tuple.0 19 | } 20 | 21 | /// Returns a mutable reference to the second element of a mutable tuple reference. 22 | pub fn keep_second_mut(tuple: &mut (A, B)) -> &mut B { 23 | &mut tuple.1 24 | } 25 | -------------------------------------------------------------------------------- /npc-engine-utils/src/global_domain.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use npc_engine_core::{AgentId, Domain}; 7 | 8 | /// A domain that provides a global state, out of which a local state for the planning can be derived. 9 | pub trait GlobalDomain: Domain { 10 | /// Global state: all data that can change in the course of the simulation. 11 | type GlobalState: std::fmt::Debug + Sized + 'static; 12 | 13 | /// Derives a new local state for the given agent from the given global state. 14 | fn derive_local_state(global_state: &Self::GlobalState, agent: AgentId) -> Self::State; 15 | 16 | /// Applies a diff from a local state to the global state. 17 | fn apply(global_state: &mut Self::GlobalState, local_state: &Self::State, diff: &Self::Diff); 18 | } 19 | -------------------------------------------------------------------------------- /npc-engine-utils/src/graphs.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use npc_engine_core::{debug_name_to_filename_safe, graphviz, ActiveTask, Domain, MCTS}; 7 | use std::fs; 8 | 9 | /// Plots the MCTS tree using graphviz's dot format. 10 | pub fn plot_tree_in_tmp( 11 | mcts: &MCTS, 12 | base_dir_name: &str, 13 | file_name: &str, 14 | ) -> std::io::Result<()> { 15 | let temp_dir = std::env::temp_dir().display().to_string(); 16 | let path = format!("{temp_dir}/{base_dir_name}/"); 17 | fs::create_dir_all(&path)?; 18 | let mut file = fs::OpenOptions::new() 19 | .create(true) 20 | .write(true) 21 | .truncate(true) 22 | .open(format!("{path}{file_name}.dot"))?; 23 | graphviz::plot_mcts_tree(mcts, &mut file) 24 | } 25 | 26 | /// Plots the MCTS tree using graphviz's dot format, with a filename derived from an active task. 27 | pub fn plot_tree_in_tmp_with_task_name( 28 | mcts: &MCTS, 29 | base_dir_name: &str, 30 | last_active_task: &ActiveTask, 31 | ) -> std::io::Result<()> { 32 | let time_text = format!("T{}", mcts.start_tick()); 33 | let agent_id_text = format!("A{}", mcts.agent().0); 34 | let last_task_name = format!("{:?}", last_active_task.task); 35 | let last_task_name = debug_name_to_filename_safe(&last_task_name); 36 | plot_tree_in_tmp( 37 | mcts, 38 | base_dir_name, 39 | &format!("{agent_id_text}-{time_text}-{last_task_name}"), 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /npc-engine-utils/src/lib.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | //! This is the utility module of the [NPC engine](https://crates.io/crates/npc-engine-core/), containing helpful utility code. 7 | //! 8 | //! It contains the following features: 9 | //! - A helper trait [OptionDiffDomain] that can be used when [Diffs](Domain::Diff) are just copies of the [State](Domain::State). 10 | //! - Two executors (update loops), [SimpleExecutor] and [ThreadedExecutor], that implement the execution logic of a [Domain] beyond planning itself, and related abstractions. 11 | //! - A simple implementation of feed-forward leaky ReLU neurons ([Neuron]) and corresponding simple networks ([NeuralNetwork]), providing learning based on back-propagation ([NeuralNetwork::train]). 12 | //! - Simple 2-D coordinates ([Coord2D]) and direction ([Direction]) implementations. 13 | //! - Helper functions to plot search trees: [plot_tree_in_tmp] and [plot_tree_in_tmp_with_task_name]. 14 | //! - Helper functions to simplify functional programming with tuples: [keep_first] and [keep_second], and their mutable versions [keep_first_mut] and [keep_second_mut]. 15 | 16 | #[cfg(doc)] 17 | use npc_engine_core::Domain; 18 | 19 | mod coord2d; 20 | mod direction; 21 | mod executor; 22 | mod functional; 23 | mod global_domain; 24 | mod graphs; 25 | mod neuron; 26 | mod option_state_diff; 27 | 28 | pub use coord2d::*; 29 | pub use direction::*; 30 | pub use executor::*; 31 | pub use functional::*; 32 | pub use global_domain::*; 33 | pub use graphs::*; 34 | pub use neuron::*; 35 | pub use option_state_diff::*; 36 | -------------------------------------------------------------------------------- /npc-engine-utils/src/option_state_diff.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use npc_engine_core::{Domain, StateDiffRef, StateDiffRefMut}; 7 | use std::hash::Hash; 8 | 9 | /// In case your domain has a [Diff](Domain::Diff) that is an [Option] 10 | /// of its [State](Domain::State), 11 | /// provides helper functions to retrieve the state in your [tasks](npc_engine_core::Task). 12 | /// 13 | /// The functions [get_cur_state](Self::get_cur_state) and [get_cur_state_mut](Self::get_cur_state_mut) 14 | /// are available when read-only, respectively read-write, access is required. 15 | /// In that case, just use the trait in your task files: `use npc_engine_utils::OptionDiffDomain;`. 16 | pub trait OptionDiffDomain { 17 | type Domain: Domain>; 18 | type State: Clone; 19 | /// Returns either the `diff` if it is not `None`, or the `initial_state`. 20 | fn get_cur_state( 21 | state_diff: StateDiffRef, 22 | ) -> &<::Domain as Domain>::State { 23 | if let Some(diff) = state_diff.diff { 24 | diff 25 | } else { 26 | state_diff.initial_state 27 | } 28 | } 29 | /// Returns either the `diff` if it is not `None`, or copies the `initial_state` into the `diff` and returns it. 30 | fn get_cur_state_mut( 31 | state_diff: StateDiffRefMut, 32 | ) -> &mut <::Domain as Domain>::State { 33 | if let Some(diff) = state_diff.diff { 34 | diff 35 | } else { 36 | let diff = state_diff.initial_state.clone(); 37 | *state_diff.diff = Some(diff); 38 | &mut *state_diff.diff.as_mut().unwrap() 39 | } 40 | } 41 | } 42 | 43 | impl< 44 | S: std::fmt::Debug + Sized + Clone + Hash + Eq, 45 | DA: std::fmt::Debug + Default, 46 | D: Domain, DisplayAction = DA>, 47 | > OptionDiffDomain for D 48 | { 49 | type Domain = D; 50 | type State = ::State; 51 | } 52 | -------------------------------------------------------------------------------- /scenario-lumberjacks/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lumberjacks" 3 | version = "0.1.0" 4 | authors = ["Sven Knobloch ", "Stéphane Magnenat "] 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | description = "The lumberjack experiment from the ETH-GTC AIIDE 2020 INT paper" 8 | repository = "https://github.com/ethz-gtc/npc-engine" 9 | homepage = "https://github.com/ethz-gtc/npc-engine" 10 | readme = "README.md" 11 | keywords = ["MCTS", "AI", "multi-agent", "simulation", "game"] 12 | categories = ["algorithms", "science", "simulation", "game-development"] 13 | rust-version = "1.62" 14 | 15 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 16 | 17 | [dependencies] 18 | ggez = "~0.5" 19 | env_logger = "0.9.0" 20 | rand = "~0.7" 21 | palette = "~0.5" 22 | log = "~0.4" 23 | image = "~0.23" 24 | lazy_static = "^1.4" 25 | serde = { version = "^1", features = [ "derive" ] } 26 | serde_json = "^1" 27 | clap = "^2" 28 | dot = "~0.1" 29 | partitions = "~0.2" 30 | rayon = "^1.3" 31 | num-traits = "*" 32 | 33 | npc-engine-core = { version = "0.1", path = "../npc-engine-core", features = [ "graphviz" ] } 34 | npc-engine-utils = { version = "0.1", path = "../npc-engine-utils" } 35 | 36 | [lib] 37 | path = "./src/lib.rs" 38 | 39 | [[bin]] 40 | name = "lumberjacks" 41 | path = "./src/bin/lumberjacks.rs" 42 | 43 | [[bin]] 44 | name = "lumberjacks-experiment" 45 | path = "./src/bin/experiment.rs" 46 | -------------------------------------------------------------------------------- /scenario-lumberjacks/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /scenario-lumberjacks/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /scenario-lumberjacks/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /scenario-lumberjacks/assets/ImpassableRock.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:4b4e359b8c3c7261eba4d671857383d0f1ca645f610beca836012bd9a853fbac 3 | size 890 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/assets/OrangeDown.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:3d52cbd0dca76cc57971b41b878c175bcec9dc636f8d66dfa187ea13cbfca624 3 | size 552 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/assets/OrangeDownBarrier.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:de6a581d14387df11f87e61c0032f8b141a3ca4543f247e797dabba805349fa7 3 | size 586 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/assets/OrangeDownChopping.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:e0d820a419433396ddc37eb88ebff93f2fc7c73a82fd3b11d8462d2578c8b9fc 3 | size 529 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/assets/OrangeLeft.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:552cb773fd3d43d6bb63d452cfd5d64f82b5a49ec17e3b36a3eb1b4fb8c12b7a 3 | size 537 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/assets/OrangeLeftBarrier.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:3db3d14b2ba48891848eacd8715cb2f3bde2e9c8393f61c53a1bba06be98110b 3 | size 739 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/assets/OrangeLeftChopping.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:6c302677cee6bcc9720e6218cdef9487b3411228d7aec44488fddae3dfafe7ef 3 | size 574 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/assets/OrangeRight.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:2d7c20ecb6c1bc73dfc882de110eaf9095d03e5525009d3352d652cce3ce74b4 3 | size 556 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/assets/OrangeRightBarrier.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:3a1d089644e3d6f0127c1f192a2af7b20cf5a54a89ffb5292cba43c23e635d3b 3 | size 724 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/assets/OrangeRightChopping.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:7a05a45a1d8489a1aebba9dd99c060fa2e4eb234ab2a748dbb155322cde6383a 3 | size 535 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/assets/OrangeTop.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:fc3e8e658542082028f5b276a77b92a5817738ea83b51393fa22991cb5d8df65 3 | size 536 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/assets/OrangeTopBarrier.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:2cace4b2b44aba8f60c9a178aa4954219a83cbde1b9eea52fa2d815288593de9 3 | size 615 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/assets/OrangeTopChopping.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:e475fb7000232c0f10f1350701d2ec401fbac44b18a7abd7fa41df39114b82c7 3 | size 440 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/assets/Tree1_3.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:10cd80e12c0bc106db872e54c4b9e34b9ebc07022a4960b2bc14999d069110a7 3 | size 880 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/assets/Tree2_3.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:85b863db7461491ee35b934c91516c1a0f7782cc61001a72378149a56dbe70f3 3 | size 1622 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/assets/Tree3_3.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:ac18ecd54655500b1b5b5f26f936a0d8c3f88466d48dcb813bbf3e38df4be021 3 | size 1882 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/assets/TreeSapling.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:beebf3a738e5890d355024e6ee7d077a91d81a5e7c964d882d1c50d65e96e80e 3 | size 360 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/assets/Well.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:756e42b01d98de936def131739506857368d9e42473fb6ca372a8ab9cc6ac4f3 3 | size 1091 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/assets/WoodenBarrier.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:35549c06ed675841606fa636aaaf888b027ad284ef56035d05639a2cb5a81498 3 | size 908 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/assets/YellowDown.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:e6f7b98a58b69261b07d316e270573245b30b2aa3517d8a46b24c799336457ac 3 | size 659 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/assets/YellowDownBarrier.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:ac2317bd937e56bd77cf93a625ac7c71186bc9ce282b050bcbd42f65c8916037 3 | size 695 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/assets/YellowDownChopping.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:acbc8b2c146323610958030f7cdd30db9e74dc3d747a19eeb2e220a85f8739a2 3 | size 705 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/assets/YellowLeft.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:df85c4ebffbf14bc8c5a2937d9a157f730034ae1d7b7dee650bf27e3ccf16f0a 3 | size 757 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/assets/YellowLeftBarrier.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:5e162d1585341f9d3759a695fdb79625b0fc62b3b5be324408675c3a047c84f5 3 | size 833 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/assets/YellowLeftChopping.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:95ce080ea5719245532ef6469365f2e5adfe35f5570ab3d932b3de8ee2c20678 3 | size 842 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/assets/YellowRight.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:eec58aab007ce28981a620eecabc221808c64195bb0b1cf082791123d933e5d1 3 | size 792 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/assets/YellowRightBarrier.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:c7275b4aa828142a063441941717d53ff3291ce9505830c23b73e6fb1d4812a8 3 | size 814 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/assets/YellowRightChopping.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:3db111adc210d6e29db609f75fb7b8308f841751172e8bfd94167265471d712c 3 | size 818 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/assets/YellowTop.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:eca31736d385964581c5d66ab86769b523f9024fe1994f0b84b6d3878077922d 3 | size 570 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/assets/YellowTopBarrier.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:e57784a804fb8e748bf36782791c324633062706d82e38853bd9a22d474731ac 3 | size 670 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/assets/YellowTopChopping.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:68483f21c902a00e5738bd66e2983f31c07debf3bed3dc3f01d432a8e4e248bc 3 | size 619 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/build.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use std::process::Command; 7 | use std::str; 8 | 9 | fn main() { 10 | let output = Command::new("git") 11 | .args(["describe", "--always", "--dirty"]) 12 | .output() 13 | .unwrap(); 14 | 15 | let git_hash = str::from_utf8(&output.stdout).unwrap(); 16 | println!("cargo:rustc-env=GIT_HASH={}", git_hash); 17 | } 18 | -------------------------------------------------------------------------------- /scenario-lumberjacks/config.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$ref": "./schema.json#/definitions/config" 4 | } 5 | -------------------------------------------------------------------------------- /scenario-lumberjacks/configs/corridor.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../config.schema.json", 3 | "map": { 4 | "generator": { 5 | "file": { 6 | "path": "../maps/corridor.png" 7 | } 8 | }, 9 | "tree-height": 2 10 | }, 11 | "mcts": { 12 | "visits": 5000, 13 | "depth": 10, 14 | "update": { 15 | "bellman": { 16 | "discount": 0.99 17 | } 18 | } 19 | }, 20 | "agents": { 21 | "objectives": true 22 | }, 23 | "analytics": { 24 | "graphs": true 25 | }, 26 | "features": { 27 | "watering": true 28 | }, 29 | "display": { 30 | "interactive": true 31 | } 32 | } -------------------------------------------------------------------------------- /scenario-lumberjacks/experiment.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$ref": "./schema.json#/definitions/experiment" 4 | } -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/barrier/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../schema.json", 3 | "turns": 25, 4 | "batch": { 5 | "runs": 100 6 | }, 7 | "map": { 8 | "generator": { 9 | "file": { 10 | "path": "./map.png" 11 | } 12 | }, 13 | "tree-height": 3 14 | }, 15 | "features": { 16 | "barriers": true, 17 | "teamwork": false, 18 | "waiting": true 19 | }, 20 | "agents": { 21 | "horizon-radius": 5, 22 | "objectives": false, 23 | "plan-others": true, 24 | "snapshot-radius": 10 25 | }, 26 | "mcts": { 27 | "depth": 45, 28 | "exploration": 1.4142135623730951, 29 | "retention": 1.0, 30 | "discount": 0.95, 31 | "visits": 1000 32 | }, 33 | "display": { 34 | "background": [ 35 | 0.0, 36 | 0.44, 37 | 0.36 38 | ], 39 | "padding": [2, 2], 40 | "inventory": true 41 | }, 42 | "analytics": { 43 | "graphs": false, 44 | "heatmaps": false, 45 | "metrics": false, 46 | "screenshot": false, 47 | "serialization": false 48 | } 49 | } -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/barrier/experiment.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../experiment.schema.json#", 3 | "base": "./base.json", 4 | "trials": { 5 | "on-125": { 6 | "mcts": { 7 | "visits": 125 8 | } 9 | }, 10 | "on-250": { 11 | "mcts": { 12 | "visits": 250 13 | } 14 | }, 15 | "on-500": { 16 | "mcts": { 17 | "visits": 500 18 | } 19 | }, 20 | "on-1000": { 21 | "mcts": { 22 | "visits": 1000 23 | } 24 | }, 25 | "off-125": { 26 | "agents": { 27 | "plan-others": false 28 | }, 29 | "mcts": { 30 | "visits": 125 31 | } 32 | }, 33 | "off-250": { 34 | "agents": { 35 | "plan-others": false 36 | }, 37 | "mcts": { 38 | "visits": 250 39 | } 40 | }, 41 | "off-500": { 42 | "agents": { 43 | "plan-others": false 44 | }, 45 | "mcts": { 46 | "visits": 500 47 | } 48 | }, 49 | "off-1000": { 50 | "agents": { 51 | "plan-others": false 52 | }, 53 | "mcts": { 54 | "visits": 1000 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/barrier/map.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:5517d7a47d7d64264c793c272e366a828c6608280f42971cb1fa145a1094753c 3 | size 120 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/barrier/plot.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "inventory", 3 | "x_axis_label": "visit count", 4 | "x_axis_label_type": "int", 5 | "condition2_label": "plan others", 6 | "y_ticks": [0, 3, 6, 9, 12] 7 | } -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../config.schema.json", 3 | "headless": { 4 | "turns": 10 5 | }, 6 | "map": { 7 | "generator": { 8 | "file": { 9 | "path": "../maps/corridor3.png" 10 | } 11 | }, 12 | "tree-height": 3 13 | }, 14 | "mcts": { 15 | "exploration": 1.414, 16 | "visits": 10000, 17 | "update": { 18 | "bellman": { 19 | "discount": 0.95 20 | } 21 | } 22 | }, 23 | "analytics": { 24 | "serialization": true 25 | }, 26 | "features": { 27 | "barriers": true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/competition-basic/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../schema.json", 3 | "turns": 5, 4 | "batch": { 5 | "runs": 100 6 | }, 7 | "map": { 8 | "generator": { 9 | "file": { 10 | "path": "./map.png" 11 | } 12 | }, 13 | "tree-height": 1 14 | }, 15 | "features": { 16 | "barriers": false, 17 | "teamwork": false 18 | }, 19 | "agents": { 20 | "horizon-radius": 5, 21 | "objectives": false, 22 | "plan-others": true, 23 | "snapshot-radius": 10 24 | }, 25 | "mcts": { 26 | "depth": 45, 27 | "exploration": 1.4142135623730951, 28 | "retention": 1.0, 29 | "discount": 0.95, 30 | "visits": 1000 31 | }, 32 | "display": { 33 | "background": [ 34 | 0.0, 35 | 0.44, 36 | 0.36 37 | ], 38 | "padding": [2, 2], 39 | "inventory": true 40 | }, 41 | "analytics": { 42 | "graphs": false, 43 | "heatmaps": false, 44 | "metrics": false, 45 | "screenshot": false, 46 | "serialization": false 47 | } 48 | } -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/competition-basic/experiment.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../experiment.schema.json", 3 | "base": "./base.json", 4 | "trials": { 5 | "on": { 6 | "agents": { 7 | "plan-others": true 8 | } 9 | }, 10 | "off": { 11 | "agents": { 12 | "plan-others": false 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/competition-basic/map.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:cf855808a292b01144ece9eaa5567eb6957055e46fa6f86c89b938bd9f528bfe 3 | size 120 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/competition-basic/plot.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "inventory", 3 | "x_axis_label": "plan for others", 4 | "y_ticks": [0,1,2,3], 5 | "color": "forestgreen", 6 | "height": 0.8 7 | } -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/config.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$ref": "./schema.json#/definitions/config" 4 | } 5 | -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/corridor.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../experiment.schema.json#", 3 | "base": "./base.json", 4 | "trials": { 5 | "trial-1": {}, 6 | "trial-2": { 7 | "headless": { 8 | "turns": 15 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/depth-choice/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../schema.json", 3 | "turns": 1, 4 | "batch": { 5 | "runs": 100 6 | }, 7 | "map": { 8 | "generator": { 9 | "file": { 10 | "path": "./map.png" 11 | } 12 | }, 13 | "tree-height": 1 14 | }, 15 | "features": { 16 | "barriers": false, 17 | "teamwork": false 18 | }, 19 | "agents": { 20 | "horizon-radius": 5, 21 | "objectives": false, 22 | "plan-others": true, 23 | "snapshot-radius": 10 24 | }, 25 | "mcts": { 26 | "depth": 45, 27 | "exploration": 1.4142135623730951, 28 | "retention": 1.0, 29 | "discount": 0.95, 30 | "visits": 1000 31 | }, 32 | "display": { 33 | "background": [ 34 | 0.0, 35 | 0.44, 36 | 0.36 37 | ], 38 | "padding": [1, 0], 39 | "inventory": false 40 | }, 41 | "analytics": { 42 | "graphs": false, 43 | "heatmaps": false, 44 | "metrics": false, 45 | "screenshot": false, 46 | "serialization": false 47 | } 48 | } -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/depth-choice/experiment.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../experiment.schema.json", 3 | "base": "./base.json", 4 | "trials": { 5 | "depth-1": { 6 | "mcts": { 7 | "depth": 1, 8 | "visits": 100 9 | } 10 | }, 11 | "depth-2": { 12 | "mcts": { 13 | "depth": 2, 14 | "visits": 200 15 | } 16 | }, 17 | "depth-3": { 18 | "mcts": { 19 | "depth": 3, 20 | "visits": 400 21 | } 22 | }, 23 | "depth-4": { 24 | "mcts": { 25 | "depth": 4, 26 | "visits": 800 27 | } 28 | }, 29 | "depth-5": { 30 | "mcts": { 31 | "depth": 5, 32 | "visits": 1600 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/depth-choice/map.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:7f8973bd85df192e28c9518301162f5754b0f29ac2697b968c766d3e5d0a0e8a 3 | size 120 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/depth-choice/plot.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "agent_pos", 3 | "x_axis_label": "search depth", 4 | "color": "blue" 5 | } -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/depth/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../schema.json", 3 | "turns": 10, 4 | "batch": { 5 | "runs": 100 6 | }, 7 | "map": { 8 | "generator": { 9 | "file": { 10 | "path": "./map.png" 11 | } 12 | }, 13 | "tree-height": 3 14 | }, 15 | "features": { 16 | "barriers": false, 17 | "teamwork": false 18 | }, 19 | "agents": { 20 | "horizon-radius": 5, 21 | "objectives": false, 22 | "plan-others": true, 23 | "snapshot-radius": 10 24 | }, 25 | "mcts": { 26 | "depth": 45, 27 | "exploration": 1.4142135623730951, 28 | "retention": 1.0, 29 | "discount": 0.95, 30 | "visits": 1000 31 | }, 32 | "display": { 33 | "background": [ 34 | 0.0, 35 | 0.44, 36 | 0.36 37 | ], 38 | "padding": [0, 1], 39 | "inventory": false 40 | }, 41 | "analytics": { 42 | "graphs": false, 43 | "heatmaps": false, 44 | "metrics": false, 45 | "screenshot": false, 46 | "serialization": false 47 | } 48 | } -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/depth/experiment.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../experiment.schema.json", 3 | "base": "./base.json", 4 | "trials": { 5 | "depth-1": { 6 | "mcts": { 7 | "depth": 1, 8 | "visits": 100 9 | } 10 | }, 11 | "depth-2": { 12 | "mcts": { 13 | "depth": 2, 14 | "visits": 200 15 | } 16 | }, 17 | "depth-3": { 18 | "mcts": { 19 | "depth": 3, 20 | "visits": 400 21 | } 22 | }, 23 | "depth-4": { 24 | "mcts": { 25 | "depth": 4, 26 | "visits": 800 27 | } 28 | }, 29 | "depth-5": { 30 | "mcts": { 31 | "depth": 5, 32 | "visits": 1600 33 | } 34 | }, 35 | "depth-6": { 36 | "mcts": { 37 | "depth": 6, 38 | "visits": 3200 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/depth/map.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:8f4076671e439095b313a68424cd3a14e9b4d24b93ce36b6744576baa273d648 3 | size 147 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/depth/plot.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "inventory", 3 | "x_axis_label": "search depth", 4 | "color": "forestgreen" 5 | } -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/experiment.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$ref": "./schema.json#/definitions/experiment" 4 | } -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/optimization/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../schema.json", 3 | "turns": 30, 4 | "batch": { 5 | "runs": 100 6 | }, 7 | "map": { 8 | "generator": { 9 | "file": { 10 | "path": "./map.png" 11 | } 12 | }, 13 | "tree-height": 3 14 | }, 15 | "features": { 16 | "barriers": false, 17 | "teamwork": false, 18 | "watering": true, 19 | "planting": false 20 | }, 21 | "agents": { 22 | "horizon-radius": 5, 23 | "objectives": false, 24 | "plan-others": true, 25 | "snapshot-radius": 10 26 | }, 27 | "mcts": { 28 | "depth": 45, 29 | "exploration": 1.4142135623730951, 30 | "retention": 1.0, 31 | "discount": 0.95, 32 | "visits": 5000 33 | }, 34 | "display": { 35 | "background": [ 36 | 0.0, 37 | 0.44, 38 | 0.36 39 | ], 40 | "padding": [2, 2], 41 | "inventory": true 42 | }, 43 | "analytics": { 44 | "graphs": false, 45 | "heatmaps": false, 46 | "metrics": false, 47 | "screenshot": false, 48 | "serialization": false 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/optimization/experiment.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../experiment.schema.json", 3 | "base": "./base.json", 4 | "trials": { 5 | "off-0.1": { 6 | "mcts": { 7 | "depth": 1, 8 | "discount": 0.1 9 | }, 10 | "features": { 11 | "planting": false 12 | } 13 | }, 14 | "off-0.3": { 15 | "mcts": { 16 | "depth": 2, 17 | "discount": 0.3 18 | }, 19 | "features": { 20 | "planting": false 21 | } 22 | }, 23 | "off-0.6": { 24 | "mcts": { 25 | "depth": 5, 26 | "disount": 0.6 27 | }, 28 | "features": { 29 | "planting": false 30 | } 31 | }, 32 | "off-0.8": { 33 | "mcts": { 34 | "depth": 11, 35 | "discount": 0.8 36 | }, 37 | "features": { 38 | "planting": false 39 | } 40 | }, 41 | "off-0.9": { 42 | "mcts": { 43 | "depth": 22, 44 | "discount": 0.9 45 | }, 46 | "features": { 47 | "planting": false 48 | } 49 | }, 50 | "off-0.95": { 51 | "mcts": { 52 | "depth": 45, 53 | "discount": 0.95 54 | }, 55 | "features": { 56 | "planting": false 57 | } 58 | }, 59 | "off-0.98": { 60 | "mcts": { 61 | "depth": 113, 62 | "discount": 0.98 63 | }, 64 | "features": { 65 | "planting": false 66 | } 67 | }, 68 | "off-1": { 69 | "mcts": { 70 | "depth": 150, 71 | "discount": 1 72 | }, 73 | "features": { 74 | "planting": false 75 | } 76 | }, 77 | "on-0.1": { 78 | "mcts": { 79 | "depth": 1, 80 | "discount": 0.1 81 | }, 82 | "features": { 83 | "planting": true 84 | } 85 | }, 86 | "on-0.3": { 87 | "mcts": { 88 | "depth": 2, 89 | "discount": 0.3 90 | }, 91 | "features": { 92 | "planting": true 93 | } 94 | }, 95 | "on-0.6": { 96 | "mcts": { 97 | "depth": 5, 98 | "disount": 0.6 99 | }, 100 | "features": { 101 | "planting": true 102 | } 103 | }, 104 | "on-0.8": { 105 | "mcts": { 106 | "depth": 11, 107 | "discount": 0.8 108 | }, 109 | "features": { 110 | "planting": true 111 | } 112 | }, 113 | "on-0.9": { 114 | "mcts": { 115 | "depth": 22, 116 | "discount": 0.9 117 | }, 118 | "features": { 119 | "planting": true 120 | } 121 | }, 122 | "on-0.95": { 123 | "mcts": { 124 | "depth": 45, 125 | "discount": 0.95 126 | }, 127 | "features": { 128 | "planting": true 129 | } 130 | }, 131 | "on-0.98": { 132 | "mcts": { 133 | "depth": 113, 134 | "discount": 0.98 135 | }, 136 | "features": { 137 | "planting": true 138 | } 139 | }, 140 | "on-1": { 141 | "mcts": { 142 | "depth": 150, 143 | "discount": 1 144 | }, 145 | "features": { 146 | "planting": true 147 | } 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/optimization/map.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:d5a53a3dd16ebe886464639f2dfb715d893897305d7ee2dde34ff08e4a3778fc 3 | size 105 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/optimization/plot.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "inventory", 3 | "x_axis_label": "discount factor", 4 | "x_axis_label_type": "float", 5 | "condition2_label": "planting", 6 | "y_ticks": [0, 3, 6, 9, 12, 15], 7 | "width": 2 8 | } -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/specialization/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../schema.json", 3 | "turns": 100, 4 | "batch": { 5 | "runs": 100 6 | }, 7 | "map": { 8 | "generator": { 9 | "file": { 10 | "path": "./map.png" 11 | } 12 | }, 13 | "tree-height": 3 14 | }, 15 | "features": { 16 | "barriers": false, 17 | "teamwork": true, 18 | "watering": true, 19 | "planting": true, 20 | "waiting": true 21 | }, 22 | "agents": { 23 | "horizon-radius": 5, 24 | "objectives": false, 25 | "plan-others": true, 26 | "snapshot-radius": 10 27 | }, 28 | "mcts": { 29 | "depth": 45, 30 | "exploration": 1.4142135623730951, 31 | "retention": 1.0, 32 | "discount": 0.95, 33 | "visits": 1000 34 | }, 35 | "display": { 36 | "background": [ 37 | 0.0, 38 | 0.44, 39 | 0.36 40 | ], 41 | "padding": [2, 2], 42 | "inventory": true 43 | }, 44 | "analytics": { 45 | "graphs": false, 46 | "heatmaps": false, 47 | "metrics": false, 48 | "screenshot": false, 49 | "serialization": true 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/specialization/experiment.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../experiment.schema.json", 3 | "base": "./base.json", 4 | "trials": { 5 | "experiment": {} 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/specialization/map.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:f560c8dd4b16e3e6c9b9b995a3913d1ef0b84972e871dddb675f26be0ce90981 3 | size 117 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/specialization/plot.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "specialization", 3 | "x_axis_label": "teamwork", 4 | "color": "forestgreen", 5 | "y_ticks": [0, 3, 6, 9, 12, 15] 6 | } -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/tasks/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../schema.json", 3 | "turns": 30, 4 | "batch": { 5 | "runs": 100 6 | }, 7 | "map": { 8 | "tree-height": 3, 9 | "generator": { 10 | "file": { 11 | "path": "./map-5.png" 12 | } 13 | } 14 | }, 15 | "features": { 16 | "barriers": false, 17 | "teamwork": false, 18 | "watering": true 19 | }, 20 | "agents": { 21 | "horizon-radius": 5, 22 | "objectives": false, 23 | "plan-others": true, 24 | "snapshot-radius": 10 25 | }, 26 | "mcts": { 27 | "depth": 45, 28 | "exploration": 1.4142135623730951, 29 | "retention": 1.0, 30 | "discount": 0.95, 31 | "visits": 1000 32 | }, 33 | "display": { 34 | "background": [ 35 | 0.0, 36 | 0.44, 37 | 0.36 38 | ], 39 | "padding": [0, 0], 40 | "inventory": false 41 | }, 42 | "analytics": { 43 | "graphs": false, 44 | "heatmaps": false, 45 | "metrics": false, 46 | "screenshot": false, 47 | "serialization": false 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/tasks/experiment.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../experiment.schema.json", 3 | "base": "./base.json", 4 | "trials": { 5 | "off-1": { 6 | "map": { 7 | "generator": { 8 | "file": { 9 | "path": "./map-1.png" 10 | } 11 | } 12 | }, 13 | "agents": { 14 | "objectives": false 15 | } 16 | }, 17 | "off-3": { 18 | "map": { 19 | "generator": { 20 | "file": { 21 | "path": "./map-3.png" 22 | } 23 | } 24 | }, 25 | "agents": { 26 | "objectives": false 27 | } 28 | }, 29 | "off-5": { 30 | "map": { 31 | "generator": { 32 | "file": { 33 | "path": "./map-5.png" 34 | } 35 | } 36 | }, 37 | "agents": { 38 | "objectives": false 39 | } 40 | }, 41 | "off-7": { 42 | "map": { 43 | "generator": { 44 | "file": { 45 | "path": "./map-7.png" 46 | } 47 | } 48 | }, 49 | "agents": { 50 | "objectives": false 51 | } 52 | }, 53 | "on-1": { 54 | "map": { 55 | "generator": { 56 | "file": { 57 | "path": "./map-1.png" 58 | } 59 | } 60 | }, 61 | "agents": { 62 | "objectives": true 63 | } 64 | }, 65 | "on-3": { 66 | "map": { 67 | "generator": { 68 | "file": { 69 | "path": "./map-3.png" 70 | } 71 | } 72 | }, 73 | "agents": { 74 | "objectives": true 75 | } 76 | }, 77 | "on-5": { 78 | "map": { 79 | "generator": { 80 | "file": { 81 | "path": "./map-5.png" 82 | } 83 | } 84 | }, 85 | "agents": { 86 | "objectives": true 87 | } 88 | }, 89 | "on-7": { 90 | "map": { 91 | "generator": { 92 | "file": { 93 | "path": "./map-7.png" 94 | } 95 | } 96 | }, 97 | "agents": { 98 | "objectives": true 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/tasks/map-1.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:1bf08dfee958301399f6a46fad6931a7d52d411146d448ec574f67763340297a 3 | size 111 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/tasks/map-3.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:2c9573a9e592a65f3c1a876267f407e791286eb34774eeab065f413fbb169db9 3 | size 117 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/tasks/map-5.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:d722d9cee9d5ddf5d097e2d577cbaff2114c4c94e1c0996844d00bc6abd5b46d 3 | size 123 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/tasks/map-7.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:9bf54bc8e4541a8ecec8bc2e2060f5dedb49a0046e0699498ba140f906d20be3 3 | size 123 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/tasks/plot.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "inventory", 3 | "x_axis_label": "distance", 4 | "color": "forestgreen", 5 | "x_axis_label_type": "int", 6 | "y_ticks": [0, 3, 6, 9, 12, 15], 7 | "condition2_label": "tasks" 8 | } -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/teamwork-basic/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../schema.json", 3 | "turns": 10, 4 | "batch": { 5 | "runs": 100 6 | }, 7 | "map": { 8 | "generator": { 9 | "file": { 10 | "path": "./map.png" 11 | } 12 | }, 13 | "tree-height": 3 14 | }, 15 | "features": { 16 | "barriers": false, 17 | "teamwork": true 18 | }, 19 | "agents": { 20 | "horizon-radius": 5, 21 | "objectives": false, 22 | "plan-others": true, 23 | "snapshot-radius": 10 24 | }, 25 | "mcts": { 26 | "depth": 45, 27 | "exploration": 1.4142135623730951, 28 | "retention": 1.0, 29 | "discount": 0.95, 30 | "visits": 1000 31 | }, 32 | "display": { 33 | "background": [ 34 | 0.0, 35 | 0.44, 36 | 0.36 37 | ], 38 | "padding": [2, 2], 39 | "inventory": true 40 | }, 41 | "analytics": { 42 | "graphs": false, 43 | "heatmaps": false, 44 | "metrics": false, 45 | "screenshot": false, 46 | "serialization": false 47 | } 48 | } -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/teamwork-basic/experiment.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../experiment.schema.json", 3 | "base": "./base.json", 4 | "trials": { 5 | "on": { 6 | "agents": { 7 | "plan-others": true 8 | } 9 | }, 10 | "off": { 11 | "agents": { 12 | "plan-others": false 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/teamwork-basic/map.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:c6108b11b7817697625c672d000c1ae1fdb7f88b04fbf331fe00c32c63141d36 3 | size 111 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/teamwork-basic/plot.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "inventory", 3 | "x_axis_label": "plan for others", 4 | "y_ticks": [0,3,6], 5 | "color": "forestgreen", 6 | "height": 0.8 7 | } -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/visit-count-large/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../schema.json", 3 | "turns": 50, 4 | "batch": { 5 | "runs": 10 6 | }, 7 | "map": { 8 | "generator": { 9 | "file": { 10 | "path": "./map.png" 11 | } 12 | }, 13 | "tree-height": 3 14 | }, 15 | "features": { 16 | "barriers": false, 17 | "teamwork": false 18 | }, 19 | "agents": { 20 | "horizon-radius": 5, 21 | "objectives": false, 22 | "plan-others": true, 23 | "snapshot-radius": 20 24 | }, 25 | "mcts": { 26 | "depth": 45, 27 | "exploration": 1.4142135623730951, 28 | "retention": 1.0, 29 | "discount": 0.95, 30 | "visits": 1000 31 | }, 32 | "display": { 33 | "background": [ 34 | 0.0, 35 | 0.44, 36 | 0.36 37 | ], 38 | "padding": [0, 0], 39 | "inventory": false 40 | }, 41 | "analytics": { 42 | "graphs": false, 43 | "heatmaps": false, 44 | "metrics": false, 45 | "screenshot": false, 46 | "serialization": false 47 | } 48 | } -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/visit-count-large/experiment.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../experiment.schema.json", 3 | "base": "./base.json", 4 | "trials": { 5 | "off-1": { 6 | "mcts": { 7 | "visits": 1 8 | } 9 | }, 10 | "off-5": { 11 | "mcts": { 12 | "visits": 5 13 | } 14 | }, 15 | "off-25": { 16 | "mcts": { 17 | "visits": 25 18 | } 19 | }, 20 | "off-125": { 21 | "mcts": { 22 | "visits": 125 23 | } 24 | }, 25 | "off-625": { 26 | "mcts": { 27 | "visits": 625 28 | } 29 | }, 30 | "off-3125": { 31 | "mcts": { 32 | "visits": 3125 33 | } 34 | }, 35 | "on-1": { 36 | "mcts": { 37 | "visits": 1 38 | }, 39 | "agents": { 40 | "objectives": true 41 | } 42 | }, 43 | "on-5": { 44 | "mcts": { 45 | "visits": 5 46 | }, 47 | "agents": { 48 | "objectives": true 49 | } 50 | }, 51 | "on-25": { 52 | "mcts": { 53 | "visits": 25 54 | }, 55 | "agents": { 56 | "objectives": true 57 | } 58 | }, 59 | "on-125": { 60 | "mcts": { 61 | "visits": 125 62 | }, 63 | "agents": { 64 | "objectives": true 65 | } 66 | }, 67 | "on-625": { 68 | "mcts": { 69 | "visits": 625 70 | }, 71 | "agents": { 72 | "objectives": true 73 | } 74 | }, 75 | "on-3125": { 76 | "mcts": { 77 | "visits": 3125 78 | }, 79 | "agents": { 80 | "objectives": true 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/visit-count-large/map.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:bab0f28ae1f9cc76720adb25b2cdfefd06144aeae002dfacdb1e35b7e5be5adf 3 | size 135 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/visit-count-large/plot.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "inventory", 3 | "x_axis_label": "visit count", 4 | "color": "forestgreen", 5 | "num_trial": true, 6 | "rotated_label": true, 7 | "y_ticks": [0, 3, 6, 9], 8 | "condition2_label": "tasks" 9 | } -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/visit-count-small/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../schema.json", 3 | "turns": 30, 4 | "batch": { 5 | "runs": 100 6 | }, 7 | "map": { 8 | "generator": { 9 | "file": { 10 | "path": "./map.png" 11 | } 12 | }, 13 | "tree-height": 3 14 | }, 15 | "features": { 16 | "barriers": false, 17 | "teamwork": false 18 | }, 19 | "agents": { 20 | "horizon-radius": 5, 21 | "objectives": false, 22 | "plan-others": true, 23 | "snapshot-radius": 20 24 | }, 25 | "mcts": { 26 | "depth": 45, 27 | "exploration": 1.4142135623730951, 28 | "retention": 1.0, 29 | "discount": 0.95, 30 | "visits": 1000 31 | }, 32 | "display": { 33 | "background": [ 34 | 0.0, 35 | 0.44, 36 | 0.36 37 | ], 38 | "padding": [0, 0], 39 | "inventory": false 40 | }, 41 | "analytics": { 42 | "graphs": false, 43 | "heatmaps": false, 44 | "metrics": false, 45 | "screenshot": false, 46 | "serialization": false 47 | } 48 | } -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/visit-count-small/experiment.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../experiment.schema.json", 3 | "base": "./base.json", 4 | "trials": { 5 | "visit-1": { 6 | "mcts": { 7 | "visits": 1 8 | } 9 | }, 10 | "visit-5": { 11 | "mcts": { 12 | "visits": 5 13 | } 14 | }, 15 | "visit-25": { 16 | "mcts": { 17 | "visits": 25 18 | } 19 | }, 20 | "visit-125": { 21 | "mcts": { 22 | "visits": 125 23 | } 24 | }, 25 | "visit-625": { 26 | "mcts": { 27 | "visits": 625 28 | } 29 | }, 30 | "visit-3125": { 31 | "mcts": { 32 | "visits": 3125 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/visit-count-small/map.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:168625a20948087c9ae53a61191eb605f996a85c928762711cae1d2dc59cdd33 3 | size 190 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/experiments/visit-count-small/plot.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "inventory", 3 | "x_axis_label": "visit count", 4 | "color": "forestgreen", 5 | "x_axis_label_type": "int", 6 | "rotated_label": true, 7 | "y_ticks": [0, 3, 6, 9] 8 | } -------------------------------------------------------------------------------- /scenario-lumberjacks/maps/corridor.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:81406869963624ad2cb1af03e0f465dad428f5c0988c79566caf5653382c6bd2 3 | size 95 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/maps/corridor1.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:002a34c74a076924cc830c12c1103e37443c636f1358bf49f8f4a13a878ae001 3 | size 116 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/maps/corridor2.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:e32c7f03be73a28d7a57ffd7dcd741447198945dcb41cc8f191396221a67f646 3 | size 119 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/maps/corridor3.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:b130759ce90e3a3f934b17e83608f561e63edd2b8e31ebcea08773cbb49ea504 3 | size 123 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/maps/teamwork.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:2496a3f60bcaa31450c3cdb5ecdd1123ae87c5964e6d922dc8098e3fd7e8cb0d 3 | size 105 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/maps/thief.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:956dfa7c416897ba1310f6f515577312d4f5e7b8e515bfaed200d94622385b18 3 | size 117 4 | -------------------------------------------------------------------------------- /scenario-lumberjacks/src/behaviors/human.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use std::fmt; 7 | 8 | use npc_engine_core::{Behavior, Context}; 9 | 10 | use crate::Lumberjacks; 11 | 12 | pub struct Human; 13 | 14 | impl fmt::Display for Human { 15 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 16 | write!(f, "Human") 17 | } 18 | } 19 | 20 | impl Behavior for Human { 21 | fn is_valid(&self, _ctx: Context) -> bool { 22 | true 23 | } 24 | 25 | fn add_own_tasks( 26 | &self, 27 | _ctx: Context, 28 | _tasks: &mut Vec>>, 29 | ) { 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /scenario-lumberjacks/src/behaviors/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | mod human; 7 | mod lumberjack; 8 | 9 | pub use human::*; 10 | pub use lumberjack::*; 11 | -------------------------------------------------------------------------------- /scenario-lumberjacks/src/fitnesses.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use crate::{Lumberjacks, WorldState}; 7 | use npc_engine_core::{AgentId, StateDiffRef}; 8 | 9 | pub(crate) fn minimalist(state: StateDiffRef, _agent: AgentId) -> f32 { 10 | -(state.trees().len() as f32) 11 | } 12 | -------------------------------------------------------------------------------- /scenario-lumberjacks/src/graph.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use std::fs; 7 | 8 | use npc_engine_core::graphviz::plot_mcts_tree; 9 | 10 | use crate::{output_path, PostMCTSHookArgs, PostMCTSHookFn}; 11 | 12 | pub fn graph_hook() -> PostMCTSHookFn { 13 | Box::new( 14 | |PostMCTSHookArgs { 15 | run, 16 | turn, 17 | agent, 18 | mcts, 19 | .. 20 | }| { 21 | fs::create_dir_all(format!( 22 | "{}/{}/graphs/agent{}/", 23 | output_path(), 24 | run.map(|n| n.to_string()).unwrap_or_default(), 25 | agent.0 26 | )) 27 | .unwrap(); 28 | let mut file = fs::OpenOptions::new() 29 | .create(true) 30 | .write(true) 31 | .truncate(true) 32 | .open(format!( 33 | "{}/{}/graphs/agent{}/turn{:06}.dot", 34 | output_path(), 35 | run.map(|n| n.to_string()).unwrap_or_default(), 36 | agent.0, 37 | turn 38 | )) 39 | .unwrap(); 40 | 41 | plot_mcts_tree(mcts, &mut file).unwrap(); 42 | }, 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /scenario-lumberjacks/src/hooks.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use std::collections::BTreeMap; 7 | 8 | use ggez::graphics::Image; 9 | use ggez::Context; 10 | use npc_engine_core::{AgentId, Task, MCTS}; 11 | 12 | use crate::{Lumberjacks, WorldGlobalState}; 13 | 14 | pub type PreWorldHookFn = Box; 15 | pub type PostWorldHookFn = Box; 16 | pub type PostMCTSHookFn = Box; 17 | 18 | // Pre world hooks are called once per game loop before any actions have executed 19 | pub struct PreWorldHookArgs<'a, 'b> { 20 | pub run: Option, 21 | pub ctx: &'a mut Option<&'b mut Context>, 22 | pub assets: &'a BTreeMap, 23 | pub turn: usize, 24 | pub world: &'a WorldGlobalState, 25 | } 26 | 27 | // Post world hooks are called once per game loop after all actions have executed 28 | pub struct PostWorldHookArgs<'a, 'b> { 29 | pub run: Option, 30 | pub ctx: &'a mut Option<&'b mut Context>, 31 | pub assets: &'a BTreeMap, 32 | pub turn: usize, 33 | pub world: &'a WorldGlobalState, 34 | pub objectives: &'a BTreeMap>>, 35 | } 36 | 37 | // Post MCTS hooks are called once per agent per loop after it runs this turn 38 | pub struct PostMCTSHookArgs<'a, 'b> { 39 | pub run: Option, 40 | pub ctx: &'a mut Option<&'b mut Context>, 41 | pub assets: &'a BTreeMap, 42 | pub turn: usize, 43 | pub world: &'a WorldGlobalState, 44 | pub agent: AgentId, 45 | pub mcts: &'a MCTS, 46 | pub objective: Box>, 47 | } 48 | -------------------------------------------------------------------------------- /scenario-lumberjacks/src/inventory.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use std::collections::BTreeMap; 7 | use std::mem; 8 | 9 | use ggez::graphics::{draw, Image, Text, DEFAULT_FONT_SCALE, WHITE}; 10 | use ggez::Context; 11 | use serde::Serialize; 12 | 13 | use npc_engine_core::AgentId; 14 | 15 | use crate::SPRITE_SIZE; 16 | 17 | #[derive(Clone, Default, Debug, PartialEq, Eq, Serialize)] 18 | pub struct Inventory(pub BTreeMap); 19 | 20 | #[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Hash)] 21 | pub struct AgentInventory { 22 | pub wood: isize, 23 | pub water: bool, 24 | } 25 | 26 | impl Inventory { 27 | pub fn draw(&self, ctx: &mut Context, assets: &BTreeMap) { 28 | let mut agents = self 29 | .0 30 | .iter() 31 | .map(|(k, v)| (*k, v.wood, v.water)) 32 | .collect::>(); 33 | 34 | agents.sort_by_key(|(k, ..)| *k); 35 | 36 | for (i, (agent, wood, water)) in agents.iter().enumerate() { 37 | let sprite_name = if agent.0 % 2 == 0 { 38 | "OrangeRight".to_owned() 39 | } else { 40 | "YellowRight".to_owned() 41 | }; 42 | 43 | draw( 44 | ctx, 45 | assets.get(&sprite_name).unwrap(), 46 | ([0 as f32 * SPRITE_SIZE, i as f32 * SPRITE_SIZE], WHITE), 47 | ) 48 | .unwrap(); 49 | 50 | draw( 51 | ctx, 52 | &Text::new(format!(":{}, {}", wood, water)), 53 | ([ 54 | SPRITE_SIZE, 55 | i as f32 * SPRITE_SIZE + (SPRITE_SIZE - DEFAULT_FONT_SCALE) / 2., 56 | ],), 57 | ) 58 | .unwrap(); 59 | } 60 | } 61 | } 62 | 63 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 64 | pub struct InventorySnapshot(pub BTreeMap); 65 | 66 | #[derive(Clone, Debug, Default, Hash, PartialEq, Eq)] 67 | pub struct InventoryDiff(pub BTreeMap); 68 | 69 | impl InventoryDiff { 70 | pub fn diff_size(&self) -> usize { 71 | self.0.len() * mem::size_of::<(AgentId, AgentInventory)>() 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /scenario-lumberjacks/src/metrics/agency.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use crate::{PostMCTSHookArgs, PostMCTSHookFn}; 7 | 8 | pub fn agency_metric_hook() -> PostMCTSHookFn { 9 | Box::new(|PostMCTSHookArgs { agent, mcts, .. }| { 10 | // Agency (diff size) 11 | // TODO: Only for this agent? 12 | let (count, size) = mcts 13 | .nodes() 14 | .fold((0usize, 0usize), |(count, size), (node, _)| { 15 | (count + 1, size + node.diff().diff_size()) 16 | }); 17 | 18 | println!("Agent{} Agency: {}", agent.0, size as f32 / count as f32); 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /scenario-lumberjacks/src/metrics/features.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use crate::{PreWorldHookArgs, PreWorldHookFn}; 7 | 8 | pub fn features_metric_hook() -> PreWorldHookFn { 9 | Box::new(|PreWorldHookArgs { world, .. }| { 10 | println!("# of trees: {}", world.map.tree_count()); 11 | println!("# of unique patches: {}", world.map.patch_count(2)); 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /scenario-lumberjacks/src/metrics/fluctuation.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | -------------------------------------------------------------------------------- /scenario-lumberjacks/src/metrics/islands.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use partitions::PartitionVec; 7 | 8 | use crate::{PreWorldHookArgs, PreWorldHookFn}; 9 | 10 | pub fn islands_metric_hook() -> PreWorldHookFn { 11 | Box::new(|PreWorldHookArgs { world, .. }| { 12 | let index_fn = |x, y| world.map.width * y + x; 13 | 14 | let mut islands = PartitionVec::with_capacity(world.map.width * world.map.height); 15 | let mut impassables = 0; 16 | 17 | world.map.tiles.iter().enumerate().for_each(|(y, row)| { 18 | row.iter().enumerate().for_each(|(x, _tile)| { 19 | islands.push((x, y)); 20 | }); 21 | }); 22 | 23 | world.map.tiles.iter().enumerate().for_each(|(y, row)| { 24 | row.iter().enumerate().for_each(|(x, tile)| { 25 | if !tile.is_impassable() { 26 | let neighbors = [ 27 | y.checked_sub(1).map(|y| (x, y)), 28 | if y + 1 < world.map.height { 29 | Some((x, y + 1)) 30 | } else { 31 | None 32 | }, 33 | x.checked_sub(1).map(|x| (x, y)), 34 | if x + 1 < world.map.width { 35 | Some((x + 1, y)) 36 | } else { 37 | None 38 | }, 39 | ]; 40 | 41 | let index = index_fn(x, y); 42 | neighbors.iter().cloned().flatten().for_each(|(x, y)| { 43 | if !world.map.tiles[y][x].is_impassable() { 44 | islands.union(index, index_fn(x, y)); 45 | } 46 | }); 47 | } else { 48 | impassables += 1; 49 | } 50 | }); 51 | }); 52 | 53 | // Impassable tiles each count as own island, need to be removed 54 | println!("# of islands: {}", islands.amount_of_sets() - impassables); 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /scenario-lumberjacks/src/metrics/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | mod agency; 7 | mod features; 8 | mod islands; 9 | mod performance; 10 | 11 | pub use agency::*; 12 | pub use features::*; 13 | pub use islands::*; 14 | pub use performance::*; 15 | -------------------------------------------------------------------------------- /scenario-lumberjacks/src/metrics/performance.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use std::collections::HashMap; 7 | use std::mem; 8 | use std::time::Duration; 9 | 10 | use crate::{Barrier, Chop, Move, Plant, PostMCTSHookArgs, PostMCTSHookFn, Refill, Wait, Water}; 11 | use npc_engine_core::AgentId; 12 | use npc_engine_utils::Direction; 13 | 14 | pub fn node_edges_count_metric_hook() -> PostMCTSHookFn { 15 | let mut stats = HashMap::::default(); 16 | 17 | Box::new(move |PostMCTSHookArgs { agent, mcts, .. }| { 18 | let (nodes, edges, count) = stats.entry(agent).or_default(); 19 | 20 | *nodes += mcts.node_count(); 21 | *edges += mcts.edge_count(); 22 | *count += 1; 23 | 24 | log::info!( 25 | "Agent {} Avg # Nodes: {} ({} samples)", 26 | agent.0, 27 | *nodes as f32 / *count as f32, 28 | count 29 | ); 30 | log::info!( 31 | "Agent {} Avg # Edges: {} ({} samples)", 32 | agent.0, 33 | *edges as f32 / *count as f32, 34 | count 35 | ); 36 | }) 37 | } 38 | 39 | pub fn diff_memory_metric_hook() -> PostMCTSHookFn { 40 | let mut stats = HashMap::::default(); 41 | 42 | Box::new(move |PostMCTSHookArgs { agent, mcts, .. }| { 43 | let (diff_size, count) = stats.entry(agent).or_default(); 44 | 45 | for (node, _) in mcts.nodes() { 46 | *diff_size += node.diff().diff_size(); 47 | *count += 1; 48 | } 49 | 50 | log::info!( 51 | "Agent {} Avg Diff Size: {} bytes ({} samples)", 52 | agent.0, 53 | *diff_size as f32 / *count as f32, 54 | count 55 | ); 56 | }) 57 | } 58 | 59 | pub fn total_memory_metric_hook() -> PostMCTSHookFn { 60 | let mut stats = HashMap::::default(); 61 | 62 | Box::new(move |PostMCTSHookArgs { agent, mcts, .. }| { 63 | let (total_size, count) = stats.entry(agent).or_default(); 64 | 65 | *total_size += mcts.size(|task| { 66 | if task.downcast_ref::().is_some() { 67 | mem::size_of::() 68 | } else if task.downcast_ref::().is_some() { 69 | mem::size_of::() 70 | } else if let Some(_move) = task.downcast_ref::() { 71 | mem::size_of::() + _move.path.len() * mem::size_of::() 72 | } else if task.downcast_ref::().is_some() { 73 | mem::size_of::() 74 | } else if task.downcast_ref::().is_some() { 75 | mem::size_of::() 76 | } else if task.downcast_ref::().is_some() { 77 | mem::size_of::() 78 | } else if task.downcast_ref::().is_some() { 79 | mem::size_of::() 80 | } else { 81 | panic!("Unrecognized task type!"); 82 | } 83 | }); 84 | 85 | *count += 1; 86 | 87 | log::info!( 88 | "Agent {} Avg Total Size: {} bytes ({} samples)", 89 | agent.0, 90 | *total_size as f32 / *count as f32, 91 | count 92 | ); 93 | }) 94 | } 95 | 96 | pub fn branching_metric_hook() -> PostMCTSHookFn { 97 | let mut stats = HashMap::::default(); 98 | 99 | Box::new(move |PostMCTSHookArgs { agent, mcts, .. }| { 100 | let (branching, count) = stats.entry(agent).or_default(); 101 | 102 | for (_, edges) in mcts.nodes() { 103 | *branching += edges.branching_factor(); 104 | *count += 1; 105 | } 106 | 107 | log::info!( 108 | "Agent {} Avg Branching Factor: {} ({} samples)", 109 | agent.0, 110 | *branching as f32 / *count as f32, 111 | count 112 | ); 113 | }) 114 | } 115 | 116 | pub fn time_metric_hook() -> PostMCTSHookFn { 117 | let mut stats = HashMap::::default(); 118 | 119 | Box::new(move |PostMCTSHookArgs { agent, mcts, .. }| { 120 | let (time, count) = stats.entry(agent).or_default(); 121 | 122 | *time += mcts.time(); 123 | *count += 1; 124 | 125 | log::info!( 126 | "Agent {} Avg Time: {:?} ({} samples)", 127 | agent.0, 128 | *time / *count as u32, 129 | count 130 | ); 131 | }) 132 | } 133 | -------------------------------------------------------------------------------- /scenario-lumberjacks/src/screenshot.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use std::collections::BTreeMap; 7 | use std::fs; 8 | use std::path::PathBuf; 9 | 10 | use ggez::graphics; 11 | use ggez::graphics::{Canvas, Image}; 12 | use ggez::Context; 13 | use image::png::PngEncoder; 14 | use image::{ColorType, ImageBuffer, Rgba}; 15 | 16 | use crate::{config, output_path, PreWorldHookArgs, PreWorldHookFn, WorldGlobalState}; 17 | 18 | pub fn screenshot( 19 | ctx: &mut Context, 20 | world: &WorldGlobalState, 21 | assets: &BTreeMap, 22 | path: &str, 23 | ) { 24 | let canvas = Canvas::with_window_size(ctx).unwrap(); 25 | graphics::set_canvas(ctx, Some(&canvas)); 26 | graphics::clear( 27 | ctx, 28 | graphics::Color::new( 29 | config().display.background.0, 30 | config().display.background.1, 31 | config().display.background.2, 32 | 1., 33 | ), 34 | ); 35 | 36 | world.draw(ctx, assets); 37 | 38 | graphics::present(ctx).unwrap(); 39 | let image = canvas.image(); 40 | 41 | let width = image.width() as u32; 42 | let height = image.height() as u32; 43 | let image_data: ImageBuffer, _> = 44 | ImageBuffer::from_raw(width, height, image.to_rgba8(ctx).unwrap()).unwrap(); 45 | 46 | let flipped_image_data = image::imageops::flip_vertical(&image_data); 47 | 48 | let dir = { 49 | let mut path = PathBuf::from(path); 50 | path.pop(); 51 | path.to_str().unwrap().to_owned() 52 | }; 53 | fs::create_dir_all(dir).unwrap(); 54 | 55 | let file = fs::OpenOptions::new() 56 | .create(true) 57 | .write(true) 58 | .truncate(true) 59 | .open(path) 60 | .unwrap(); 61 | 62 | PngEncoder::new(file) 63 | .encode(&flipped_image_data, width, height, ColorType::Rgba8) 64 | .unwrap(); 65 | } 66 | 67 | pub fn screenshot_hook() -> PreWorldHookFn { 68 | Box::new( 69 | |PreWorldHookArgs { 70 | world, 71 | ctx, 72 | assets, 73 | run, 74 | turn, 75 | .. 76 | }| { 77 | if let Some(ctx) = ctx { 78 | screenshot( 79 | ctx, 80 | world, 81 | assets, 82 | &format!( 83 | "{}/{}/screenshots/turn{:06}.png", 84 | output_path(), 85 | run.map(|n| n.to_string()).unwrap_or_default(), 86 | turn, 87 | ), 88 | ); 89 | } 90 | }, 91 | ) 92 | } 93 | -------------------------------------------------------------------------------- /scenario-lumberjacks/src/serialization.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use std::fs; 7 | 8 | use crate::{output_path, PreWorldHookArgs, PreWorldHookFn}; 9 | 10 | pub fn world_serialization_hook() -> PreWorldHookFn { 11 | Box::new( 12 | |PreWorldHookArgs { 13 | world, run, turn, .. 14 | }| { 15 | fs::create_dir_all(format!( 16 | "{}/{}/serialization/", 17 | output_path(), 18 | run.map(|n| n.to_string()).unwrap_or_default(), 19 | )) 20 | .unwrap(); 21 | 22 | let file = fs::OpenOptions::new() 23 | .create(true) 24 | .write(true) 25 | .truncate(true) 26 | .open(format!( 27 | "{}/{}/serialization/map{:06}.json", 28 | output_path(), 29 | run.map(|n| n.to_string()).unwrap_or_default(), 30 | turn, 31 | )) 32 | .unwrap(); 33 | 34 | serde_json::to_writer_pretty(file, world).unwrap(); 35 | }, 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /scenario-lumberjacks/src/tasks/barrier.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use std::hash::Hash; 7 | 8 | use npc_engine_core::{ 9 | impl_task_boxed_methods, Context, ContextMut, Domain, IdleTask, Task, TaskDuration, 10 | }; 11 | use npc_engine_utils::{Direction, DIRECTIONS}; 12 | 13 | use crate::{apply_direction, config, Action, Lumberjacks, Tile, WorldState, WorldStateMut}; 14 | 15 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 16 | pub struct Barrier { 17 | pub direction: Direction, 18 | } 19 | 20 | impl Task for Barrier { 21 | fn weight(&self, _ctx: Context) -> f32 { 22 | config().action_weights.barrier 23 | } 24 | 25 | fn duration(&self, _ctx: Context) -> TaskDuration { 26 | 0 27 | } 28 | 29 | fn execute(&self, ctx: ContextMut) -> Option>> { 30 | let ContextMut { 31 | mut state_diff, 32 | agent, 33 | .. 34 | } = ctx; 35 | state_diff.increment_time(); 36 | 37 | if let Some((x, y)) = state_diff.find_agent(agent) { 38 | let (x, y) = apply_direction(self.direction, x, y); 39 | state_diff.set_tile(x, y, Tile::Barrier); 40 | state_diff.decrement_inventory(agent); 41 | 42 | Some(Box::new(IdleTask)) 43 | } else { 44 | unreachable!() 45 | } 46 | } 47 | 48 | fn display_action(&self) -> ::DisplayAction { 49 | Action::Barrier(self.direction) 50 | } 51 | 52 | fn is_valid(&self, ctx: Context) -> bool { 53 | let Context { 54 | state_diff, agent, .. 55 | } = ctx; 56 | if let Some((x, y)) = state_diff.find_agent(agent) { 57 | let (x, y) = apply_direction(self.direction, x, y); 58 | let empty = matches!(state_diff.get_tile(x, y), Some(Tile::Empty)); 59 | let supported = DIRECTIONS 60 | .into_iter() 61 | .filter(|direction| { 62 | let (x, y) = apply_direction(*direction, x, y); 63 | state_diff 64 | .get_tile(x, y) 65 | .map(|tile| tile.is_support()) 66 | .unwrap_or(false) 67 | }) 68 | .count() 69 | >= 1; 70 | 71 | empty && supported 72 | } else { 73 | unreachable!() 74 | } 75 | } 76 | 77 | impl_task_boxed_methods!(Lumberjacks); 78 | } 79 | -------------------------------------------------------------------------------- /scenario-lumberjacks/src/tasks/chop.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use std::hash::Hash; 7 | use std::num::NonZeroU8; 8 | 9 | use npc_engine_core::{ 10 | impl_task_boxed_methods, Context, ContextMut, Domain, IdleTask, Task, TaskDuration, 11 | }; 12 | use npc_engine_utils::{Direction, DIRECTIONS}; 13 | 14 | use crate::{apply_direction, config, Action, Lumberjacks, Tile, WorldState, WorldStateMut}; 15 | 16 | // SAFETY: this is safe as 1 is non-zero. This is actually a work-around the fact 17 | // that Option::unwrap() is currently not const, but we need a constant in the match arm below. 18 | // See the related Rust issue: https://github.com/rust-lang/rust/issues/67441 19 | const NON_ZERO_U8_1: NonZeroU8 = unsafe { NonZeroU8::new_unchecked(1) }; 20 | 21 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 22 | pub struct Chop { 23 | pub direction: Direction, 24 | } 25 | 26 | impl Task for Chop { 27 | fn weight(&self, _ctx: Context) -> f32 { 28 | config().action_weights.chop 29 | } 30 | 31 | fn duration(&self, _ctx: Context) -> TaskDuration { 32 | 0 33 | } 34 | 35 | fn execute(&self, ctx: ContextMut) -> Option>> { 36 | let ContextMut { 37 | mut state_diff, 38 | agent, 39 | .. 40 | } = ctx; 41 | state_diff.increment_time(); 42 | 43 | if let Some((x, y)) = state_diff.find_agent(agent) { 44 | let (x, y) = apply_direction(self.direction, x, y); 45 | 46 | match state_diff.get_tile_ref_mut(x, y) { 47 | Some(tile @ Tile::Tree(NON_ZERO_U8_1)) => { 48 | *tile = Tile::Empty; 49 | } 50 | Some(Tile::Tree(height)) => { 51 | *height = NonZeroU8::new(height.get() - 1).unwrap(); 52 | } 53 | _ => return Some(Box::new(IdleTask)), 54 | } 55 | 56 | if config().features.teamwork { 57 | for direction in DIRECTIONS { 58 | let (x, y) = apply_direction(direction, x, y); 59 | if let Some(Tile::Agent(agent)) = state_diff.get_tile(x, y) { 60 | state_diff.increment_inventory(agent); 61 | } 62 | } 63 | } else { 64 | state_diff.increment_inventory(agent); 65 | } 66 | 67 | Some(Box::new(IdleTask)) 68 | } else { 69 | unreachable!("Could not find agent on map!") 70 | } 71 | } 72 | 73 | fn display_action(&self) -> ::DisplayAction { 74 | Action::Chop(self.direction) 75 | } 76 | 77 | fn is_valid(&self, ctx: Context) -> bool { 78 | let Context { 79 | state_diff, agent, .. 80 | } = ctx; 81 | if let Some((x, y)) = state_diff.find_agent(agent) { 82 | let (x, y) = apply_direction(self.direction, x, y); 83 | matches!(state_diff.get_tile(x, y), Some(Tile::Tree(_))) 84 | } else { 85 | unreachable!("Could not find agent on map!") 86 | } 87 | } 88 | 89 | impl_task_boxed_methods!(Lumberjacks); 90 | } 91 | -------------------------------------------------------------------------------- /scenario-lumberjacks/src/tasks/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | mod barrier; 7 | mod chop; 8 | mod r#move; 9 | mod plant; 10 | mod refill; 11 | mod wait; 12 | mod water; 13 | 14 | pub use barrier::*; 15 | pub use chop::*; 16 | pub use plant::*; 17 | pub use r#move::*; 18 | pub use refill::*; 19 | pub use wait::*; 20 | pub use water::*; 21 | -------------------------------------------------------------------------------- /scenario-lumberjacks/src/tasks/move.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use std::hash::Hash; 7 | 8 | use npc_engine_core::{ 9 | impl_task_boxed_methods, Context, ContextMut, Domain, IdleTask, Task, TaskDuration, 10 | }; 11 | use npc_engine_utils::Direction; 12 | 13 | use crate::{apply_direction, config, Action, Lumberjacks, Tile, WorldState, WorldStateMut}; 14 | 15 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 16 | pub struct Move { 17 | pub path: Vec, 18 | pub x: usize, 19 | pub y: usize, 20 | } 21 | 22 | impl Task for Move { 23 | fn weight(&self, _ctx: Context) -> f32 { 24 | config().action_weights.r#move 25 | } 26 | 27 | fn duration(&self, _ctx: Context) -> TaskDuration { 28 | 0 29 | } 30 | 31 | fn execute(&self, ctx: ContextMut) -> Option>> { 32 | let ContextMut { 33 | mut state_diff, 34 | agent, 35 | .. 36 | } = ctx; 37 | state_diff.increment_time(); 38 | 39 | if let Some((x, y)) = state_diff.find_agent(agent) { 40 | let direction = self.path.first().unwrap(); 41 | let (_x, _y) = apply_direction(*direction, x, y); 42 | state_diff.set_tile(x, y, Tile::Empty); 43 | state_diff.set_tile(_x, _y, Tile::Agent(agent)); 44 | 45 | let mut path = self.path.iter().skip(1).copied(); 46 | 47 | if path.next().is_some() { 48 | panic!( 49 | "Objectives are currently disabled in Lumberjack, so path do not make sense" 50 | ); // objectives are disabled 51 | /*Some(Box::new(Move { 52 | path, 53 | x: self.x, 54 | y: self.y, 55 | }))*/ 56 | } else { 57 | Some(Box::new(IdleTask)) 58 | } 59 | } else { 60 | unreachable!() 61 | } 62 | } 63 | 64 | fn display_action(&self) -> ::DisplayAction { 65 | Action::Walk(*self.path.first().unwrap()) 66 | } 67 | 68 | fn is_valid(&self, ctx: Context) -> bool { 69 | let Context { 70 | state_diff, agent, .. 71 | } = ctx; 72 | if let Some((mut x, mut y)) = state_diff.find_agent(agent) { 73 | self.path.iter().enumerate().all(|(idx, direction)| { 74 | let tmp = apply_direction(*direction, x, y); 75 | x = tmp.0; 76 | y = tmp.1; 77 | state_diff 78 | .get_tile(x, y) 79 | .map(|tile| { 80 | if idx == 0 { 81 | tile.is_walkable() 82 | } else { 83 | tile.is_pathfindable() 84 | } 85 | }) 86 | .unwrap_or(false) 87 | }) 88 | } else { 89 | unreachable!() 90 | } 91 | } 92 | 93 | impl_task_boxed_methods!(Lumberjacks); 94 | } 95 | -------------------------------------------------------------------------------- /scenario-lumberjacks/src/tasks/plant.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use std::hash::Hash; 7 | use std::num::NonZeroU8; 8 | 9 | use npc_engine_core::{ 10 | impl_task_boxed_methods, Context, ContextMut, Domain, IdleTask, Task, TaskDuration, 11 | }; 12 | use npc_engine_utils::Direction; 13 | 14 | use crate::{apply_direction, config, Action, Lumberjacks, Tile, WorldState, WorldStateMut}; 15 | 16 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 17 | pub struct Plant { 18 | pub direction: Direction, 19 | } 20 | 21 | impl Task for Plant { 22 | fn weight(&self, _ctx: Context) -> f32 { 23 | config().action_weights.plant 24 | } 25 | 26 | fn duration(&self, _ctx: Context) -> TaskDuration { 27 | 0 28 | } 29 | 30 | fn execute(&self, ctx: ContextMut) -> Option>> { 31 | let ContextMut { 32 | mut state_diff, 33 | agent, 34 | .. 35 | } = ctx; 36 | state_diff.increment_time(); 37 | 38 | if let Some((x, y)) = state_diff.find_agent(agent) { 39 | let (x, y) = apply_direction(self.direction, x, y); 40 | 41 | match state_diff.get_tile_ref_mut(x, y) { 42 | Some(tile @ Tile::Empty) => { 43 | *tile = Tile::Tree(NonZeroU8::new(1).unwrap()); 44 | } 45 | _ => return Some(Box::new(IdleTask)), 46 | } 47 | 48 | state_diff.decrement_inventory(agent); 49 | 50 | Some(Box::new(IdleTask)) 51 | } else { 52 | unreachable!("Could not find agent on map!") 53 | } 54 | } 55 | 56 | fn display_action(&self) -> ::DisplayAction { 57 | Action::Plant(self.direction) 58 | } 59 | 60 | fn is_valid(&self, ctx: Context) -> bool { 61 | let Context { 62 | state_diff, agent, .. 63 | } = ctx; 64 | if let Some((x, y)) = state_diff.find_agent(agent) { 65 | let (x, y) = apply_direction(self.direction, x, y); 66 | matches!(state_diff.get_tile(x, y), Some(Tile::Empty)) 67 | } else { 68 | unreachable!("Could not find agent on map!") 69 | } 70 | } 71 | 72 | impl_task_boxed_methods!(Lumberjacks); 73 | } 74 | -------------------------------------------------------------------------------- /scenario-lumberjacks/src/tasks/refill.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use std::hash::Hash; 7 | 8 | use npc_engine_core::{ 9 | impl_task_boxed_methods, Context, ContextMut, Domain, IdleTask, Task, TaskDuration, 10 | }; 11 | use npc_engine_utils::DIRECTIONS; 12 | 13 | use crate::{apply_direction, config, Action, Lumberjacks, Tile, WorldState, WorldStateMut}; 14 | 15 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 16 | pub struct Refill; 17 | 18 | impl Task for Refill { 19 | fn weight(&self, _ctx: Context) -> f32 { 20 | config().action_weights.refill 21 | } 22 | 23 | fn duration(&self, _ctx: Context) -> TaskDuration { 24 | 0 25 | } 26 | 27 | fn execute(&self, ctx: ContextMut) -> Option>> { 28 | let ContextMut { 29 | mut state_diff, 30 | agent, 31 | .. 32 | } = ctx; 33 | state_diff.increment_time(); 34 | 35 | state_diff.set_water(agent, true); 36 | Some(Box::new(IdleTask)) 37 | } 38 | 39 | fn display_action(&self) -> ::DisplayAction { 40 | Action::Refill 41 | } 42 | 43 | fn is_valid(&self, ctx: Context) -> bool { 44 | let Context { 45 | state_diff, agent, .. 46 | } = ctx; 47 | if let Some((x, y)) = state_diff.find_agent(agent) { 48 | !state_diff.get_water(agent) 49 | && DIRECTIONS.iter().any(|direction| { 50 | let (x, y) = apply_direction(*direction, x, y); 51 | matches!(state_diff.get_tile(x, y), Some(Tile::Well)) 52 | }) 53 | } else { 54 | unreachable!("Failed to find agent on map"); 55 | } 56 | } 57 | 58 | impl_task_boxed_methods!(Lumberjacks); 59 | } 60 | -------------------------------------------------------------------------------- /scenario-lumberjacks/src/tasks/wait.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use std::hash::Hash; 7 | 8 | use npc_engine_core::{ 9 | impl_task_boxed_methods, Context, ContextMut, Domain, IdleTask, Task, TaskDuration, 10 | }; 11 | 12 | use crate::{config, Action, Lumberjacks, WorldStateMut}; 13 | 14 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 15 | pub struct Wait; 16 | 17 | impl Task for Wait { 18 | fn weight(&self, _ctx: Context) -> f32 { 19 | config().action_weights.wait 20 | } 21 | 22 | fn duration(&self, _ctx: Context) -> TaskDuration { 23 | 0 24 | } 25 | 26 | fn execute(&self, ctx: ContextMut) -> Option>> { 27 | let ContextMut { mut state_diff, .. } = ctx; 28 | state_diff.increment_time(); 29 | 30 | Some(Box::new(IdleTask)) 31 | } 32 | 33 | fn display_action(&self) -> ::DisplayAction { 34 | Action::Wait 35 | } 36 | 37 | fn is_valid(&self, _ctx: Context) -> bool { 38 | true 39 | } 40 | 41 | impl_task_boxed_methods!(Lumberjacks); 42 | } 43 | -------------------------------------------------------------------------------- /scenario-lumberjacks/src/tasks/water.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: Apache-2.0 OR MIT 3 | * © 2020-2022 ETH Zurich and other contributors, see AUTHORS.txt for details 4 | */ 5 | 6 | use std::hash::Hash; 7 | 8 | use npc_engine_core::{ 9 | impl_task_boxed_methods, Context, ContextMut, Domain, IdleTask, Task, TaskDuration, 10 | }; 11 | use npc_engine_utils::Direction; 12 | 13 | use crate::{apply_direction, config, Action, Lumberjacks, Tile, WorldState, WorldStateMut}; 14 | 15 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 16 | pub struct Water { 17 | pub direction: Direction, 18 | } 19 | 20 | impl Task for Water { 21 | fn weight(&self, _ctx: Context) -> f32 { 22 | config().action_weights.water 23 | } 24 | 25 | fn duration(&self, _ctx: Context) -> TaskDuration { 26 | 0 27 | } 28 | 29 | fn execute(&self, ctx: ContextMut) -> Option>> { 30 | let ContextMut { 31 | mut state_diff, 32 | agent, 33 | .. 34 | } = ctx; 35 | state_diff.increment_time(); 36 | 37 | if let Some((x, y)) = state_diff.find_agent(agent) { 38 | state_diff.set_water(agent, false); 39 | 40 | let (x, y) = apply_direction(self.direction, x, y); 41 | if let Some(Tile::Tree(height)) = state_diff.get_tile_ref_mut(x, y) { 42 | *height = config().map.tree_height; 43 | } 44 | 45 | Some(Box::new(IdleTask)) 46 | } else { 47 | unreachable!("Failed to find agent on map"); 48 | } 49 | } 50 | 51 | fn display_action(&self) -> ::DisplayAction { 52 | Action::Water(self.direction) 53 | } 54 | 55 | fn is_valid(&self, ctx: Context) -> bool { 56 | let Context { 57 | state_diff, agent, .. 58 | } = ctx; 59 | state_diff.get_water(agent) 60 | && if let Some((x, y)) = state_diff.find_agent(agent) { 61 | let (x, y) = apply_direction(self.direction, x, y); 62 | matches!(state_diff.get_tile(x, y), Some(Tile::Tree(_))) 63 | } else { 64 | unreachable!("Failed to find agent on map"); 65 | } 66 | } 67 | 68 | impl_task_boxed_methods!(Lumberjacks); 69 | } 70 | --------------------------------------------------------------------------------