aoc_ornaments/
spatial.rs

1use std::{collections::{HashMap, HashSet}, str::FromStr};
2use miette::Diagnostic;
3use thiserror::Error;
4
5pub type Position = glam::IVec2;
6pub type Velocity = glam::IVec2;
7
8pub fn manhattan_distance(a: &Position, b: &Position) -> i32 {
9    (a.x - b.x).abs() + (a.y - b.y).abs()
10}
11
12/// a Region or set of Positions
13pub type UniquePositions = HashSet<Position>;
14/// Up, Right, Down, Left
15pub const DIRECTIONS: [Position; 4] = [Position::NEG_Y, Position::X, Position::Y, Position::NEG_X];
16
17/// NW, SW, NE, SE
18pub const DIAGONALS: [Position; 4] = [Position::NEG_ONE, Position::new(-1, 1), Position::new(1, -1), Position::ONE];
19
20/// Up, SE, Right, NE, Down, NW, Left, SW
21pub const ALL_DIRECTIONS: [Position; 8] = [Position::NEG_Y, Position::ONE, Position::X, Position::new(1, -1), Position::Y, Position::NEG_ONE, Position::NEG_X, Position::new(-1, 1)];
22
23#[derive(Debug, derive_more::Deref, derive_more::DerefMut)]
24pub struct Grid<T>(pub Vec<Vec<T>>);
25
26impl FromStr for Grid<char> {
27    type Err = miette::Error;
28
29    fn from_str(input: &str) -> miette::Result<Self> {
30        Ok(Self(input.lines()
31            .map(|line| line.chars().collect()).collect()))
32    }
33}
34
35impl FromStr for Grid<bool> {
36    type Err = miette::Error;
37
38    fn from_str(input: &str) -> miette::Result<Self> {
39        Ok(Self(input.lines()
40            .map(|line| line.chars().map(|c| c == '#').collect()).collect()))
41    }
42}
43
44impl<T: std::fmt::Debug + Copy + PartialEq> Grid<T> {
45    pub fn initialize(width: usize, height: usize, value: T) -> Self {
46        let mut grid = Vec::with_capacity(height);
47        for _ in 0..height {
48            let row = vec![value; width];
49            grid.push(row);
50        }
51
52        Self(grid)
53    }
54
55    pub fn get_width(&self) -> usize {
56        self[0].len()
57    }
58
59    pub fn get_height(&self) -> usize {
60        self.len()
61    }
62
63    pub fn get_at_unbounded(&self, pos: Position) -> T {
64        self[pos.y as usize][pos.x as usize]
65    }
66
67    /// Bounded by the grid's dimensions
68    pub fn get_at(&self, pos: Position) -> Option<T> {
69        // if pos.x < 0 || pos.y < 0 || pos.x >= self.get_width() as i32 || pos.y >= self.get_height() as i32 {
70        if !self.in_bounds(pos) {
71            return None;
72        }
73
74        Some(self[pos.y as usize][pos.x as usize])
75        // Some(self.get_at_unbounded(pos))
76    }
77    /// ORTHOGONAL neighbors. use [Self::get_all_neighbors] for all 8
78    pub fn get_neighbors(&self, pos: Position) -> Vec<(Position, T)> {
79        let mut neighbors = Vec::with_capacity(4);
80
81        for delta in DIRECTIONS.iter() {
82            let new_pos = pos + *delta;
83
84            // Boundary check matching the working version
85            if new_pos.x >= 0 && new_pos.x < self.get_width() as i32 && 
86               new_pos.y >= 0 && new_pos.y < self.get_height() as i32 {
87                neighbors.push((new_pos, self.get_at_unbounded(new_pos)));
88            }
89        }
90
91        neighbors
92    }
93
94    pub fn get_all_neighbors(&self, pos: Position) -> Vec<(Position, T)> {
95        let mut neighbors = Vec::with_capacity(8);
96
97        for delta in ALL_DIRECTIONS.iter() {
98            let new_pos = pos + *delta;
99
100            // Boundary check matching the working version
101            if new_pos.x >= 0 && new_pos.x < self.get_width() as i32 && 
102               new_pos.y >= 0 && new_pos.y < self.get_height() as i32 {
103                neighbors.push((new_pos, self.get_at_unbounded(new_pos)));
104            }
105        }
106
107        neighbors
108    }
109
110    pub fn in_bounds(&self, pos: Position) -> bool {
111        pos.x >= 0 && pos.y >= 0 && pos.x < self.get_width() as i32 && pos.y < self.get_height() as i32
112    }
113
114    pub fn set_at(&mut self, pos: Position, value: T) -> Option<()> {
115        if !self.in_bounds(pos) {
116            return None;
117        }
118        self[pos.y as usize][pos.x as usize] = value;
119        Some(())
120    }
121
122    // Unbounded version if you need it
123    pub fn set_at_unbounded(&mut self, pos: Position, value: T) {
124        self[pos.y as usize][pos.x as usize] = value;
125    }
126
127    /// Walks the grid from top-left to bottom-right
128    pub fn walk<F: FnMut(Position) -> O, O>(&self, mut see: F) {
129        for row in 0..self.get_height() {
130            for col in 0..self.get_width() {
131                let pos = Position::new(col as i32, row as i32);
132
133                see(pos);
134            }
135        }
136    }
137
138    pub fn walk_region<F>(&mut self, start: Position, end: Position, mut see: F) 
139    where 
140        F: FnMut(&mut Self, Position)
141    {
142        for row in start.y..=end.y {
143            for col in start.x..=end.x {
144                let pos = Position::new(col, row);
145                see(self, pos);
146            }
147        }
148    }
149
150    // pub fn walk_region<F: FnMut(Position) -> O, O>(&self, start: Position, end: Position, mut see: F) {
151    //     for row in start.y..=end.y {
152    //         for col in start.x..=end.x {
153    //             let pos = Position::new(col, row);
154    //             see(pos);
155    //         }
156    //     }
157    // }
158
159    /// because Position is a type and not a NewType, we can't impl FromStr for it
160    pub fn position_from_str(s: &str) -> miette::Result<Position> {
161        let parts: Vec<i32> = s.split(',').map(|s| s.parse().unwrap()).collect();
162        Ok(Position::new(parts[0], parts[1]))
163    }
164}
165
166#[derive(Debug, Default, derive_more::Deref, derive_more::DerefMut)]
167pub struct Visited<T>(HashMap<Position, T>);
168
169impl<T> Visited<T> {
170    pub fn new(k: Position, v: T) -> Self {
171        let mut map = HashMap::new();
172        map.insert(k, v).expect("unique key");
173
174        Self(map)
175    }
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
179pub enum Direction {
180    /// A, North
181    Up,
182    /// X, South
183    Down,
184    /// #, West
185    Left,
186    /// O, East
187    Right
188}
189
190#[derive(Error, Diagnostic, Debug)]
191pub enum DirectionError {
192    #[error("Invalid direction character: {0}")]
193    #[diagnostic(code(direction::invalid_char))]
194    InvalidChar(char),
195
196    #[error("Invalid direction string: {0}")]
197    #[diagnostic(code(direction::invalid_str))]
198    InvalidStr(String),
199
200    #[error("Invalid direction mapping: Expected 4 unique directions")]
201    #[diagnostic(code(direction::invalid_mapping))]
202    InvalidMapping,
203
204    // #[error("Invalid symbol: {0}")]
205    // #[diagnostic(code(direction::invalid_symbol))]
206    // InvalidSymbol(T),
207}
208
209impl FromStr for Direction {
210    type Err = DirectionError;
211
212    fn from_str(s: &str) -> miette::Result<Self, Self::Err> {
213        match s.to_lowercase().as_str() {
214            "^" | "up" | "a" | "north" | "n" => Ok(Direction::Up),
215            "v" | "down" | "x" | "south" | "s" => Ok(Direction::Down),
216            // CARFEUL here, I thought PlayStation buttons (`#`) were cute...
217            "<" | "left" | "#" | "west" | "w" => Ok(Direction::Left),
218            ">" | "right" | "o" | "east" | "e" => Ok(Direction::Right),
219            _ => Err(DirectionError::InvalidStr(s.to_string()))
220        }
221    }
222}
223
224impl Direction {
225    /// Creates a parser function that maps custom direction symbols to Direction variants
226    /// Order is [Up, Down, Left, Right]
227    pub fn with_mapping4<T>(mapping: [T; 4]) -> impl Fn(&T) -> Result<Direction, DirectionError> 
228    where 
229        T: std::fmt::Display + Eq + Clone + std::hash::Hash,
230    {
231        // // Validate no duplicates using HashSet
232        // let unique: HashSet<_> = mapping.iter().collect();
233        // if unique.len() != 4 {
234        //     return Err(DirectionError::InvalidMapping);
235        // }
236
237        move |s| {
238            if *s == mapping[0] { Ok(Direction::Up) }
239            else if *s == mapping[1] { Ok(Direction::Down) }
240            else if *s == mapping[2] { Ok(Direction::Left) }
241            else if *s == mapping[3] { Ok(Direction::Right) }
242            else { Err(DirectionError::InvalidStr(s.to_string())) }
243        }
244    }
245
246    /// for parsing from a CHAR, otherwise use [FromStr] because we get [String.parse] for free
247    pub fn parse(c: char) -> miette::Result<Self, DirectionError> {
248        match c.to_ascii_lowercase() {
249            '^' | 'a' | 'n' => Ok(Direction::Up),
250            'v' | 'x' | 's' => Ok(Direction::Down),
251            // CARFEUL here, I thought PlayStation buttons (`#`) were cute...
252            '<' | '#' | 'e' => Ok(Direction::Left),
253            // also here: `o`
254            '>' | 'o' | 'w' => Ok(Direction::Right),
255            _ => Err(DirectionError::InvalidChar(c))
256        }
257    }
258
259    fn opposite(&self) -> Direction {
260        match self {
261            Direction::Up => Direction::Down,
262            Direction::Down => Direction::Up,
263            Direction::Left => Direction::Right,
264            Direction::Right => Direction::Left
265        }
266    }
267
268    pub fn to_offset(&self) -> Position {
269        match self {
270            Direction::Up => Position::NEG_Y,
271            Direction::Down => Position::Y,
272            Direction::Left => Position::NEG_X,
273            Direction::Right => Position::X
274        }
275    }
276
277    pub fn turn_right(&self) -> Direction {
278        match self {
279            Direction::Up => Direction::Right,
280            Direction::Right => Direction::Down,
281            Direction::Down => Direction::Left,
282            Direction::Left => Direction::Up,
283        }
284    }
285
286    pub fn turn_left(&self) -> Direction {
287        self.turn_right().opposite()
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294
295    
296
297}