1use murk_space::Space;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum GridConnectivity {
13 FourWay,
15 EightWay,
17 Hex,
19}
20
21#[derive(Debug, Clone)]
31pub struct GridGeometry {
32 pub coord_dims: Vec<u32>,
34 pub coord_strides: Vec<usize>,
36 pub ndim: usize,
38 pub all_wrap: bool,
40 pub connectivity: GridConnectivity,
42}
43
44impl GridGeometry {
45 pub fn from_space(space: &dyn Space) -> Option<Self> {
51 if let Some(sq4) = space.downcast_ref::<murk_space::Square4>() {
53 let all_wrap = sq4.edge_behavior() == murk_space::EdgeBehavior::Wrap;
54 return Some(GridGeometry {
55 coord_dims: vec![sq4.rows(), sq4.cols()],
56 coord_strides: vec![sq4.cols() as usize, 1],
57 ndim: 2,
58 all_wrap,
59 connectivity: GridConnectivity::FourWay,
60 });
61 }
62
63 if let Some(sq8) = space.downcast_ref::<murk_space::Square8>() {
65 let all_wrap = sq8.edge_behavior() == murk_space::EdgeBehavior::Wrap;
66 return Some(GridGeometry {
67 coord_dims: vec![sq8.rows(), sq8.cols()],
68 coord_strides: vec![sq8.cols() as usize, 1],
69 ndim: 2,
70 all_wrap,
71 connectivity: GridConnectivity::EightWay,
72 });
73 }
74
75 if let Some(hex) = space.downcast_ref::<murk_space::Hex2D>() {
79 return Some(GridGeometry {
80 coord_dims: vec![hex.cols(), hex.rows()],
81 coord_strides: vec![1, hex.cols() as usize],
82 ndim: 2,
83 all_wrap: false,
84 connectivity: GridConnectivity::Hex,
85 });
86 }
87
88 None
89 }
90
91 pub fn canonical_rank(&self, coord: &[i32]) -> usize {
96 debug_assert_eq!(coord.len(), self.ndim);
97 let mut rank = 0usize;
98 for (c, &stride) in coord.iter().zip(&self.coord_strides) {
99 rank += *c as usize * stride;
100 }
101 rank
102 }
103
104 pub fn in_bounds(&self, coord: &[i32]) -> bool {
106 if coord.len() != self.ndim {
107 return false;
108 }
109 for (c, &dim) in coord.iter().zip(&self.coord_dims) {
110 if *c < 0 || *c >= dim as i32 {
111 return false;
112 }
113 }
114 true
115 }
116
117 pub fn is_interior(&self, center: &[i32], radius: u32) -> bool {
125 if self.all_wrap {
126 return true;
127 }
128 let r = radius as i32;
129 for (c, &dim) in center.iter().zip(&self.coord_dims) {
130 if *c < r || *c + r >= dim as i32 {
131 return false;
132 }
133 }
134 true
135 }
136
137 pub fn graph_distance(&self, relative: &[i32]) -> u32 {
144 match self.connectivity {
145 GridConnectivity::FourWay => relative.iter().map(|&d| d.unsigned_abs()).sum(),
146 GridConnectivity::EightWay => relative
147 .iter()
148 .map(|&d| d.unsigned_abs())
149 .max()
150 .unwrap_or(0),
151 GridConnectivity::Hex => {
152 debug_assert_eq!(relative.len(), 2);
154 let dq = relative[0];
155 let dr = relative[1];
156 let ds = dq + dr; dq.unsigned_abs()
158 .max(dr.unsigned_abs())
159 .max(ds.unsigned_abs())
160 }
161 }
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168 use murk_space::{EdgeBehavior, Hex2D, Square4, Square8};
169
170 #[test]
171 fn extract_square4() {
172 let s = Square4::new(10, 8, EdgeBehavior::Absorb).unwrap();
173 let geo = GridGeometry::from_space(&s).unwrap();
174 assert_eq!(geo.coord_dims, vec![10, 8]); assert_eq!(geo.coord_strides, vec![8, 1]);
176 assert_eq!(geo.ndim, 2);
177 assert!(!geo.all_wrap);
178 }
179
180 #[test]
181 fn extract_square4_wrap() {
182 let s = Square4::new(5, 5, EdgeBehavior::Wrap).unwrap();
183 let geo = GridGeometry::from_space(&s).unwrap();
184 assert!(geo.all_wrap);
185 }
186
187 #[test]
188 fn extract_square8() {
189 let s = Square8::new(6, 7, EdgeBehavior::Clamp).unwrap();
190 let geo = GridGeometry::from_space(&s).unwrap();
191 assert_eq!(geo.coord_dims, vec![6, 7]);
192 assert!(!geo.all_wrap);
193 }
194
195 #[test]
196 fn extract_hex2d() {
197 let s = Hex2D::new(12, 15).unwrap();
198 let geo = GridGeometry::from_space(&s).unwrap();
199 assert_eq!(geo.coord_dims, vec![15, 12]);
201 assert_eq!(geo.coord_strides, vec![1, 15]);
202 assert!(!geo.all_wrap);
203 }
204
205 #[test]
206 fn extract_line1d_returns_none() {
207 let s = murk_space::Line1D::new(10, EdgeBehavior::Absorb).unwrap();
208 assert!(GridGeometry::from_space(&s).is_none());
209 }
210
211 #[test]
212 fn extract_product_returns_none() {
213 let a = murk_space::Line1D::new(5, EdgeBehavior::Absorb).unwrap();
214 let b = murk_space::Line1D::new(3, EdgeBehavior::Absorb).unwrap();
215 let p = murk_space::ProductSpace::new(vec![Box::new(a), Box::new(b)]).unwrap();
216 assert!(GridGeometry::from_space(&p).is_none());
217 }
218
219 #[test]
220 fn canonical_rank_matches_space() {
221 let s = Square4::new(5, 7, EdgeBehavior::Absorb).unwrap();
222 let geo = GridGeometry::from_space(&s).unwrap();
223 assert_eq!(geo.canonical_rank(&[3, 4]), 25);
225 assert_eq!(s.canonical_rank(&smallvec::smallvec![3, 4]), Some(25));
226 }
227
228 #[test]
229 fn canonical_rank_hex_matches() {
230 let s = Hex2D::new(5, 7).unwrap();
231 let geo = GridGeometry::from_space(&s).unwrap();
232 assert_eq!(geo.canonical_rank(&[3, 2]), 17);
234 assert_eq!(s.canonical_rank(&smallvec::smallvec![3, 2]), Some(17));
235 }
236
237 #[test]
238 fn in_bounds_checks() {
239 let s = Square4::new(5, 5, EdgeBehavior::Absorb).unwrap();
240 let geo = GridGeometry::from_space(&s).unwrap();
241 assert!(geo.in_bounds(&[0, 0]));
242 assert!(geo.in_bounds(&[4, 4]));
243 assert!(!geo.in_bounds(&[-1, 0]));
244 assert!(!geo.in_bounds(&[5, 0]));
245 assert!(!geo.in_bounds(&[0, 5]));
246 }
247
248 #[test]
249 fn is_interior_absorb() {
250 let s = Square4::new(20, 20, EdgeBehavior::Absorb).unwrap();
251 let geo = GridGeometry::from_space(&s).unwrap();
252 assert!(geo.is_interior(&[10, 10], 3));
254 assert!(!geo.is_interior(&[2, 10], 3));
256 assert!(!geo.is_interior(&[17, 10], 3));
258 }
259
260 #[test]
261 fn is_interior_wrap_always_true() {
262 let s = Square4::new(5, 5, EdgeBehavior::Wrap).unwrap();
263 let geo = GridGeometry::from_space(&s).unwrap();
264 assert!(geo.is_interior(&[0, 0], 3));
265 assert!(geo.is_interior(&[4, 4], 10));
266 }
267
268 #[test]
269 fn interior_count_20x20_radius3() {
270 let s = Square4::new(20, 20, EdgeBehavior::Absorb).unwrap();
271 let geo = GridGeometry::from_space(&s).unwrap();
272 let mut interior = 0;
273 for r in 0..20 {
274 for c in 0..20 {
275 if geo.is_interior(&[r, c], 3) {
276 interior += 1;
277 }
278 }
279 }
280 assert_eq!(interior, 196);
285 assert!(interior as f64 / 400.0 > 0.45);
286 }
287
288 #[test]
291 fn graph_distance_four_way_manhattan() {
292 let s = Square4::new(10, 10, EdgeBehavior::Absorb).unwrap();
293 let geo = GridGeometry::from_space(&s).unwrap();
294 assert_eq!(geo.graph_distance(&[0, 0]), 0);
295 assert_eq!(geo.graph_distance(&[1, 0]), 1);
296 assert_eq!(geo.graph_distance(&[0, 1]), 1);
297 assert_eq!(geo.graph_distance(&[1, 1]), 2); assert_eq!(geo.graph_distance(&[-2, 3]), 5);
299 }
300
301 #[test]
302 fn graph_distance_eight_way_chebyshev() {
303 let s = Square8::new(10, 10, EdgeBehavior::Absorb).unwrap();
304 let geo = GridGeometry::from_space(&s).unwrap();
305 assert_eq!(geo.graph_distance(&[0, 0]), 0);
306 assert_eq!(geo.graph_distance(&[1, 1]), 1); assert_eq!(geo.graph_distance(&[-2, 3]), 3);
308 assert_eq!(geo.graph_distance(&[5, -3]), 5);
309 }
310
311 #[test]
312 fn graph_distance_hex_cube() {
313 let s = Hex2D::new(10, 10).unwrap();
314 let geo = GridGeometry::from_space(&s).unwrap();
315 assert_eq!(geo.graph_distance(&[0, 0]), 0);
317 assert_eq!(geo.graph_distance(&[1, 0]), 1);
318 assert_eq!(geo.graph_distance(&[0, 1]), 1);
319 assert_eq!(geo.graph_distance(&[1, -1]), 1); assert_eq!(geo.graph_distance(&[1, 1]), 2); assert_eq!(geo.graph_distance(&[-2, -2]), 4); assert_eq!(geo.graph_distance(&[2, -1]), 2); }
324
325 #[test]
326 fn connectivity_type_correct() {
327 let sq4 = Square4::new(5, 5, EdgeBehavior::Absorb).unwrap();
328 assert_eq!(
329 GridGeometry::from_space(&sq4).unwrap().connectivity,
330 GridConnectivity::FourWay
331 );
332 let sq8 = Square8::new(5, 5, EdgeBehavior::Absorb).unwrap();
333 assert_eq!(
334 GridGeometry::from_space(&sq8).unwrap().connectivity,
335 GridConnectivity::EightWay
336 );
337 let hex = Hex2D::new(5, 5).unwrap();
338 assert_eq!(
339 GridGeometry::from_space(&hex).unwrap().connectivity,
340 GridConnectivity::Hex
341 );
342 }
343}