diff --git a/Cargo.toml b/Cargo.toml index 83dc44d..ca0c338 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,3 +7,10 @@ authors = ["Adrian Heine "] [dependencies] tui = "0.15" termion = "1.5" + +[dev-dependencies] +criterion = "0.3" + +[[bench]] +name = "benchmark" +harness = false diff --git a/README.md b/README.md new file mode 100644 index 0000000..3b11625 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# gntag + +gntag (from German »Guten Tag«: »good day«, roughly equivalent to »g'day«) is an agent-based simulation of [tag](https://en.wikipedia.org/wiki/Tag_(game)) implemented in rust. + +## Running + +```sh +git clone https://git.adrianheine.de/adrian/gntag.git +cd gntag +cargo run +``` + +To exit the simulation, press q, ESC or Ctrl+c. + +## Using as a library + +`src/main.rs` is a pretty minimal example of the currently exposed high-level functionality. `gntag::agent` includes simpler agent iplementations and all the types, `gntag::world` contains `ActualWorld` and `WorldState`. + +## Tests + +A few tests can be run with: + +```sh +cargo test +``` + +## Benchmarks + +`cargo bench` runs benchmarks. Their results are available under `target/criterion/report/index.html`. + +## Simulation + +* The world is a simple two-dimensional rectangle with integer positions +* Agents can occupy the same space +* Agents can make one move per simulation step, either by trying to move by at most one unit in both directions or by trying to tag another agent +* Agents need to be at most one unit away in both directions from others in order to tag them +* Agents act at the same time and their actions are validated against the previous state diff --git a/benches/benchmark.rs b/benches/benchmark.rs new file mode 100644 index 0000000..78d990e --- /dev/null +++ b/benches/benchmark.rs @@ -0,0 +1,39 @@ +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; +use gntag::get_world; + +fn world(c: &mut Criterion) { + let mut group = c.benchmark_group("world"); + let width: isize = 1000; + for spacing in (50..=100).step_by(25) { + group.throughput(Throughput::Elements((width / spacing).pow(2) as u64)); + group.bench_with_input( + BenchmarkId::new("validating", spacing), + &spacing, + |b, &spacing| { + let mut world = get_world(width, width, spacing as usize, true); + b.iter(|| world.do_step()); + }, + ); + group.bench_with_input( + BenchmarkId::new("non-validating", spacing), + &spacing, + |b, &spacing| { + let mut world = get_world(width, width, spacing as usize, false); + b.iter(|| world.do_step()); + }, + ); + } + group.finish(); +} + +criterion_group!(benches, world); +criterion_main!(benches); + +// 5000/1000 -> 9,993 +// 5/01 -> 23,252 +// 50/10 -> 14,000 +// 100/20 -> 14,000 +// 100/10 -> 96,000 +// 100/05 -> 917,767 +// 200/10 -> 916,172 +// 1000/10 -> 443,482,526 diff --git a/src/agent.rs b/src/agent.rs index 2fdc50b..7cd68f7 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -1,5 +1,6 @@ pub type Distance = isize; +#[derive(Clone)] pub struct Position { pub x: Distance, pub y: Distance, @@ -25,12 +26,12 @@ impl From<(Distance, Distance)> for Direction { pub type AgentId = usize; -pub struct WorldView { +pub struct WorldView<'o> { pub self_id: AgentId, pub tagged: AgentId, pub tagged_by: Option, pub bounds_distance: (Distance, Distance, Distance, Distance), - pub other_agents: Vec<(Direction, AgentId)>, + pub other_agents: &'o mut dyn Iterator, } #[derive(Debug)] @@ -109,7 +110,7 @@ fn random_move_within( loop { let mv = random_move(); if let Move::TryMove(Direction { x, y }) = mv { - if top + y > 0 && bottom - y > 0 && left + x > 0 && right - x > 0 { + if top + y >= 0 && bottom - y >= 0 && left + x >= 0 && right - x >= 0 { return mv; } } diff --git a/src/lib.rs b/src/lib.rs index 53a7904..6e4f78e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,67 @@ pub mod agent; pub mod view; pub mod world; + +use agent::{Agent, SimpleAgent}; +use std::error::Error; +use std::io; +use std::io::Read; +use std::process::exit; +use std::sync::{Arc, Mutex}; +use std::thread; +use view::{RawTerminal, TerminalView, TermionBackend}; +use world::ActualWorld; + +pub fn get_world(width: isize, height: isize, spacing: usize, validating: bool) -> ActualWorld { + let mut agents: Vec<(_, Box)> = vec![]; + for x in (0..width).step_by(spacing) { + for y in (0..height).step_by(spacing) { + agents.push(((x, y).into(), Box::new(SimpleAgent))); + } + } + ActualWorld::new((width, height).into(), agents, validating) +} + +pub type DefaultView = Arc>>>>>; +pub fn get_view() -> DefaultView { + let view = Arc::new(Mutex::new(Some(TerminalView::try_new().unwrap()))); + + // Exit on q, ESC and Ctrl-C and reset terminal + let view2 = view.clone(); + thread::spawn(move || { + let stdin = io::stdin(); + for byte in stdin.bytes().flatten() { + if byte == b'q' || byte == 0x1b || byte == 0x03 { + if let Ok(mut view) = view2.lock() { + *view = None; // drop view + println!("\n"); + } + exit(0); + } + } + }); + view +} + +pub fn draw_world( + world: &ActualWorld, + gen: usize, + view: &DefaultView, +) -> Result> { + (*view.lock().unwrap()).as_mut().unwrap().draw( + gen, + world + .state + .agent_positions + .get(&world.state.tagged) + .map(|pos| (pos.x, pos.y)) + .unwrap(), + world + .state + .agent_positions + .iter() + .map(|(_id, pos)| (pos.x, pos.y)) + .collect::>() + .as_ref(), + ) +} diff --git a/src/main.rs b/src/main.rs index b21a9c9..777bc82 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,69 +1,28 @@ -use gntag::agent::{Agent, SimpleAgent}; -use gntag::view::TerminalView; -use gntag::world::ActualWorld; -use std::io; -use std::io::Read; -use std::process::exit; -use std::sync::{Arc, Mutex}; -use std::thread; +use gntag::{draw_world, get_view, get_world, DefaultView}; fn main() { - let view = Arc::new(Mutex::new(Some(TerminalView::try_new().unwrap()))); - let view2 = view.clone(); - thread::spawn(move || { - let stdin = io::stdin(); - for byte in stdin.bytes().flatten() { - if byte == b'q' || byte == 0x1b || byte == 0x03 { - if let Ok(mut view) = view2.lock() { - *view = None; // drop view - println!("\n"); - } - exit(0); - } - } - }); - loop { - let (width, height) = (*view.lock().unwrap()) - .as_mut() - .unwrap() - .content_size() - .unwrap(); - let mut agents: Vec<(_, Box)> = vec![]; - for x in (0..width).step_by(10) { - for y in (0..height).step_by(10) { - agents.push(((x, y).into(), Box::new(SimpleAgent))); - } - } - let mut world = ActualWorld::new((width, height).into(), agents); + let view = get_view(); - let mut gen = 0; - loop { - let resized = (*view.lock().unwrap()) - .as_mut() - .unwrap() - .draw( - gen, - world - .state - .agent_positions - .get(&world.state.tagged) - .map(|pos| (pos.x, pos.y)) - .unwrap(), - world - .state - .agent_positions - .iter() - .map(|(_id, pos)| (pos.x, pos.y)) - .collect::>() - .as_ref(), - ) - .unwrap(); - if resized { - break; - }; - world.do_step(); - gen += 1; - //std::thread::sleep(std::time::Duration::from_millis(500)); - } + loop { + run_simulation(&view); + } +} + +fn run_simulation(view: &DefaultView) { + let (width, height) = (*view.lock().unwrap()) + .as_mut() + .unwrap() + .content_size() + .unwrap(); + let mut world = get_world(width, height, 10, true); + + let mut gen = 0; + loop { + let resized = draw_world(&world, gen, view).unwrap(); + if resized { + return; + }; + world.do_step(); + gen += 1; } } diff --git a/src/view.rs b/src/view.rs index 320eaee..8aa8a21 100644 --- a/src/view.rs +++ b/src/view.rs @@ -2,8 +2,10 @@ use std::convert::TryInto; use std::error::Error; use std::io::{stdout, Stdout, Write}; use termion::clear; -use termion::raw::{IntoRawMode, RawTerminal}; -use tui::backend::{Backend, TermionBackend}; +use termion::raw::IntoRawMode; +pub use termion::raw::RawTerminal; +use tui::backend::Backend; +pub use tui::backend::TermionBackend; use tui::layout::{Alignment, Constraint, Direction, Layout, Rect}; use tui::style::{Color, Modifier, Style}; use tui::text::{Span, Spans}; diff --git a/src/world.rs b/src/world.rs index 9c1ecc4..6633052 100644 --- a/src/world.rs +++ b/src/world.rs @@ -5,6 +5,7 @@ pub struct ActualWorld { size: Position, pub agents: HashMap>, pub state: WorldState, + validating: bool, } pub struct WorldState { @@ -16,7 +17,11 @@ pub struct WorldState { impl ActualWorld { /// Agents receive incrementing ids starting with 0 /// The last agent is 'It' - pub fn new(size: Position, input_agents: Vec<(Position, Box)>) -> Self { + pub fn new( + size: Position, + input_agents: Vec<(Position, Box)>, + validating: bool, + ) -> Self { let agent_count = input_agents.len(); assert!(agent_count > 0); let mut id = 0; @@ -36,6 +41,7 @@ impl ActualWorld { size, agents, state, + validating, } } @@ -51,8 +57,13 @@ impl ActualWorld { self_id: *id, tagged_by: self.state.tagged_by, tagged: self.state.tagged, - bounds_distance: (pos.y, self.size.x - pos.x, self.size.y - pos.y, pos.x), - other_agents: self + bounds_distance: ( + pos.y, + self.size.x - pos.x - 1, + self.size.y - pos.y - 1, + pos.x, + ), + other_agents: &mut self .state .agent_positions .iter() @@ -65,8 +76,7 @@ impl ActualWorld { }, *id, ) - }) - .collect(), + }), }), ) }) @@ -77,9 +87,11 @@ impl ActualWorld { tagged_by: self.state.tagged_by, }; for (id, mv) in moves { - self.check_move(id, &mv); - //println!("{} {:?}", id, mv); + if self.validating { + self.check_move(id, &mv); + } let mut new_pos = None; + let pos = self.state.agent_positions.get(&id).unwrap(); match mv { Move::Noop => {} Move::TryTag(other_id) => { @@ -87,14 +99,12 @@ impl ActualWorld { new_state.tagged_by = Some(id); } Move::TryMove(dir) => { - let pos = self.state.agent_positions.get(&id).unwrap(); new_pos = Some((pos.x + dir.x, pos.y + dir.y).into()); } } - new_state.agent_positions.insert( - id, - new_pos.unwrap_or_else(|| self.state.agent_positions.remove(&id).unwrap()), - ); + new_state + .agent_positions + .insert(id, new_pos.unwrap_or_else(|| pos.clone())); } self.state = new_state; } @@ -102,7 +112,10 @@ impl ActualWorld { match mv { Move::Noop => {} Move::TryTag(other_id) => { - // FIXME check distance + let my_pos = self.state.agent_positions.get(&id).unwrap(); + let other_pos = self.state.agent_positions.get(&other_id).unwrap(); + assert!((my_pos.x - other_pos.x).abs() <= 1); + assert!((my_pos.y - other_pos.y).abs() <= 1); assert_eq!(self.state.tagged, id); assert_ne!(self.state.tagged_by, Some(*other_id)); assert!( @@ -111,12 +124,13 @@ impl ActualWorld { ); } Move::TryMove(dir) => { - // FIXME check speed + assert!(dir.x.abs() <= 1); + assert!(dir.y.abs() <= 1); let pos = self.state.agent_positions.get(&id).unwrap(); let size = &self.size; - assert!(pos.x + dir.x > 0); + assert!(pos.x + dir.x >= 0); assert!(pos.x + dir.x < size.x); - assert!(pos.y + dir.y > 0); + assert!(pos.y + dir.y >= 0); assert!(pos.y + dir.y < size.y); } } @@ -131,7 +145,7 @@ mod test { #[test] #[should_panic] fn empty() { - ActualWorld::new(Position { x: 0, y: 0 }, vec![]); + ActualWorld::new(Position { x: 0, y: 0 }, vec![], true); } #[test] @@ -139,10 +153,11 @@ mod test { let mut world = ActualWorld::new( Position { x: 0, y: 0 }, vec![(Position { x: 0, y: 0 }, Box::new(NullAgent))], + true, ); world.do_step(); - assert_eq!(world.tagged, 0); - assert_eq!(world.tagged_by, None); + assert_eq!(world.state.tagged, 0); + assert_eq!(world.state.tagged_by, None); } #[test] @@ -159,10 +174,11 @@ mod test { Box::new(ScriptedAgent::new(vec![Move::TryTag(0)])), ), ], + true, ); world.do_step(); - assert_eq!(world.tagged, 0); - assert_eq!(world.tagged_by, Some(1)); + assert_eq!(world.state.tagged, 0); + assert_eq!(world.state.tagged_by, Some(1)); } #[test] @@ -174,6 +190,7 @@ mod test { Position { x: 0, y: 0 }, Box::new(ScriptedAgent::new(vec![Move::TryMove((-1, -1).into())])), )], + true, ); world.do_step(); }