use rand::Rng;
use rustsim_core::{
interaction::{PositionedAgent, SpaceInteraction},
space::Space,
types::AgentId,
};
use thiserror::Error;
pub type GridPos2 = (usize, usize);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
pub enum GridError {
#[error("position ({}, {}) is out of bounds", .0.0, .0.1)]
OutOfBounds(GridPos2),
#[error("position ({}, {}) is already occupied", .0.0, .0.1)]
Occupied(GridPos2),
}
#[derive(Debug, Clone)]
pub struct Grid2D {
width: usize,
height: usize,
periodic: bool,
cells: Vec<Vec<AgentId>>,
}
impl Grid2D {
pub fn new(width: usize, height: usize, periodic: bool) -> Self {
assert!(width > 0 && height > 0, "grid dimensions must be positive");
let cells = vec![Vec::new(); width * height];
Self {
width,
height,
periodic,
cells,
}
}
pub fn dimensions(&self) -> (usize, usize) {
(self.width, self.height)
}
pub fn periodic(&self) -> bool {
self.periodic
}
pub fn is_in_bounds(&self, pos: GridPos2) -> bool {
pos.0 < self.width && pos.1 < self.height
}
pub fn is_empty(&self, pos: GridPos2) -> bool {
match self.normalize_pos(pos) {
Some(pos) => self.cells[self.index(pos)].is_empty(),
None => true,
}
}
pub fn ids_in_position(&self, pos: GridPos2) -> Option<&[AgentId]> {
let pos = self.normalize_pos(pos)?;
Some(&self.cells[self.index(pos)])
}
pub fn add_agent(&mut self, pos: GridPos2, id: AgentId) -> Result<(), GridError> {
let pos = self.normalize_pos(pos).ok_or(GridError::OutOfBounds(pos))?;
let index = self.index(pos);
self.cells[index].push(id);
Ok(())
}
pub fn remove_agent(&mut self, pos: GridPos2, id: AgentId) -> Result<bool, GridError> {
let pos = self.normalize_pos(pos).ok_or(GridError::OutOfBounds(pos))?;
let index = self.index(pos);
let cell = &mut self.cells[index];
if let Some(index) = cell.iter().position(|value| *value == id) {
cell.swap_remove(index);
Ok(true)
} else {
Ok(false)
}
}
fn normalize_pos(&self, pos: GridPos2) -> Option<GridPos2> {
if self.periodic {
Some((pos.0 % self.width, pos.1 % self.height))
} else if self.is_in_bounds(pos) {
Some(pos)
} else {
None
}
}
fn index(&self, pos: GridPos2) -> usize {
pos.1 * self.width + pos.0
}
}
impl Space for Grid2D {}
impl<A> SpaceInteraction<A> for Grid2D
where
A: PositionedAgent<Position = GridPos2>,
{
type Error = GridError;
fn random_position<R: rand::RngCore>(&self, rng: &mut R) -> A::Position {
let x = rng.gen_range(0..self.width);
let y = rng.gen_range(0..self.height);
(x, y)
}
fn add_agent(&mut self, agent: &A) -> Result<(), Self::Error> {
self.add_agent(*agent.position(), agent.id())
}
fn remove_agent(&mut self, agent: &A) -> Result<(), Self::Error> {
self.remove_agent(*agent.position(), agent.id()).map(|_| ())
}
fn nearby_ids(&self, position: &A::Position, radius: usize) -> Vec<AgentId> {
let mut ids = Vec::new();
let radius = radius as i32;
let (cx, cy) = (position.0 as i32, position.1 as i32);
for dy in -radius..=radius {
for dx in -radius..=radius {
let x = cx + dx;
let y = cy + dy;
let pos = if self.periodic {
let px = ((x % self.width as i32) + self.width as i32) % self.width as i32;
let py = ((y % self.height as i32) + self.height as i32) % self.height as i32;
Some((px as usize, py as usize))
} else if x >= 0 && y >= 0 && x < self.width as i32 && y < self.height as i32 {
Some((x as usize, y as usize))
} else {
None
};
if let Some(pos) = pos {
ids.extend(self.cells[self.index(pos)].iter().copied());
}
}
}
ids
}
}
#[derive(Debug, Clone)]
pub struct Grid2DSingle {
width: usize,
height: usize,
periodic: bool,
cells: Vec<Option<AgentId>>,
}
impl Grid2DSingle {
pub fn new(width: usize, height: usize, periodic: bool) -> Self {
assert!(width > 0 && height > 0, "grid dimensions must be positive");
let cells = vec![None; width * height];
Self {
width,
height,
periodic,
cells,
}
}
pub fn dimensions(&self) -> (usize, usize) {
(self.width, self.height)
}
pub fn periodic(&self) -> bool {
self.periodic
}
pub fn is_in_bounds(&self, pos: GridPos2) -> bool {
pos.0 < self.width && pos.1 < self.height
}
pub fn is_empty(&self, pos: GridPos2) -> bool {
match self.normalize_pos(pos) {
Some(pos) => self.cells[self.index(pos)].is_none(),
None => true,
}
}
pub fn id_in_position(&self, pos: GridPos2) -> Option<AgentId> {
let pos = self.normalize_pos(pos)?;
self.cells[self.index(pos)]
}
pub fn add_agent(&mut self, pos: GridPos2, id: AgentId) -> Result<(), GridError> {
let pos = self.normalize_pos(pos).ok_or(GridError::OutOfBounds(pos))?;
let index = self.index(pos);
let cell = &mut self.cells[index];
if cell.is_some() {
return Err(GridError::Occupied(pos));
}
*cell = Some(id);
Ok(())
}
pub fn remove_agent(&mut self, pos: GridPos2, id: AgentId) -> Result<bool, GridError> {
let pos = self.normalize_pos(pos).ok_or(GridError::OutOfBounds(pos))?;
let index = self.index(pos);
let cell = &mut self.cells[index];
if cell == &Some(id) {
*cell = None;
Ok(true)
} else {
Ok(false)
}
}
fn normalize_pos(&self, pos: GridPos2) -> Option<GridPos2> {
if self.periodic {
Some((pos.0 % self.width, pos.1 % self.height))
} else if self.is_in_bounds(pos) {
Some(pos)
} else {
None
}
}
fn index(&self, pos: GridPos2) -> usize {
pos.1 * self.width + pos.0
}
}
impl Space for Grid2DSingle {}
impl<A> SpaceInteraction<A> for Grid2DSingle
where
A: PositionedAgent<Position = GridPos2>,
{
type Error = GridError;
fn random_position<R: rand::RngCore>(&self, rng: &mut R) -> A::Position {
let x = rng.gen_range(0..self.width);
let y = rng.gen_range(0..self.height);
(x, y)
}
fn add_agent(&mut self, agent: &A) -> Result<(), Self::Error> {
self.add_agent(*agent.position(), agent.id())
}
fn remove_agent(&mut self, agent: &A) -> Result<(), Self::Error> {
self.remove_agent(*agent.position(), agent.id()).map(|_| ())
}
fn nearby_ids(&self, position: &A::Position, radius: usize) -> Vec<AgentId> {
let mut ids = Vec::new();
let radius = radius as i32;
let (cx, cy) = (position.0 as i32, position.1 as i32);
for dy in -radius..=radius {
for dx in -radius..=radius {
let x = cx + dx;
let y = cy + dy;
let pos = if self.periodic {
let px = ((x % self.width as i32) + self.width as i32) % self.width as i32;
let py = ((y % self.height as i32) + self.height as i32) % self.height as i32;
Some((px as usize, py as usize))
} else if x >= 0 && y >= 0 && x < self.width as i32 && y < self.height as i32 {
Some((x as usize, y as usize))
} else {
None
};
if let Some(pos) = pos {
if let Some(id) = self.cells[self.index(pos)] {
ids.push(id);
}
}
}
}
ids
}
}