diff --git a/src/lib/ai.rs b/src/lib/ai.rs index 23381fe..c4a5c0f 100644 --- a/src/lib/ai.rs +++ b/src/lib/ai.rs @@ -1,67 +1,108 @@ use crate::lib::entity::{Ant, Egg, Queen}; -use crate::lib::point::{astar, Point}; +use crate::lib::point::{Direction, Point}; use crate::lib::screen::{BoardCommand, Screen}; -use crate::lib::world::World; +use crate::lib::world::{Pheremone, World}; use rand::prelude::SliceRandom; use rand::thread_rng; +use std::iter::zip; pub trait AI { fn step(&mut self, b: &Screen, w: &mut World) -> BoardCommand; - fn plan(&mut self, w: &World) {} + fn plan(&mut self, b: &Screen, w: &mut World) -> BoardCommand { + BoardCommand::Noop + } } #[derive(Clone, Debug)] pub enum AIGoal { Seek, - //Pickup(Point), - //Drop(), - Idle, + Return, } impl AI for Ant { - fn plan(&mut self, w: &World) { - // check last part of plan - if let Some(goal) = self.plan.last() { - match goal { - AIGoal::Seek => { - // if we reach food, we change state - if w.food.contains(&self.pos) { - self.plan.pop(); + fn plan(&mut self, b: &Screen, w: &mut World) -> BoardCommand { + match self.goal { + AIGoal::Seek => { + // if we reach food, we change state + if w.food.contains(&self.pos) { + for p in &self.history { + w.drop_pheremone(&p, &self.goal); } + self.history.clear(); + self.cw(); + self.cw(); + self.goal = AIGoal::Return; + } + BoardCommand::Noop + } + AIGoal::Return => { + if self.pos == b.center { + for p in &self.history { + w.drop_pheremone(&p, &self.goal); + } + self.history.clear(); + self.cw(); + self.cw(); + self.goal = AIGoal::Seek; + return BoardCommand::SpawnAnt + } else { + BoardCommand::Noop } - AIGoal::Idle => {} } } } - // return the next move for this ant fn step(&mut self, b: &Screen, w: &mut World) -> BoardCommand { - let goal = match self.plan.last() { - Some(g) => g, - None => &AIGoal::Idle, + + let valid = vec![ + (self.dir, self.dir.relative_point(&self.pos)), + (self.dir.ccw(), self.dir.ccw().relative_point(&self.pos)), + (self.dir.cw(), self.dir.cw().relative_point(&self.pos)), + ]; + + let ph: Vec = valid + .iter() + .map(|(_, pnt)| { + let op = w.cleared.get(pnt); + match op { + Some(ph) => ph.clone(), + None => { + w.cleared.insert(pnt.clone(), Pheremone::new()); + w.cleared.get(pnt).unwrap().clone() + } + } + }) + .collect(); + + let ph_fn = match self.goal { + AIGoal::Seek => |ph: &Pheremone| ph.food, + AIGoal::Return => |ph: &Pheremone| ph.home, }; - let valid = w.get_valid_movements(&self.pos, b, true); - let mut rng = thread_rng(); - let choice = valid.choose(&mut rng).cloned(); + let r: f32 = rand::random(); - if !choice.is_none() { - let pos = choice.unwrap(); - if w.cleared.contains(&pos) { - self.pos = pos; - } else { - - match goal { - AIGoal::Seek => { - if w.is_safe_to_dig(&pos, &b) { - w.clear(pos); - } - }, - AIGoal::Idle => {} + let mut dir = &valid[0].0; + if r < 0.2 || ph.len() == 0 { + let mut rng = thread_rng(); + let choice = valid.choose(&mut rng).unwrap(); + dir = &choice.0; + } else { + let mut greatest = &ph[0]; + for (tup, p) in zip(&valid, &ph) { + if ph_fn(p) > ph_fn(greatest) { + greatest = p; + dir = &tup.0; } } } - + + if dir == &self.dir { + self.forward(w,b); + } else if dir == &self.dir.cw() { + self.cw(); + } else if dir == &self.dir.ccw() { + self.ccw(); + } BoardCommand::Noop } } @@ -79,14 +120,14 @@ impl AI for Egg { impl AI for Queen { fn step(&mut self, b: &Screen, w: &mut World) -> BoardCommand { - let valid: Vec = w.get_valid_movements(&self.pos, b, false); + let valid: Vec = self.pos.get_neighbors(); let mut rng = thread_rng(); let choice = valid.choose(&mut rng).cloned(); if !choice.is_none() { let pos = choice.unwrap(); - if w.cleared.contains(&pos) { + if w.is_cleared(&pos) { // choose between laying an egg and moving if self.egg_count < 3 { self.egg_count += 1; diff --git a/src/lib/entity.rs b/src/lib/entity.rs index c22168a..e85199c 100644 --- a/src/lib/entity.rs +++ b/src/lib/entity.rs @@ -1,10 +1,10 @@ use crate::lib::ai::{AIGoal, AI}; -use crate::lib::point::Point; +use crate::lib::point::{Direction, Point}; use crate::lib::screen::{BoardCommand, Screen}; use crate::lib::world::World; use rand::Rng; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use ncurses::*; @@ -40,7 +40,9 @@ impl_downcast!(Renderable); pub struct Ant { pub pos: Point, pub id: u32, - pub plan: Vec, + pub goal: AIGoal, + pub dir: Direction, + pub history: HashSet, } impl Ant { @@ -48,10 +50,36 @@ impl Ant { Ant { pos: Point(x, y), id: 0, - plan: vec![], + goal: AIGoal::Seek, + dir: Direction::Up, + history: HashSet::new(), } } + + pub fn forward(&mut self, w: &mut World, b: &Screen) { + let target = self.dir.relative_point(&self.pos); + if b.is_in_bounds(&target) { + if w.is_cleared(&self.pos) { + self.history.insert(self.pos); + self.pos = target; + } else { + w.clear(target); + } + } else { + self.cw(); + self.cw(); + } + } + + pub fn ccw(&mut self) { + self.dir = self.dir.ccw(); + } + + pub fn cw(&mut self) { + self.dir = self.dir.cw(); + } } + impl_entity!(Ant); impl_entity!(Food); impl_entity!(FoodGenerator); @@ -60,14 +88,27 @@ impl_entity!(Queen); impl Renderable for Ant { fn representation(&self) -> &str { - "o" + match self.dir { + Direction::Up => "^", + Direction::Down => "v", + Direction::Right => ">", + Direction::Left => "<", + } } fn before_render(&self) { attron(A_BOLD()); + match self.goal { + AIGoal::Return => attron(A_UNDERLINE()), + AIGoal::Seek => 0, + }; } fn after_render(&self) { attroff(A_BOLD()); + match self.goal { + AIGoal::Return => attroff(A_UNDERLINE()), + AIGoal::Seek => 0, + }; } } @@ -188,8 +229,11 @@ impl Food { impl AI for Food { fn step(&mut self, b: &Screen, w: &mut World) -> BoardCommand { - // perhaps check if we're in target? - // implement drag logic? + for n in self.pos.get_neighbors() { + w.drop_pheremone(&n, &AIGoal::Seek); + } + w.drop_pheremone(&self.pos, &AIGoal::Seek); + BoardCommand::Noop } } @@ -206,12 +250,17 @@ pub struct FoodGenerator { timer: u32, pos: Point, id: u32, - counter: u32 + counter: u32, } impl FoodGenerator { pub fn new() -> FoodGenerator { - FoodGenerator { timer: 0, id: 0, pos: Point(0,0), counter: 0 } + FoodGenerator { + timer: 0, + id: 0, + pos: Point(0, 0), + counter: 0, + } } } @@ -226,7 +275,7 @@ impl AI for FoodGenerator { let mut rng = rand::thread_rng(); let r_x = rng.gen_range(min.0..max.0 + 1); let r_y = rng.gen_range(min.1..max.1 + 1); - + self.counter += 1; return BoardCommand::SpawnFood(Point(r_x, r_y)); } diff --git a/src/lib/point.rs b/src/lib/point.rs index 46aab2d..6607232 100644 --- a/src/lib/point.rs +++ b/src/lib/point.rs @@ -14,29 +14,10 @@ impl Point { Point(self.0 - 1, self.1) } - pub fn is_below(&self, other: Point) -> bool { - other.down() == *self - } - pub fn right(&self) -> Point { Point(self.0 + 1, self.1) } - pub fn uldiag(&self) -> Point { - Point(self.0 - 1, self.1 - 1) - } - - pub fn bldiag(&self) -> Point { - Point(self.0 + 1, self.1 - 1) - } - - pub fn brdiag(&self) -> Point { - Point(self.0 + 1, self.1 + 1) - } - - pub fn urdiag(&self) -> Point { - Point(self.0 - 1, self.1 + 1) - } // y values are reversed in ncurses pub fn down(&self) -> Point { Point(self.0, self.1 + 1) @@ -47,111 +28,45 @@ impl Point { } } -fn manhattan(c1: &Point, c2: &Point) -> i32 { - return (c2.0 - c1.0).abs() + (c2.1 - c1.1).abs(); -} - -fn weigh_point(w: &World, p: &Point) -> i32 { - if w.is_cleared(p) { - 3 - } else { - 15 - } +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Direction { + Up = 0, + Right = 1, + Down = 2, + Left = 3, } -pub struct PathNotFoundError; - -pub fn astar(start: &Point, goal: &Point, w: Option<&World>, s: Option<&Screen>) -> Result, PathNotFoundError> { - let mut open_set: HashSet = HashSet::new(); - open_set.insert(*start); - - let mut came_from: HashMap = HashMap::new(); - - let mut gscore: HashMap = HashMap::new(); - gscore.insert(*start, 0); - - let mut fscore: HashMap = HashMap::new(); - fscore.insert(*start, manhattan(&start, &goal)); - - let mut answer = Vec::new(); - - let mut current: Point = *start; - while !open_set.is_empty() { - let mut min_score = i32::MAX; - for c in open_set.iter() { - let val = fscore[c]; - current = if val < min_score { c.clone() } else { current }; - min_score = if val < min_score { val } else { min_score }; - } - - if current == *goal { - answer.push(current.clone()); - while came_from.contains_key(¤t) { - current = came_from[¤t].clone(); - if current != *start { - answer.push(current.clone()); - } - } - return Ok(answer); +impl Direction { + pub fn from_u8(v: u8) -> Direction { + match v { + 0 => Direction::Up, + 1 => Direction::Right, + 2 => Direction::Down, + 3 => Direction::Left, + _ => panic!("Shouldn't happen") } + } - open_set.remove(¤t); - - let current_gscore = gscore[¤t]; - - let all_neighbors: Vec = match w { - Some(w) => current - .get_neighbors() - .iter() - .filter(|p| w.is_valid_movement(¤t, p, s.unwrap())) - .map(|e| e.clone()) - .collect(), - None => current.get_neighbors(), - }; - - for neighbor in all_neighbors.iter() { - let neighbor_score = if gscore.contains_key(&neighbor) { - gscore[&neighbor] - } else { - i32::MAX - }; - - let weight = match w { - Some(w) => weigh_point(&w, neighbor), - None => 1, - }; - - let score = current_gscore + weight; + pub fn cw(&self) -> Direction { + let val = *self as u8; + Direction::from_u8((val + 1) % 4) + } - if score < neighbor_score { - gscore.insert(neighbor.clone(), score); - fscore.insert(neighbor.clone(), score + manhattan(&neighbor, &goal)); - came_from.insert(neighbor.clone(), current.clone()); - open_set.insert(neighbor.clone()); - } + pub fn ccw(&self) -> Direction { + let val = *self as u8; + if val == 0 { + Direction::Left + } else { + Direction::from_u8(val - 1) } } - Err(PathNotFoundError) -} - -#[test] -fn test_astar() { - let start = Point(0, 0); - - let goal = Point(10, 10); - let answers = astar(&start, &goal, None, None); - - match answers { - Ok(ans) => { - for a in ans.iter() { - println!("{:?}", &a); - } - }, - Err(_) => { - panic!("Path not found. That shouldn't happen!") + pub fn relative_point(&self, p: &Point) -> Point { + match self { + Direction::Up => p.up(), + Direction::Left => p.left(), + Direction::Right => p.right(), + Direction::Down => p.down(), } } - - } diff --git a/src/lib/screen.rs b/src/lib/screen.rs index ac6c73d..ea2e630 100644 --- a/src/lib/screen.rs +++ b/src/lib/screen.rs @@ -11,6 +11,8 @@ use ncurses::*; pub fn init_screen() -> Screen { initscr(); + start_color(); + init_pair(0, 0, 1); /* Invisible cursor. */ curs_set(CURSOR_VISIBILITY::CURSOR_INVISIBLE); @@ -81,5 +83,6 @@ pub enum BoardCommand { LayEgg(Point, u32), SpawnFood(Point), Hatch(u32, u32), + SpawnAnt, Noop, } diff --git a/src/lib/world.rs b/src/lib/world.rs index 222bcaf..ba88115 100644 --- a/src/lib/world.rs +++ b/src/lib/world.rs @@ -1,24 +1,46 @@ +use crate::lib::ai::AIGoal; use crate::lib::entity::{Ant, Egg, Entities, Food, Queen}; -use crate::lib::point::{astar, Point}; +use crate::lib::point::Point; use crate::lib::screen::{BoardCommand, Screen}; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; #[derive(Clone)] pub struct World { - pub cleared: HashSet, + pub cleared: HashMap, pub food: HashSet, } +#[derive(Clone, PartialEq, PartialOrd, Debug)] +pub struct Pheremone { + pub home: u32, + pub food: u32, +} + +impl Pheremone { + pub fn new() -> Pheremone { + Pheremone { home: 0, food: 0 } + } + + pub fn decay(&mut self) { + if self.home > 0 { + self.home -= 1; + } + if self.food > 0 { + self.food -= 1; + } + } +} + impl World { pub fn new() -> World { World { - cleared: HashSet::new(), + cleared: HashMap::new(), food: HashSet::new(), } } pub fn clear(&mut self, pos: Point) { - self.cleared.insert(pos); + self.cleared.insert(pos, Pheremone::new()); } pub fn create_chamber(&mut self, center: Point, radius: i32) { @@ -32,67 +54,30 @@ impl World { } } - pub fn is_valid_movement(&self, current: &Point, target: &Point, b: &Screen) -> bool { - // should allow down movements always - - let safe = target.is_below(*current) - || (!self.cleared.contains(&target.down()) - || !self.cleared.contains(&target.left()) - || !self.cleared.contains(&target.right()) - || !self.cleared.contains(&target.bldiag()) - || !self.cleared.contains(&target.uldiag()) - || !self.cleared.contains(&target.brdiag()) - || !self.cleared.contains(&target.urdiag())); - - // of course, its only a valid move if you can walk there! - b.is_in_bounds(target) && self.cleared.contains(target) && safe - } - pub fn is_cleared(&self, pos: &Point) -> bool { - self.cleared.contains(pos) + self.cleared.contains_key(pos) } - pub fn is_safe_to_dig(&self, target: &Point, b: &Screen) -> bool { - let mut hypothetical_world = self.clone(); - hypothetical_world.clear(*target); - let result = astar(target, &b.center, Some(&hypothetical_world), Some(b)); - b.is_in_bounds(target) && result.is_ok() - } + pub fn drop_pheremone(&mut self, pos: &Point, state: &AIGoal) { + let op = self.cleared.get_mut(&pos); - pub fn get_diggable(&self, b: &Screen, pos: &Point) -> Vec { - let moves = b.get_valid_movements(pos); - moves - .iter() - .filter(|p| !self.is_cleared(p)) - .map(|p| p.clone()) - .collect() - } + let ph = match op { + Some(p) => p, + None => { + self.cleared.insert(*pos, Pheremone::new()); + self.cleared.get_mut(pos).unwrap() + } + }; - pub fn get_valid_movements( - &self, - pos: &Point, - b: &Screen, - return_diggables: bool, - ) -> Vec { - let moves = b.get_valid_movements(pos); - let mut ans: Vec = moves - .iter() - .filter(|p| self.is_valid_movement(pos, p, b)) - .map(|p| p.clone()) - .collect(); - - if return_diggables { - let digs = self.get_diggable(b, pos); - ans.extend(digs); + match state { + AIGoal::Seek => ph.home += 1, + AIGoal::Return => ph.food += 1, } - - ans - } } pub fn render(e: &Entities, w: &World, b: &Screen) { - for c in w.cleared.iter() { + for c in w.cleared.keys() { b.render(c, "x"); } @@ -101,18 +86,19 @@ pub fn render(e: &Entities, w: &World, b: &Screen) { } } -pub fn simulate(e: &mut Entities, w: &mut World, b: &mut Screen) { - let cmds: Vec = e - .data - .values_mut() - .map(|a| { - a.plan(w); - a.step(b, w) - }) - .collect(); +pub fn simulate(e: &mut Entities, w: &mut World, b: &mut Screen, step: u32) { + let plan_cmds: Vec = e.data.values_mut().map(|a| a.plan(b, w)).collect(); + + let mut cmds: Vec = e.data.values_mut().map(|a| a.step(b, w)).collect(); + + cmds.extend(plan_cmds); for cmd in cmds { match cmd { + BoardCommand::SpawnAnt => { + let ant = Ant::new(b.center.0, b.center.1); + e.add_entity(&ant); + } BoardCommand::Dig(pos) => { w.clear(pos); } @@ -138,4 +124,16 @@ pub fn simulate(e: &mut Entities, w: &mut World, b: &mut Screen) { BoardCommand::Noop => {} } } + + for n in b.center.get_neighbors() { + w.drop_pheremone(&n, &AIGoal::Return); + } + w.drop_pheremone(&b.center, &AIGoal::Return); + + // decay all pheremones by some amount + if step % 60 == 0 { + for ph in w.cleared.values_mut() { + ph.decay(); + } + } } diff --git a/src/main.rs b/src/main.rs index 5f8f67e..f6935c4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,42 +7,46 @@ use std::thread::sleep; use std::time; mod lib { + pub mod ai; + pub mod entity; pub mod point; pub mod screen; pub mod world; - pub mod entity; - pub mod ai; } +use lib::entity::{Ant, Entities, FoodGenerator}; use lib::point::Point; use lib::screen::init_screen; -use lib::world::{World, simulate, render}; -use lib::entity::{Entities, Ant, FoodGenerator}; +use lib::world::{render, simulate, World}; fn main() { - let mut board = init_screen(); + let mut board = init_screen(); let mut world = World::new(); let mut entities = Entities::new(); //let q = Queen::new(board.center.0,board.center.1); //entities.add_entity(&q); - + let fg = FoodGenerator::new(); entities.add_entity(&fg); - let mut a = Ant::new(board.center.0, board.center.1); - a.plan.push(lib::ai::AIGoal::Seek); - entities.add_entity(&a); - + for _ in 0..5 { + let mut a = Ant::new(board.center.0, board.center.1); + a.goal = lib::ai::AIGoal::Seek; + entities.add_entity(&a); + } + world.create_chamber(Point(board.center.0, board.center.1), 3); + let mut t = 0; loop { // TODO: add way to break out of the loop by hitting a random key - simulate(&mut entities, &mut world, &mut board); + simulate(&mut entities, &mut world, &mut board, t); render(&entities, &world, &board); sleep(time::Duration::from_millis(100)); refresh(); + t += 1; } endwin(); } diff --git a/tests/ai_test.rs b/tests/ai_test.rs index ea10e44..00ea159 100644 --- a/tests/ai_test.rs +++ b/tests/ai_test.rs @@ -23,15 +23,15 @@ fn test_reach_astar() { let a = entities.data.get_mut(&id).unwrap(); let ant: &mut Ant = a.downcast_mut::().unwrap(); - ant.plan.push(AIGoal::Reach(Point(0, 0))); + /*ant.plan.push(AIGoal::Reach(Point(0, 0))); ant.plan.push(AIGoal::Reach(Point(20, 20))); ant.plan.push(AIGoal::Reach(Point(0, 0))); - ant.plan.push(AIGoal::Reach(Point(20, 20))); + ant.plan.push(AIGoal::Reach(Point(20, 20)));*/ // craps out... need to make sure unwrap() is safe - for _ in 0..420 { + for t in 0..420 { // TODO: add way to break out of the loop by hitting a random key - simulate(&mut entities, &mut world, &mut board); + simulate(&mut entities, &mut world, &mut board, t); render(&entities, &world, &board); sleep(time::Duration::from_millis(100)); refresh(); diff --git a/tests/entity_test.rs b/tests/entity_test.rs index 6053ed2..825511e 100644 --- a/tests/entity_test.rs +++ b/tests/entity_test.rs @@ -15,9 +15,9 @@ fn test_foodgen() { let fg = FoodGenerator::new(); entities.add_entity(&fg); - for _ in 0..60 { + for t in 0..60 { // TODO: add way to break out of the loop by hitting a random key - simulate(&mut entities, &mut world, &mut board); + simulate(&mut entities, &mut world, &mut board, t); render(&entities, &world, &board); sleep(time::Duration::from_millis(100)); refresh();