semi functioning pheremone sim

sideways
Rostyslav Hnatyshyn 9 months ago
parent 47c6eff011
commit 823b5d39b8
  1. 107
      src/lib/ai.rs
  2. 67
      src/lib/entity.rs
  3. 141
      src/lib/point.rs
  4. 3
      src/lib/screen.rs
  5. 126
      src/lib/world.rs
  6. 16
      src/main.rs
  7. 8
      tests/ai_test.rs
  8. 4
      tests/entity_test.rs

@ -1,67 +1,108 @@
use crate::lib::entity::{Ant, Egg, Queen}; 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::screen::{BoardCommand, Screen};
use crate::lib::world::World; use crate::lib::world::{Pheremone, World};
use rand::prelude::SliceRandom; use rand::prelude::SliceRandom;
use rand::thread_rng; use rand::thread_rng;
use std::iter::zip;
pub trait AI { pub trait AI {
fn step(&mut self, b: &Screen, w: &mut World) -> BoardCommand; 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)] #[derive(Clone, Debug)]
pub enum AIGoal { pub enum AIGoal {
Seek, Seek,
//Pickup(Point), Return,
//Drop(),
Idle,
} }
impl AI for Ant { impl AI for Ant {
fn plan(&mut self, w: &World) { fn plan(&mut self, b: &Screen, w: &mut World) -> BoardCommand {
// check last part of plan match self.goal {
if let Some(goal) = self.plan.last() {
match goal {
AIGoal::Seek => { AIGoal::Seek => {
// if we reach food, we change state // if we reach food, we change state
if w.food.contains(&self.pos) { if w.food.contains(&self.pos) {
self.plan.pop(); 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 { fn step(&mut self, b: &Screen, w: &mut World) -> BoardCommand {
let goal = match self.plan.last() {
Some(g) => g, let valid = vec![
None => &AIGoal::Idle, (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<Pheremone> = 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 r: f32 = rand::random();
let mut rng = thread_rng();
let choice = valid.choose(&mut rng).cloned();
if !choice.is_none() { let mut dir = &valid[0].0;
let pos = choice.unwrap(); if r < 0.2 || ph.len() == 0 {
if w.cleared.contains(&pos) { let mut rng = thread_rng();
self.pos = pos; let choice = valid.choose(&mut rng).unwrap();
dir = &choice.0;
} else { } else {
let mut greatest = &ph[0];
match goal { for (tup, p) in zip(&valid, &ph) {
AIGoal::Seek => { if ph_fn(p) > ph_fn(greatest) {
if w.is_safe_to_dig(&pos, &b) { greatest = p;
w.clear(pos); dir = &tup.0;
}
},
AIGoal::Idle => {}
} }
} }
} }
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 BoardCommand::Noop
} }
} }
@ -79,14 +120,14 @@ impl AI for Egg {
impl AI for Queen { impl AI for Queen {
fn step(&mut self, b: &Screen, w: &mut World) -> BoardCommand { fn step(&mut self, b: &Screen, w: &mut World) -> BoardCommand {
let valid: Vec<Point> = w.get_valid_movements(&self.pos, b, false); let valid: Vec<Point> = self.pos.get_neighbors();
let mut rng = thread_rng(); let mut rng = thread_rng();
let choice = valid.choose(&mut rng).cloned(); let choice = valid.choose(&mut rng).cloned();
if !choice.is_none() { if !choice.is_none() {
let pos = choice.unwrap(); let pos = choice.unwrap();
if w.cleared.contains(&pos) { if w.is_cleared(&pos) {
// choose between laying an egg and moving // choose between laying an egg and moving
if self.egg_count < 3 { if self.egg_count < 3 {
self.egg_count += 1; self.egg_count += 1;

@ -1,10 +1,10 @@
use crate::lib::ai::{AIGoal, AI}; 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::screen::{BoardCommand, Screen};
use crate::lib::world::World; use crate::lib::world::World;
use rand::Rng; use rand::Rng;
use std::collections::HashMap; use std::collections::{HashMap, HashSet};
use ncurses::*; use ncurses::*;
@ -40,7 +40,9 @@ impl_downcast!(Renderable);
pub struct Ant { pub struct Ant {
pub pos: Point, pub pos: Point,
pub id: u32, pub id: u32,
pub plan: Vec<AIGoal>, pub goal: AIGoal,
pub dir: Direction,
pub history: HashSet<Point>,
} }
impl Ant { impl Ant {
@ -48,10 +50,36 @@ impl Ant {
Ant { Ant {
pos: Point(x, y), pos: Point(x, y),
id: 0, 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!(Ant);
impl_entity!(Food); impl_entity!(Food);
impl_entity!(FoodGenerator); impl_entity!(FoodGenerator);
@ -60,14 +88,27 @@ impl_entity!(Queen);
impl Renderable for Ant { impl Renderable for Ant {
fn representation(&self) -> &str { fn representation(&self) -> &str {
"o" match self.dir {
Direction::Up => "^",
Direction::Down => "v",
Direction::Right => ">",
Direction::Left => "<",
}
} }
fn before_render(&self) { fn before_render(&self) {
attron(A_BOLD()); attron(A_BOLD());
match self.goal {
AIGoal::Return => attron(A_UNDERLINE()),
AIGoal::Seek => 0,
};
} }
fn after_render(&self) { fn after_render(&self) {
attroff(A_BOLD()); attroff(A_BOLD());
match self.goal {
AIGoal::Return => attroff(A_UNDERLINE()),
AIGoal::Seek => 0,
};
} }
} }
@ -188,8 +229,11 @@ impl Food {
impl AI for Food { impl AI for Food {
fn step(&mut self, b: &Screen, w: &mut World) -> BoardCommand { fn step(&mut self, b: &Screen, w: &mut World) -> BoardCommand {
// perhaps check if we're in target? for n in self.pos.get_neighbors() {
// implement drag logic? w.drop_pheremone(&n, &AIGoal::Seek);
}
w.drop_pheremone(&self.pos, &AIGoal::Seek);
BoardCommand::Noop BoardCommand::Noop
} }
} }
@ -206,12 +250,17 @@ pub struct FoodGenerator {
timer: u32, timer: u32,
pos: Point, pos: Point,
id: u32, id: u32,
counter: u32 counter: u32,
} }
impl FoodGenerator { impl FoodGenerator {
pub fn new() -> 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,
}
} }
} }

@ -14,29 +14,10 @@ impl Point {
Point(self.0 - 1, self.1) Point(self.0 - 1, self.1)
} }
pub fn is_below(&self, other: Point) -> bool {
other.down() == *self
}
pub fn right(&self) -> Point { pub fn right(&self) -> Point {
Point(self.0 + 1, self.1) 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 // y values are reversed in ncurses
pub fn down(&self) -> Point { pub fn down(&self) -> Point {
Point(self.0, self.1 + 1) Point(self.0, self.1 + 1)
@ -47,111 +28,45 @@ impl Point {
} }
} }
fn manhattan(c1: &Point, c2: &Point) -> i32 { #[derive(Clone, Copy, PartialEq, Eq, Debug)]
return (c2.0 - c1.0).abs() + (c2.1 - c1.1).abs(); pub enum Direction {
Up = 0,
Right = 1,
Down = 2,
Left = 3,
} }
fn weigh_point(w: &World, p: &Point) -> i32 { impl Direction {
if w.is_cleared(p) { pub fn from_u8(v: u8) -> Direction {
3 match v {
} else { 0 => Direction::Up,
15 1 => Direction::Right,
} 2 => Direction::Down,
3 => Direction::Left,
_ => panic!("Shouldn't happen")
} }
pub struct PathNotFoundError;
pub fn astar(start: &Point, goal: &Point, w: Option<&World>, s: Option<&Screen>) -> Result<Vec<Point>, PathNotFoundError> {
let mut open_set: HashSet<Point> = HashSet::new();
open_set.insert(*start);
let mut came_from: HashMap<Point, Point> = HashMap::new();
let mut gscore: HashMap<Point, i32> = HashMap::new();
gscore.insert(*start, 0);
let mut fscore: HashMap<Point, i32> = 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 { pub fn cw(&self) -> Direction {
answer.push(current.clone()); let val = *self as u8;
while came_from.contains_key(&current) { Direction::from_u8((val + 1) % 4)
current = came_from[&current].clone();
if current != *start {
answer.push(current.clone());
}
}
return Ok(answer);
} }
open_set.remove(&current); pub fn ccw(&self) -> Direction {
let val = *self as u8;
let current_gscore = gscore[&current]; if val == 0 {
Direction::Left
let all_neighbors: Vec<Point> = match w {
Some(w) => current
.get_neighbors()
.iter()
.filter(|p| w.is_valid_movement(&current, 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 { } else {
i32::MAX Direction::from_u8(val - 1)
};
let weight = match w {
Some(w) => weigh_point(&w, neighbor),
None => 1,
};
let score = current_gscore + weight;
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());
} }
} }
}
Err(PathNotFoundError)
}
#[test]
fn test_astar() {
let start = Point(0, 0);
let goal = Point(10, 10); pub fn relative_point(&self, p: &Point) -> Point {
let answers = astar(&start, &goal, None, None); match self {
Direction::Up => p.up(),
match answers { Direction::Left => p.left(),
Ok(ans) => { Direction::Right => p.right(),
for a in ans.iter() { Direction::Down => p.down(),
println!("{:?}", &a);
}
},
Err(_) => {
panic!("Path not found. That shouldn't happen!")
} }
} }
} }

@ -11,6 +11,8 @@ use ncurses::*;
pub fn init_screen() -> Screen { pub fn init_screen() -> Screen {
initscr(); initscr();
start_color();
init_pair(0, 0, 1);
/* Invisible cursor. */ /* Invisible cursor. */
curs_set(CURSOR_VISIBILITY::CURSOR_INVISIBLE); curs_set(CURSOR_VISIBILITY::CURSOR_INVISIBLE);
@ -81,5 +83,6 @@ pub enum BoardCommand {
LayEgg(Point, u32), LayEgg(Point, u32),
SpawnFood(Point), SpawnFood(Point),
Hatch(u32, u32), Hatch(u32, u32),
SpawnAnt,
Noop, Noop,
} }

@ -1,24 +1,46 @@
use crate::lib::ai::AIGoal;
use crate::lib::entity::{Ant, Egg, Entities, Food, Queen}; 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 crate::lib::screen::{BoardCommand, Screen};
use std::collections::HashSet; use std::collections::{HashMap, HashSet};
#[derive(Clone)] #[derive(Clone)]
pub struct World { pub struct World {
pub cleared: HashSet<Point>, pub cleared: HashMap<Point, Pheremone>,
pub food: HashSet<Point>, pub food: HashSet<Point>,
} }
#[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 { impl World {
pub fn new() -> World { pub fn new() -> World {
World { World {
cleared: HashSet::new(), cleared: HashMap::new(),
food: HashSet::new(), food: HashSet::new(),
} }
} }
pub fn clear(&mut self, pos: Point) { 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) { 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 { 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 { pub fn drop_pheremone(&mut self, pos: &Point, state: &AIGoal) {
let mut hypothetical_world = self.clone(); let op = self.cleared.get_mut(&pos);
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 get_diggable(&self, b: &Screen, pos: &Point) -> Vec<Point> { let ph = match op {
let moves = b.get_valid_movements(pos); Some(p) => p,
moves None => {
.iter() self.cleared.insert(*pos, Pheremone::new());
.filter(|p| !self.is_cleared(p)) self.cleared.get_mut(pos).unwrap()
.map(|p| p.clone())
.collect()
} }
};
pub fn get_valid_movements( match state {
&self, AIGoal::Seek => ph.home += 1,
pos: &Point, AIGoal::Return => ph.food += 1,
b: &Screen,
return_diggables: bool,
) -> Vec<Point> {
let moves = b.get_valid_movements(pos);
let mut ans: Vec<Point> = 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);
} }
ans
} }
} }
pub fn render(e: &Entities, w: &World, b: &Screen) { 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"); 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) { pub fn simulate(e: &mut Entities, w: &mut World, b: &mut Screen, step: u32) {
let cmds: Vec<BoardCommand> = e let plan_cmds: Vec<BoardCommand> = e.data.values_mut().map(|a| a.plan(b, w)).collect();
.data
.values_mut() let mut cmds: Vec<BoardCommand> = e.data.values_mut().map(|a| a.step(b, w)).collect();
.map(|a| {
a.plan(w); cmds.extend(plan_cmds);
a.step(b, w)
})
.collect();
for cmd in cmds { for cmd in cmds {
match cmd { match cmd {
BoardCommand::SpawnAnt => {
let ant = Ant::new(b.center.0, b.center.1);
e.add_entity(&ant);
}
BoardCommand::Dig(pos) => { BoardCommand::Dig(pos) => {
w.clear(pos); w.clear(pos);
} }
@ -138,4 +124,16 @@ pub fn simulate(e: &mut Entities, w: &mut World, b: &mut Screen) {
BoardCommand::Noop => {} 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();
}
}
} }

@ -7,17 +7,17 @@ use std::thread::sleep;
use std::time; use std::time;
mod lib { mod lib {
pub mod ai;
pub mod entity;
pub mod point; pub mod point;
pub mod screen; pub mod screen;
pub mod world; pub mod world;
pub mod entity;
pub mod ai;
} }
use lib::entity::{Ant, Entities, FoodGenerator};
use lib::point::Point; use lib::point::Point;
use lib::screen::init_screen; use lib::screen::init_screen;
use lib::world::{World, simulate, render}; use lib::world::{render, simulate, World};
use lib::entity::{Entities, Ant, FoodGenerator};
fn main() { fn main() {
let mut board = init_screen(); let mut board = init_screen();
@ -31,18 +31,22 @@ fn main() {
let fg = FoodGenerator::new(); let fg = FoodGenerator::new();
entities.add_entity(&fg); entities.add_entity(&fg);
for _ in 0..5 {
let mut a = Ant::new(board.center.0, board.center.1); let mut a = Ant::new(board.center.0, board.center.1);
a.plan.push(lib::ai::AIGoal::Seek); a.goal = lib::ai::AIGoal::Seek;
entities.add_entity(&a); entities.add_entity(&a);
}
world.create_chamber(Point(board.center.0, board.center.1), 3); world.create_chamber(Point(board.center.0, board.center.1), 3);
let mut t = 0;
loop { loop {
// TODO: add way to break out of the loop by hitting a random key // 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); render(&entities, &world, &board);
sleep(time::Duration::from_millis(100)); sleep(time::Duration::from_millis(100));
refresh(); refresh();
t += 1;
} }
endwin(); endwin();
} }

@ -23,15 +23,15 @@ fn test_reach_astar() {
let a = entities.data.get_mut(&id).unwrap(); let a = entities.data.get_mut(&id).unwrap();
let ant: &mut Ant = a.downcast_mut::<Ant>().unwrap(); let ant: &mut Ant = a.downcast_mut::<Ant>().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(20, 20)));
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(20, 20)));*/
// craps out... need to make sure unwrap() is safe // 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 // 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); render(&entities, &world, &board);
sleep(time::Duration::from_millis(100)); sleep(time::Duration::from_millis(100));
refresh(); refresh();

@ -15,9 +15,9 @@ fn test_foodgen() {
let fg = FoodGenerator::new(); let fg = FoodGenerator::new();
entities.add_entity(&fg); 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 // 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); render(&entities, &world, &board);
sleep(time::Duration::from_millis(100)); sleep(time::Duration::from_millis(100));
refresh(); refresh();

Loading…
Cancel
Save