diff --git a/src/system/spaces/square_grid.rs b/src/system/spaces/square_grid.rs index d7690cb..823c362 100644 --- a/src/system/spaces/square_grid.rs +++ b/src/system/spaces/square_grid.rs @@ -3,7 +3,7 @@ use num_integer::Integer; use crate::system::{GriddedPosition, Position}; use serde::{Serialize, Deserialize}; -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] pub struct Grid2D { pub x: i32, pub y: i32, diff --git a/src/system/walker.rs b/src/system/walker.rs index bd943c7..5911782 100644 --- a/src/system/walker.rs +++ b/src/system/walker.rs @@ -1,6 +1,10 @@ +use std::hash::Hash; +use bevy::render::render_resource::encase::private::RuntimeSizedArray; +use itertools::Itertools; +use rand::distributions::Slice; use rand::prelude::Rng; use crate::system::{GriddedPosition, Position}; -use crate::system::spaces::square_grid::Grid3D; +use crate::system::spaces::square_grid::{Grid2D, Grid3D}; pub trait Walker { fn walk(&self, rng: &mut R, position: &P) -> P; @@ -18,14 +22,131 @@ pub struct DiagonalRandomWalker; impl Walker for DiagonalRandomWalker { fn walk(&self, rng: &mut R, position: &Grid3D) -> Grid3D { - let a: Vec = (0..3) - .map(|r| rng.gen_range(-1..=1)) - .collect(); + static OFFSETS: [Grid3D; 26] = [ + Grid3D { x: 1, y: 0, z: 0 }, + Grid3D { x: 1, y: 1, z: 0 }, + Grid3D { x: 1, y: -1, z: 0 }, + Grid3D { x: -1, y: 0, z: 0 }, + Grid3D { x: -1, y: 1, z: 0 }, + Grid3D { x: -1, y: -1, z: 0 }, + Grid3D { x: 0, y: 1, z: 0 }, + Grid3D { x: 0, y: -1, z: 0 }, + Grid3D { x: 1, y: 0, z: -1 }, + Grid3D { x: 1, y: 1, z: -1 }, + Grid3D { x: 1, y: -1, z: -1 }, + Grid3D { x: -1, y: 0, z: -1 }, + Grid3D { x: -1, y: 1, z: -1 }, + Grid3D { x: -1, y: -1, z: -1 }, + Grid3D { x: 0, y: 1, z: -1 }, + Grid3D { x: 0, y: -1, z: -1 }, + Grid3D { x: 0, y: 0, z: -1 }, + Grid3D { x: 1, y: 0, z: 1 }, + Grid3D { x: 1, y: 1, z: 1 }, + Grid3D { x: 1, y: -1, z: 1 }, + Grid3D { x: -1, y: 0, z: 1 }, + Grid3D { x: -1, y: 1, z: 1 }, + Grid3D { x: -1, y: -1, z: 1 }, + Grid3D { x: 0, y: 1, z: 1 }, + Grid3D { x: 0, y: -1, z: 1 }, + Grid3D { x: 0, y: 0, z: 1 }, + ]; - position.clone() + Grid3D::from_cartesian([a[0] as f32, a[1] as f32, a[2] as f32]) + position.clone() + OFFSETS[rng.gen_range(0..OFFSETS.len())].clone() } } +impl Walker for DiagonalRandomWalker { + fn walk(&self, rng: &mut R, position: &Grid2D) -> Grid2D { + static OFFSETS: [Grid2D; 8] = [ + Grid2D { x: 1, y: 0 }, + Grid2D { x: 1, y: 1 }, + Grid2D { x: 1, y: -1 }, + Grid2D { x: -1, y: 0 }, + Grid2D { x: -1, y: 1 }, + Grid2D { x: -1, y: -1 }, + Grid2D { x: 0, y: 1 }, + Grid2D { x: 0, y: -1 }, + ]; + + position.clone() + OFFSETS[rng.gen_range(0..OFFSETS.len())].clone() + } +} + +fn test_uniformity_and_range, P>(walker: W, expected: &[P], n: usize, tolerance: f32) where P: GriddedPosition + Hash + Eq { + use rand::thread_rng; + let mut rng = thread_rng(); + let origin = &P::zero(); + + let results: Vec

= (0..n) + .map(|_| walker.walk(&mut rng, origin)) + .collect(); + + let groups = results.iter() + .into_group_map_by(|a| (*a).clone()); + + assert_eq!(groups.len(), expected.len(), "Wrong number of walk positions generated"); + assert!(results.iter().unique().all(|a| expected.contains(a)), "Contains unexpected walk position"); + + for group in groups.values() { + let proportion = group.len() as f32 / n as f32; + assert!((proportion - (1.0 / expected.len() as f32)).abs() < tolerance, "Failed tolerance check"); + } +} + +#[test] +fn uniformity_direct_grid2d() { + test_uniformity_and_range(LocalRandomWalker, &[ + Grid2D { x: 1, y: 0 }, + Grid2D { x: -1, y: 0 }, + Grid2D { x: 0, y: 1 }, + Grid2D { x: 0, y: -1 }, + ], 1_000_000, 0.001); +} + +#[test] +fn uniformity_direct_grid3d() { + test_uniformity_and_range(LocalRandomWalker, &[ + Grid3D { x: 1, y: 0, z: 0 }, + Grid3D { x: -1, y: 0, z: 0 }, + Grid3D { x: 0, y: 1, z: 0 }, + Grid3D { x: 0, y: -1, z: 0 }, + Grid3D { x: 0, y: 0, z: 1 }, + Grid3D { x: 0, y: 0, z: -1 }, + ], 1_000_000, 0.001); +} + +#[test] +fn diagonal_grid2d() { + let mut expected = Vec::new(); + + for x in -1..=1 { + for y in -1..=1 { + if !(x == 0 && y == 0) { + expected.push(Grid2D { x, y }) + } + } + } + + test_uniformity_and_range(DiagonalRandomWalker, &expected, 1_000_000, 0.001); +} + +#[test] +fn diagonal_grid3d() { + let mut expected = Vec::new(); + + for x in -1..=1 { + for y in -1..=1 { + for z in -1..=1 { + if !(x == 0 && y == 0 && z == 0) { + expected.push(Grid3D { x, y, z }) + } + } + } + } + + test_uniformity_and_range(DiagonalRandomWalker, &expected, 1_000_000, 0.001); +} + mod test { use rand::rngs::SmallRng; use rand::{SeedableRng, thread_rng}; @@ -33,42 +154,6 @@ mod test { use crate::system::spaces::square_grid::{Grid2D, Grid3D}; use crate::system::walker::{DiagonalRandomWalker, LocalRandomWalker, Walker}; - #[test] - fn uniformity() { - let walker = LocalRandomWalker; - let mut rng = SmallRng::from_rng(thread_rng()).unwrap(); - let mut results: Vec = vec![]; - - let origin = &Grid2D::zero(); - - let x: u32 = (1_000_000); - for i in 0..x { - results.push(walker.walk(&mut rng, origin)); - } - - let a = results - .iter() - .filter(|a| **a == Grid2D { x: 0, y: 1 }) - .count(); - - let b = results - .iter() - .filter(|a| **a == Grid2D { x: 0, y: -1 }) - .count(); - - let c = results - .iter() - .filter(|a| **a == Grid2D { x: 1, y: 0 }) - .count(); - - let d = results - .iter() - .filter(|a| **a == Grid2D { x: -1, y: 0 }) - .count(); - - println!("{} {} {} {}", a as f32 / x as f32, b as f32 / x as f32, c as f32 / x as f32, d as f32 / x as f32); - } - #[test] fn diagonal() { let drw = DiagonalRandomWalker;