1use std::{error::Error, fmt};
11
12use crate::point::Point;
13
14pub mod reachability;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum Cell {
20 Open,
22 Blocked,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28#[non_exhaustive]
29pub enum GridStorage {
30 Cells,
32 TraversalCosts,
34}
35
36impl fmt::Display for GridStorage {
37 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
38 match self {
39 Self::Cells => formatter.write_str("cells"),
40 Self::TraversalCosts => formatter.write_str("traversal costs"),
41 }
42 }
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47#[non_exhaustive]
48pub enum GridBuildError {
49 ZeroWidth,
51 ZeroHeight,
53 CellCountOverflow { width: usize, height: usize },
55 CapacityExceeded { storage: GridStorage },
57 EmptyRows,
59 ZeroWidthRow,
61 RaggedRow {
63 row: usize,
64 expected_width: usize,
65 actual_width: usize,
66 },
67 InvalidRowCharacter {
69 row: usize,
70 column: usize,
71 character: char,
72 },
73 Edit(GridEditError),
75}
76
77impl fmt::Display for GridBuildError {
78 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
79 match self {
80 Self::ZeroWidth => formatter.write_str("grid width must be greater than zero"),
81 Self::ZeroHeight => formatter.write_str("grid height must be greater than zero"),
82 Self::CellCountOverflow { width, height } => {
83 write!(formatter, "grid dimensions {width}x{height} overflow usize")
84 }
85 Self::CapacityExceeded { storage } => {
86 write!(formatter, "grid {storage} exceed available capacity")
87 }
88 Self::EmptyRows => formatter.write_str("grid rows must not be empty"),
89 Self::ZeroWidthRow => formatter.write_str("grid rows must not be zero-width"),
90 Self::RaggedRow {
91 row,
92 expected_width,
93 actual_width,
94 } => write!(
95 formatter,
96 "grid row {row} has width {actual_width}, expected {expected_width}"
97 ),
98 Self::InvalidRowCharacter {
99 row,
100 column,
101 character,
102 } => write!(
103 formatter,
104 "grid row {row}, column {column} contains unsupported character {character:?}"
105 ),
106 Self::Edit(error) => write!(formatter, "grid edit failed: {error}"),
107 }
108 }
109}
110
111impl Error for GridBuildError {
112 fn source(&self) -> Option<&(dyn Error + 'static)> {
113 match self {
114 Self::Edit(error) => Some(error),
115 _ => None,
116 }
117 }
118}
119
120impl From<GridEditError> for GridBuildError {
121 fn from(error: GridEditError) -> Self {
122 Self::Edit(error)
123 }
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128#[non_exhaustive]
129pub enum GridEditError {
130 OutOfBounds {
132 point: Point,
133 width: usize,
134 height: usize,
135 },
136 BlockedCell { point: Point },
138 ZeroTraversalCost { point: Point },
140}
141
142impl fmt::Display for GridEditError {
143 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
144 match self {
145 Self::OutOfBounds {
146 point,
147 width,
148 height,
149 } => write!(
150 formatter,
151 "point ({}, {}) is outside grid bounds {width}x{height}",
152 point.x, point.y
153 ),
154 Self::BlockedCell { point } => write!(
155 formatter,
156 "cannot set traversal cost for blocked cell ({}, {})",
157 point.x, point.y
158 ),
159 Self::ZeroTraversalCost { point } => write!(
160 formatter,
161 "traversal cost at ({}, {}) must be greater than zero",
162 point.x, point.y
163 ),
164 }
165 }
166}
167
168impl Error for GridEditError {}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171enum GridBuilderEdit {
172 Cell { point: Point, cell: Cell },
173 Cost { point: Point, cost: usize },
174}
175
176#[derive(Debug, Clone, PartialEq, Eq)]
178pub struct GridBuilder {
179 width: usize,
180 height: usize,
181 edits: Vec<GridBuilderEdit>,
182 index_reachability: bool,
183}
184
185impl GridBuilder {
186 #[must_use]
191 pub const fn new(width: usize, height: usize) -> Self {
192 Self {
193 width,
194 height,
195 edits: Vec::new(),
196 index_reachability: false,
197 }
198 }
199
200 #[must_use]
202 pub fn blocked<I, P>(mut self, points: I) -> Self
203 where
204 I: IntoIterator<Item = P>,
205 P: Into<Point>,
206 {
207 self.edits
208 .extend(points.into_iter().map(|point| GridBuilderEdit::Cell {
209 point: point.into(),
210 cell: Cell::Blocked,
211 }));
212 self
213 }
214
215 #[must_use]
217 pub fn open<I, P>(mut self, points: I) -> Self
218 where
219 I: IntoIterator<Item = P>,
220 P: Into<Point>,
221 {
222 self.edits
223 .extend(points.into_iter().map(|point| GridBuilderEdit::Cell {
224 point: point.into(),
225 cell: Cell::Open,
226 }));
227 self
228 }
229
230 #[must_use]
232 pub fn costs<I, P>(mut self, costs: I) -> Self
233 where
234 I: IntoIterator<Item = (P, usize)>,
235 P: Into<Point>,
236 {
237 self.edits.extend(
238 costs
239 .into_iter()
240 .map(|(point, cost)| GridBuilderEdit::Cost {
241 point: point.into(),
242 cost,
243 }),
244 );
245 self
246 }
247
248 #[must_use]
250 pub const fn index_reachability(mut self) -> Self {
251 self.index_reachability = true;
252 self
253 }
254
255 pub fn build(self) -> Result<Grid, GridBuildError> {
262 let mut grid = Grid::new(self.width, self.height)?;
263 for edit in self.edits {
264 match edit {
265 GridBuilderEdit::Cell { point, cell } => grid.set_cell(point, cell)?,
266 GridBuilderEdit::Cost { point, cost } => {
267 grid.set_traversal_cost(point, cost)?;
268 }
269 }
270 }
271 if self.index_reachability {
272 grid.index_reachability();
273 }
274 Ok(grid)
275 }
276}
277
278#[derive(Debug, Clone, PartialEq, Eq)]
284pub struct Grid {
285 width: usize,
286 height: usize,
287 cells: Vec<Cell>,
288 traversal_costs: Vec<usize>,
289 reachability: Option<reachability::GridReachabilityIndex>,
290}
291
292impl Grid {
293 pub fn new(width: usize, height: usize) -> Result<Self, GridBuildError> {
300 if width == 0 {
301 return Err(GridBuildError::ZeroWidth);
302 }
303 if height == 0 {
304 return Err(GridBuildError::ZeroHeight);
305 }
306 let Some(cell_count) = width.checked_mul(height) else {
307 return Err(GridBuildError::CellCountOverflow { width, height });
308 };
309
310 let mut cells = Vec::new();
311 cells
312 .try_reserve_exact(cell_count)
313 .map_err(|_| GridBuildError::CapacityExceeded {
314 storage: GridStorage::Cells,
315 })?;
316 cells.resize(cell_count, Cell::Open);
317
318 let mut traversal_costs = Vec::new();
319 traversal_costs.try_reserve_exact(cell_count).map_err(|_| {
320 GridBuildError::CapacityExceeded {
321 storage: GridStorage::TraversalCosts,
322 }
323 })?;
324 traversal_costs.resize(cell_count, 1);
325
326 Ok(Self {
327 width,
328 height,
329 cells,
330 traversal_costs,
331 reachability: None,
332 })
333 }
334
335 #[must_use]
337 pub const fn builder(width: usize, height: usize) -> GridBuilder {
338 GridBuilder::new(width, height)
339 }
340
341 pub fn try_from_rows<I, S>(rows: I) -> Result<Self, GridBuildError>
351 where
352 I: IntoIterator<Item = S>,
353 S: AsRef<str>,
354 {
355 let rows: Vec<S> = rows.into_iter().collect();
356 let Some(first) = rows.first() else {
357 return Err(GridBuildError::EmptyRows);
358 };
359 let width = first.as_ref().chars().count();
360 if width == 0 {
361 return Err(GridBuildError::ZeroWidthRow);
362 }
363
364 let mut grid = Self::new(width, rows.len())?;
365 for (row, source) in rows.iter().enumerate() {
366 let actual_width = source.as_ref().chars().count();
367 if actual_width != width {
368 return Err(GridBuildError::RaggedRow {
369 row,
370 expected_width: width,
371 actual_width,
372 });
373 }
374 for (column, character) in source.as_ref().chars().enumerate() {
375 let point = Point::new(column, row);
376 match character {
377 '.' | '1' => {}
378 '#' => grid.set_cell(point, Cell::Blocked)?,
379 '2'..='9' => {
380 let cost = character.to_digit(10).expect("matched an ASCII digit") as usize;
381 grid.set_traversal_cost(point, cost)?;
382 }
383 character => {
384 return Err(GridBuildError::InvalidRowCharacter {
385 row,
386 column,
387 character,
388 });
389 }
390 }
391 }
392 }
393 Ok(grid)
394 }
395
396 pub fn index_reachability(&mut self) {
398 self.reachability = Some(reachability::GridReachabilityIndex::from_grid(self));
399 }
400
401 #[must_use]
406 pub fn is_reachable(&self, start: Point, goal: Point) -> bool {
407 if let Some(reachability) = &self.reachability {
408 reachability.is_reachable(start, goal)
409 } else {
410 true
411 }
412 }
413
414 #[must_use]
416 pub const fn width(&self) -> usize {
417 self.width
418 }
419
420 #[must_use]
422 pub const fn height(&self) -> usize {
423 self.height
424 }
425
426 #[must_use]
428 pub fn cell_count(&self) -> usize {
429 self.cells.len()
430 }
431
432 #[must_use]
434 pub fn contains(&self, point: Point) -> bool {
435 self.index_of(point).is_some()
436 }
437
438 #[must_use]
440 pub fn cell(&self, point: Point) -> Option<Cell> {
441 self.index_of(point).map(|index| self.cells[index])
442 }
443
444 pub fn set_cell(&mut self, point: Point, cell: Cell) -> Result<(), GridEditError> {
453 let Some(index) = self.index_of(point) else {
454 return Err(GridEditError::OutOfBounds {
455 point,
456 width: self.width,
457 height: self.height,
458 });
459 };
460
461 if self.cells[index] != cell {
462 self.cells[index] = cell;
463 self.reachability = None;
464 }
465 Ok(())
466 }
467
468 #[must_use]
470 pub fn is_walkable(&self, point: Point) -> bool {
471 matches!(self.cell(point), Some(Cell::Open))
472 }
473
474 #[must_use]
476 pub fn path_is_walkable(&self, points: &[Point]) -> bool {
477 points.iter().all(|&p| self.is_walkable(p))
478 && points
479 .windows(2)
480 .all(|pair| self.segment_is_walkable(pair[0], pair[1]))
481 }
482
483 #[must_use]
485 pub fn segment_is_walkable(&self, start: Point, end: Point) -> bool {
486 if start == end {
487 return self.is_walkable(start);
488 }
489
490 let dx = (start.x as i64 - end.x as i64).abs();
491 let dy = (start.y as i64 - end.y as i64).abs();
492
493 if (dx == 1 && dy == 0) || (dx == 0 && dy == 1) {
494 self.is_walkable(start) && self.is_walkable(end)
495 } else {
496 false
497 }
498 }
499
500 #[must_use]
502 pub fn traversal_cost(&self, point: Point) -> Option<usize> {
503 let index = self.index_of(point)?;
504 self.is_walkable(point)
505 .then_some(self.traversal_costs[index])
506 }
507
508 pub fn set_traversal_cost(&mut self, point: Point, cost: usize) -> Result<(), GridEditError> {
515 if cost == 0 {
516 return Err(GridEditError::ZeroTraversalCost { point });
517 }
518
519 let Some(index) = self.index_of(point) else {
520 return Err(GridEditError::OutOfBounds {
521 point,
522 width: self.width,
523 height: self.height,
524 });
525 };
526 if !matches!(self.cells[index], Cell::Open) {
527 return Err(GridEditError::BlockedCell { point });
528 }
529
530 self.traversal_costs[index] = cost;
531 Ok(())
532 }
533
534 #[must_use]
536 pub fn neighbors4(&self, point: Point) -> Vec<Point> {
537 let mut neighbors = Vec::with_capacity(4);
538
539 if point.x > 0 {
540 let candidate = Point::new(point.x - 1, point.y);
541 if self.is_walkable(candidate) {
542 neighbors.push(candidate);
543 }
544 }
545
546 if point.x + 1 < self.width {
547 let candidate = Point::new(point.x + 1, point.y);
548 if self.is_walkable(candidate) {
549 neighbors.push(candidate);
550 }
551 }
552
553 if point.y > 0 {
554 let candidate = Point::new(point.x, point.y - 1);
555 if self.is_walkable(candidate) {
556 neighbors.push(candidate);
557 }
558 }
559
560 if point.y + 1 < self.height {
561 let candidate = Point::new(point.x, point.y + 1);
562 if self.is_walkable(candidate) {
563 neighbors.push(candidate);
564 }
565 }
566
567 neighbors
568 }
569
570 #[must_use]
572 #[doc(hidden)]
573 pub fn index_of(&self, point: Point) -> Option<usize> {
574 if point.x >= self.width || point.y >= self.height {
575 return None;
576 }
577
578 Some((point.y * self.width) + point.x)
579 }
580
581 #[must_use]
583 #[doc(hidden)]
584 pub fn point_from_index(&self, index: usize) -> Point {
585 let x = index % self.width;
586 let y = index / self.width;
587 Point::new(x, y)
588 }
589}
590
591#[macro_export]
593macro_rules! grid {
594 () => {
595 $crate::Grid::try_from_rows(::core::iter::empty::<&str>())
596 };
597 ($($row:expr),+ $(,)?) => {
598 $crate::Grid::try_from_rows([$($row),+])
599 };
600}
601
602#[cfg(test)]
603mod tests {
604 use super::{Cell, Grid, GridBuildError, GridEditError, Point};
605
606 #[test]
607 fn blocking_and_reopening_preserves_custom_traversal_cost() {
608 let mut grid = Grid::new(3, 1).expect("grid dimensions are valid");
609 let point = Point::new(1, 0);
610
611 assert_eq!(grid.set_traversal_cost(point, 5), Ok(()));
612 assert_eq!(grid.traversal_cost(point), Some(5));
613
614 assert_eq!(grid.set_cell(point, Cell::Blocked), Ok(()));
615 assert_eq!(grid.traversal_cost(point), None);
616
617 assert_eq!(grid.set_cell(point, Cell::Open), Ok(()));
618 assert_eq!(grid.traversal_cost(point), Some(5));
619 }
620
621 #[test]
622 fn builder_applies_bulk_edits_and_indexes_reachability() {
623 let grid = Grid::builder(3, 2)
624 .blocked([(1, 0), (1, 1), (0, 1)])
625 .open([(0, 1)])
626 .costs([((2, 1), 4)])
627 .index_reachability()
628 .build()
629 .expect("builder input is valid");
630
631 assert_eq!(grid.cell(Point::new(1, 0)), Some(Cell::Blocked));
632 assert_eq!(grid.cell(Point::new(0, 1)), Some(Cell::Open));
633 assert_eq!(grid.cell(Point::new(1, 1)), Some(Cell::Blocked));
634 assert_eq!(grid.traversal_cost(Point::new(2, 1)), Some(4));
635 assert!(!grid.is_reachable(Point::new(0, 0), Point::new(2, 0)));
636 }
637
638 #[test]
639 fn builder_reports_invalid_edits_with_context() {
640 let error = Grid::builder(2, 2)
641 .blocked([(2, 0)])
642 .build()
643 .expect_err("point is out of bounds");
644
645 assert_eq!(
646 error,
647 GridBuildError::Edit(GridEditError::OutOfBounds {
648 point: Point::new(2, 0),
649 width: 2,
650 height: 2,
651 })
652 );
653 }
654
655 #[test]
656 fn rows_parse_cells_and_costs() {
657 let grid = Grid::try_from_rows([".#1", ".29"]).expect("rows are valid");
658
659 assert_eq!(grid.cell(Point::new(1, 0)), Some(Cell::Blocked));
660 assert_eq!(grid.traversal_cost(Point::new(2, 0)), Some(1));
661 assert_eq!(grid.traversal_cost(Point::new(1, 1)), Some(2));
662 assert_eq!(grid.traversal_cost(Point::new(2, 1)), Some(9));
663 }
664
665 #[test]
666 fn rows_report_shape_and_character_errors() {
667 assert_eq!(
668 Grid::try_from_rows(Vec::<String>::new()),
669 Err(GridBuildError::EmptyRows)
670 );
671 assert_eq!(Grid::try_from_rows([""]), Err(GridBuildError::ZeroWidthRow));
672 assert_eq!(
673 Grid::try_from_rows(["..", "."]),
674 Err(GridBuildError::RaggedRow {
675 row: 1,
676 expected_width: 2,
677 actual_width: 1,
678 })
679 );
680 assert_eq!(
681 Grid::try_from_rows([".x"]),
682 Err(GridBuildError::InvalidRowCharacter {
683 row: 0,
684 column: 1,
685 character: 'x',
686 })
687 );
688 }
689
690 #[test]
691 fn macro_delegates_to_row_parser() {
692 let from_macro = crate::grid![".#", ".3"].expect("rows are valid");
693 let from_parser = Grid::try_from_rows([".#", ".3"]).expect("rows are valid");
694 assert_eq!(from_macro, from_parser);
695 assert_eq!(crate::grid!(), Err(GridBuildError::EmptyRows));
696 }
697
698 #[test]
699 fn builder_accepts_tuple_coordinates_and_builds_index() {
700 let grid = Grid::builder(3, 2)
701 .blocked([(1, 0), (1, 1), (0, 1)])
702 .open([(0, 1)])
703 .costs([((2, 1), 7)])
704 .index_reachability()
705 .build()
706 .expect("builder input is valid");
707
708 assert_eq!(grid.cell(Point::new(1, 0)), Some(Cell::Blocked));
709 assert_eq!(grid.traversal_cost(Point::new(2, 1)), Some(7));
710 assert!(!grid.is_reachable(Point::new(0, 0), Point::new(2, 0)));
711 }
712
713 #[test]
714 fn setters_report_why_an_edit_failed() {
715 let mut grid = Grid::new(2, 2).expect("dimensions are valid");
716
717 assert_eq!(
718 grid.set_cell(Point::new(2, 0), Cell::Blocked),
719 Err(GridEditError::OutOfBounds {
720 point: Point::new(2, 0),
721 width: 2,
722 height: 2,
723 })
724 );
725 grid.set_cell(Point::new(1, 1), Cell::Blocked)
726 .expect("point is in bounds");
727 assert_eq!(
728 grid.set_traversal_cost(Point::new(1, 1), 4),
729 Err(GridEditError::BlockedCell {
730 point: Point::new(1, 1),
731 })
732 );
733 assert_eq!(
734 grid.set_traversal_cost(Point::new(0, 0), 0),
735 Err(GridEditError::ZeroTraversalCost {
736 point: Point::new(0, 0),
737 })
738 );
739 }
740
741 #[test]
742 fn row_parser_and_macro_share_the_same_result() {
743 let parsed = Grid::try_from_rows([".#.", ".39"]).expect("rows are valid");
744 let expanded = crate::grid![".#.", ".39"].expect("rows are valid");
745
746 assert_eq!(expanded, parsed);
747 assert_eq!(parsed.cell(Point::new(1, 0)), Some(Cell::Blocked));
748 assert_eq!(parsed.traversal_cost(Point::new(1, 1)), Some(3));
749 assert_eq!(parsed.traversal_cost(Point::new(2, 1)), Some(9));
750 }
751
752 #[test]
753 fn row_parser_reports_precise_shape_and_character_errors() {
754 assert_eq!(
755 Grid::try_from_rows(Vec::<String>::new()),
756 Err(GridBuildError::EmptyRows)
757 );
758 assert_eq!(Grid::try_from_rows([""]), Err(GridBuildError::ZeroWidthRow));
759 assert_eq!(
760 Grid::try_from_rows(["..", "."]),
761 Err(GridBuildError::RaggedRow {
762 row: 1,
763 expected_width: 2,
764 actual_width: 1,
765 })
766 );
767 assert_eq!(
768 Grid::try_from_rows([".x"]),
769 Err(GridBuildError::InvalidRowCharacter {
770 row: 0,
771 column: 1,
772 character: 'x',
773 })
774 );
775 }
776}