1use std::collections::{HashMap, VecDeque};
2
3use crate::{
4 BoardArea, BoardSyntaxError, BoardSyntaxErrorKind, BoxID, Boxbox, Direction, Goal, GoalID,
5 Point2D, Pusher, PusherID, Size2D, SokobanPlus, Tessellation, TileKind, TileShape,
6};
7
8pub struct StaticBoardAnalysis {
29 tessellation: Tessellation,
30 size: Size2D,
31
32 walls_positions: BoardArea,
33 pushers_positions: BoardArea,
34 boxes_positions: BoardArea,
35 goals_positions: BoardArea,
36
37 boxes: Box<[Boxbox]>,
38 goals: Box<[Goal]>,
39 pushers: Box<[Pusher]>,
40}
41
42impl StaticBoardAnalysis {
43 pub fn new<I>(
48 board: I, size: Size2D, tessellation: Tessellation, sokoban_plus: Option<&SokobanPlus>,
49 ) -> Result<Self, BoardSyntaxError>
50 where
51 I: Iterator<Item = TileKind>,
52 {
53 let expected_len = size.size();
54
55 let mut walls_pos = BoardArea::new(size);
56 let mut pushers_pos = BoardArea::new(size);
57 let mut boxes_pos = BoardArea::new(size);
58 let mut goals_pos = BoardArea::new(size);
59
60 let mut boxes: Vec<Boxbox> = Vec::default();
61 let mut pushers: Vec<Pusher> = Vec::default();
62 let mut goals: Vec<Goal> = Vec::default();
63
64 for (idx, tile) in board.enumerate() {
65 if idx >= expected_len {
66 return Err(BoardSyntaxError::new(
67 BoardSyntaxErrorKind::InputSizeNotMatchedByDeclaredSize {
68 input: idx.checked_add(1).unwrap_or(idx),
69 declared_width: size.width,
70 declared_height: size.height,
71 },
72 ));
73 }
74
75 let pos = Point2D::new_from_idx(idx, &size);
76
77 if tile.is_wall() {
78 walls_pos.add_point(pos);
79 }
80
81 if tile.has_pusher() {
82 let p = Self::build_pusher(pushers.len(), pos.x, pos.y)?;
83 pushers_pos.add_point(pos);
84 pushers.push(p);
85 }
86
87 if tile.has_box() {
88 let b = Self::build_box(boxes.len(), pos.x, pos.y, sokoban_plus)?;
89 boxes_pos.add_point(pos);
90 boxes.push(b);
91 }
92
93 if tile.has_goal() {
94 let g = Self::build_goal(goals.len(), pos.x, pos.y, sokoban_plus)?;
95 goals_pos.add_point(pos);
96 goals.push(g);
97 }
98 }
99
100 Ok(Self {
101 tessellation,
102 walls_positions: walls_pos,
103 pushers_positions: pushers_pos,
104 boxes_positions: boxes_pos,
105 goals_positions: goals_pos,
106 size,
107 boxes: boxes.into_boxed_slice(),
108 goals: goals.into_boxed_slice(),
109 pushers: pushers.into_boxed_slice(),
110 })
111 }
112
113 pub fn flood_fill<F>(
121 root: Point2D, is_obstacle: F, size: Size2D, tessellation: Tessellation,
122 ) -> Option<BoardArea>
123 where
124 F: Fn(Point2D) -> bool,
125 {
126 let mut retv = BoardArea::new(size);
127 let mut visited = BoardArea::new(size);
128 let mut to_inspect: VecDeque<Point2D> = VecDeque::new();
129 to_inspect.push_back(root);
130
131 while !to_inspect.is_empty() {
132 let current_position = to_inspect.pop_front()?;
133 retv.add_point(current_position);
134
135 for (_, neighbor) in current_position.neighbors(tessellation, size)? {
136 if !visited.contains(neighbor) {
137 if !is_obstacle(neighbor) {
138 to_inspect.push_back(neighbor);
139 }
140 visited.add_point(neighbor);
141 }
142 }
143 }
144
145 Some(retv)
146 }
147
148 pub fn size(&self) -> Size2D { self.size }
150
151 pub fn tessellation(&self) -> Tessellation { self.tessellation }
153
154 pub fn walls_positions(&self) -> &BoardArea { &self.walls_positions }
156
157 pub fn boxes_positions(&self) -> &BoardArea { &self.boxes_positions }
159
160 pub fn goals_positions(&self) -> &BoardArea { &self.goals_positions }
162
163 pub fn pushers_positions(&self) -> &BoardArea { &self.pushers_positions }
165
166 pub fn boxes(&self) -> &[Boxbox] { &self.boxes }
168
169 pub fn goals(&self) -> &[Goal] { &self.goals }
171
172 pub fn pushers(&self) -> &[Pusher] { &self.pushers }
174
175 pub fn outer_area(&self) -> BoardArea {
177 let play_area = self.playable_area();
178 let significant_walls = self.significant_walls();
179
180 let is_candidate =
181 |pos: Point2D| -> bool { !significant_walls.contains(pos) && !play_area.contains(pos) };
182
183 let mut candidates: Vec<Point2D> = Vec::with_capacity(
184 self.size
185 .width
186 .saturating_mul(2)
187 .saturating_add(self.size.height.saturating_mul(2)),
188 );
189 for x in 0..self.size.width {
190 let pos = Point2D::new(x, 0);
191 if is_candidate(pos) {
192 candidates.push(pos);
193 }
194 let pos = Point2D::new(x, self.size.height - 1);
195 if is_candidate(pos) {
196 candidates.push(pos);
197 }
198 }
199 for y in 0..self.size.height {
200 let pos = Point2D::new(0, y);
201 if is_candidate(pos) {
202 candidates.push(pos);
203 }
204 let pos = Point2D::new(self.size.width - 1, y);
205 if is_candidate(pos) {
206 candidates.push(pos);
207 }
208 }
209
210 let is_border = |pos: Point2D| -> bool { significant_walls.contains(pos) };
211
212 let mut retv = BoardArea::new(self.size);
213
214 for candidate in candidates {
215 if !retv.contains(candidate)
216 && let Some(neighbor_outers) =
217 Self::flood_fill(candidate, is_border, self.size, self.tessellation)
218 {
219 neighbor_outers.into_iter().for_each(|pos| {
220 retv.add_point(pos);
221 });
222 }
223 }
224
225 retv
226 }
227
228 pub fn playable_area(&self) -> BoardArea {
245 let is_obstacle = |pos: Point2D| -> bool { self.walls_positions.contains(pos) };
246
247 let mut retv = BoardArea::new(self.size);
248
249 let pushers = self.pushers_positions.clone();
250
251 for pos in pushers {
252 if let Some(rp) = Self::flood_fill(pos, is_obstacle, self.size, self.tessellation) {
253 for pos in rp {
254 retv.add_point(pos);
255 }
256 }
257 }
258
259 retv
260 }
261
262 pub fn is_playable_area_confined(&self) -> bool {
274 let Some((top_left, bottom_right)) = self.playable_area().bounds() else {
275 return true;
276 };
277
278 top_left.x > 0
279 && top_left.y > 0
280 && bottom_right.x.saturating_add(1) < self.size.width
281 && bottom_right.y.saturating_add(1) < self.size.height
282 }
283
284 pub fn flood_path<F>(
295 from: Point2D, to: Point2D, is_obstacle: F, size: Size2D, tessellation: Tessellation,
296 ) -> Option<Vec<Direction>>
297 where
298 F: Fn(Point2D) -> bool,
299 {
300 if !from.is_on_board(size) || !to.is_on_board(size) {
301 return None;
302 }
303
304 if from == to {
305 return Some(Vec::new());
306 }
307
308 let mut visited = BoardArea::new(size);
312 visited.add_point(from);
313
314 let mut came_from: HashMap<Point2D, (Direction, Point2D)> = HashMap::new();
315 let mut to_inspect: VecDeque<Point2D> = VecDeque::new();
316 to_inspect.push_back(from);
317
318 while let Some(current_position) = to_inspect.pop_front() {
319 for (direction, neighbor) in current_position.neighbors(tessellation, size)? {
320 if visited.contains(neighbor) || is_obstacle(neighbor) {
321 continue;
322 }
323
324 visited.add_point(neighbor);
325 came_from.insert(neighbor, (direction, current_position));
326
327 if neighbor == to {
328 let mut path = Vec::new();
329 let mut position = to;
330
331 while position != from {
332 let (direction, previous) = came_from.get(&position)?;
333 path.push(*direction);
334 position = *previous;
335 }
336
337 path.reverse();
338 return Some(path);
339 }
340
341 to_inspect.push_back(neighbor);
342 }
343 }
344
345 None
346 }
347
348 pub fn corners(&self) -> BoardArea {
355 let mut retv = BoardArea::new(self.size);
356
357 for pos in self.playable_area() {
358 if !self.walls_positions.contains(pos) && self.is_corner(pos) {
359 retv.add_point(pos);
360 }
361 }
362
363 retv
364 }
365
366 pub fn build_box(
369 maybe_id: usize, x: usize, y: usize, sokoban_plus: Option<&SokobanPlus>,
370 ) -> Result<Boxbox, BoardSyntaxError> {
371 let box_id = BoxID::try_from(maybe_id).map_err(|e| {
372 BoardSyntaxError::new_with_source(BoardSyntaxErrorKind::TooManyBoxes, Some(e))
373 })?;
374
375 Ok(Boxbox {
376 id: box_id,
377 plus_id: sokoban_plus.and_then(|sp| sp.get_box_plus_id(box_id)),
378 x,
379 y,
380 })
381 }
382
383 pub fn build_goal(
386 maybe_id: usize, x: usize, y: usize, sokoban_plus: Option<&SokobanPlus>,
387 ) -> Result<Goal, BoardSyntaxError> {
388 let goal_id = GoalID::try_from(maybe_id).map_err(|e| {
389 BoardSyntaxError::new_with_source(BoardSyntaxErrorKind::TooManyGoals, Some(e))
390 })?;
391
392 Ok(Goal {
393 id: goal_id,
394 plus_id: sokoban_plus.and_then(|sp| sp.get_goal_plus_id(goal_id)),
395 x,
396 y,
397 })
398 }
399
400 pub fn build_pusher(maybe_id: usize, x: usize, y: usize) -> Result<Pusher, BoardSyntaxError> {
403 let pusher_id = PusherID::try_from(maybe_id).map_err(|e| {
404 BoardSyntaxError::new_with_source(BoardSyntaxErrorKind::TooManyPushers, Some(e))
405 })?;
406 Ok(Pusher {
407 id: pusher_id,
408 x,
409 y,
410 })
411 }
412
413 fn is_corner(&self, at: Point2D) -> bool {
416 let is_wall =
417 |p: Option<Point2D>| -> bool { p.is_some_and(|pp| self.walls_positions.contains(pp)) };
418
419 match self.tessellation {
420 Tessellation::Sokoban => {
421 let l = at.neighbor(self.tessellation, Direction::Left, self.size);
422 let u = at.neighbor(self.tessellation, Direction::Up, self.size);
423 let r = at.neighbor(self.tessellation, Direction::Right, self.size);
424 let d = at.neighbor(self.tessellation, Direction::Down, self.size);
425
426 (is_wall(l) && is_wall(u))
427 || (is_wall(u) && is_wall(r))
428 || (is_wall(r) && is_wall(d))
429 || (is_wall(d) && is_wall(l))
430 }
431
432 Tessellation::Hexoban => {
433 let l = at.neighbor(self.tessellation, Direction::Left, self.size);
434 let r = at.neighbor(self.tessellation, Direction::Right, self.size);
435 let nw = at.neighbor(self.tessellation, Direction::NorthWest, self.size);
436 let ne = at.neighbor(self.tessellation, Direction::NorthEast, self.size);
437 let sw = at.neighbor(self.tessellation, Direction::SouthWest, self.size);
438 let se = at.neighbor(self.tessellation, Direction::SouthEast, self.size);
439
440 (is_wall(l) && is_wall(nw) && is_wall(ne))
441 || (is_wall(nw) && is_wall(ne) && is_wall(r))
442 || (is_wall(ne) && is_wall(r) && is_wall(se))
443 || (is_wall(r) && is_wall(se) && is_wall(sw))
444 || (is_wall(se) && is_wall(sw) && is_wall(l))
445 || (is_wall(sw) && is_wall(l) && is_wall(nw))
446 }
447
448 Tessellation::Trioban => {
449 at.neighbors(self.tessellation, self.size)
450 .map_or(0, |iter_neighbors| {
451 iter_neighbors.filter(|(_d, p)| is_wall(Some(*p))).count()
452 })
453 >= 4
454 }
455
456 Tessellation::Octoban => {
457 if self.tessellation.tile_shape_from_pos(at.x, at.y) == TileShape::Octagon {
458 let l = at.neighbor(self.tessellation, Direction::Left, self.size);
459 let u = at.neighbor(self.tessellation, Direction::Up, self.size);
460 let r = at.neighbor(self.tessellation, Direction::Right, self.size);
461 let d = at.neighbor(self.tessellation, Direction::Down, self.size);
462 let nw = at.neighbor(self.tessellation, Direction::NorthWest, self.size);
463 let ne = at.neighbor(self.tessellation, Direction::NorthEast, self.size);
464 let sw = at.neighbor(self.tessellation, Direction::SouthWest, self.size);
465 let se = at.neighbor(self.tessellation, Direction::SouthEast, self.size);
466
467 (is_wall(l) && is_wall(nw) && is_wall(u))
468 || (is_wall(u) && is_wall(ne) && is_wall(r))
469 || (is_wall(r) && is_wall(se) && is_wall(d))
470 || (is_wall(d) && is_wall(sw) && is_wall(l))
471 } else {
472 let l = at.neighbor(self.tessellation, Direction::Left, self.size);
474 let u = at.neighbor(self.tessellation, Direction::Up, self.size);
475 let r = at.neighbor(self.tessellation, Direction::Right, self.size);
476 let d = at.neighbor(self.tessellation, Direction::Down, self.size);
477
478 (is_wall(l) && is_wall(u))
479 || (is_wall(u) && is_wall(r))
480 || (is_wall(r) && is_wall(d))
481 || (is_wall(d) && is_wall(l))
482 }
483 }
484 }
485 }
486
487 pub fn significant_walls(&self) -> BoardArea {
496 let mut playable_area = self.playable_area();
497
498 let mut non_area = BoardArea::new(self.size);
499 for y in 0..self.size.height {
500 for x in 0..self.size.width {
501 let pos = Point2D::new(x, y);
502 if !playable_area.contains(pos) {
503 non_area.add_point(pos);
504 }
505 }
506 }
507
508 let mut retv = BoardArea::new(self.size);
510 let mut remainder = BoardArea::new(self.size);
511 for pos in non_area {
512 if pos
513 .neighbors(self.tessellation, self.size)
514 .is_some_and(|mut iter_neigh| iter_neigh.any(|(_d, p)| playable_area.contains(p)))
515 {
516 retv.add_point(pos);
517 } else {
518 remainder.add_point(pos);
519 }
520 }
521
522 for pos in &retv {
523 playable_area.add_point(pos);
524 }
525
526 if self.tessellation == Tessellation::Sokoban {
527 for pos in self.additional_sig_walls_squares(&playable_area, &remainder) {
528 retv.add_point(pos);
529 }
530 }
531 if self.tessellation == Tessellation::Trioban {
532 for pos in self.additional_sig_walls_triangles(&playable_area, &remainder) {
533 retv.add_point(pos);
534 }
535 }
536
537 retv
538 }
539
540 fn additional_sig_walls_triangles(
542 &self, found_so_far: &BoardArea, to_check: &BoardArea,
543 ) -> BoardArea {
544 let mut retv = BoardArea::new(self.size);
545
546 for pos in to_check {
547 if pos
548 .neighbors(self.tessellation, self.size)
549 .map_or(0, |nps| nps.filter(|(_d, p)| found_so_far.contains(*p)).count())
550 > 1
551 {
552 retv.add_point(pos);
553 }
554 }
555
556 retv
557 }
558
559 fn additional_sig_walls_squares(
561 &self, found_so_far: &BoardArea, to_check: &BoardArea,
562 ) -> BoardArea {
563 let mut retv = BoardArea::new(self.size);
564
565 let neighbor = |p: Option<Point2D>, d: Direction| -> Option<Point2D> {
566 if let Some(p1) = p {
567 p1.neighbor(self.tessellation, d, self.size)
568 } else {
569 None
570 }
571 };
572
573 let is_in_area =
574 |p: Option<Point2D>| -> bool { p.is_some_and(|pp| found_so_far.contains(pp)) };
575
576 let is_wall =
577 |p: Option<Point2D>| -> bool { p.is_some_and(|pp| self.walls_positions.contains(pp)) };
578
579 for pos in to_check {
584 let l = pos.neighbor(self.tessellation, Direction::Left, self.size);
585 let u = pos.neighbor(self.tessellation, Direction::Up, self.size);
586 let r = pos.neighbor(self.tessellation, Direction::Right, self.size);
587 let d = pos.neighbor(self.tessellation, Direction::Down, self.size);
588
589 let ne = neighbor(u, Direction::Right);
590 let se = neighbor(d, Direction::Right);
591 let sw = neighbor(d, Direction::Left);
592 let nw = neighbor(u, Direction::Left);
593
594 if (is_wall(u) && is_wall(r) && is_in_area(u) && is_in_area(r) && !is_wall(ne))
595 || (is_wall(r) && is_wall(d) && is_in_area(r) && is_in_area(d) && !is_wall(se))
596 || (is_wall(d) && is_wall(l) && is_in_area(d) && is_in_area(l) && !is_wall(sw))
597 || (is_wall(l) && is_wall(u) && is_in_area(l) && is_in_area(u) && !is_wall(nw))
598 {
599 retv.add_point(pos);
600 }
601 }
602
603 retv
604 }
605}
606
607#[cfg(test)]
608mod tests {
609 use std::collections::HashSet;
610
611 use indoc::indoc;
612
613 use super::*;
614 use crate::{GameBoard, Tessellation};
615
616 #[test]
617 fn it_calculates_playable_areas() -> Result<(), BoardSyntaxError> {
618 let src = indoc! {"##### ###########
627 #@ ###### ##
628 #$ # # . * $ #
629 #. *#$ ## #
630 ##### $### ######
631 ## $ $ # ##
632 ### # ## # ######
633 # # ## ##### ..#
634 # $ $ ..#
635 ##### ### #@ .+#
636 # ## #########
637 # #### ## ### #
638 ######## # ### #
639 # ### ####### #
640 # ## #
641 ###################"};
642
643 let board = GameBoard::parsed(src, Tessellation::Sokoban)?;
644
645 let expected_playable: HashSet<usize> = HashSet::from_iter([
646 20, 21, 22, 39, 40, 41, 43, 44, 45, 58, 59, 60, 62, 63, 64, 81, 82, 83, 98, 99, 100,
647 101, 102, 103, 104, 117, 119, 122, 123, 124, 134, 135, 136, 138, 141, 142, 148, 149,
648 150, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168,
649 169, 176, 180, 182, 183, 184, 185, 186, 187, 188, 195, 196, 197, 198, 199, 217, 218,
650 226, 236, 237, 245, 248, 249, 250, 251, 255, 256, 264, 267, 270, 271, 272, 273, 274,
651 275, 276, 277, 278, 279, 280, 281, 282, 283,
652 ]);
653
654 assert_eq!(
655 HashSet::from_iter(
656 StaticBoardAnalysis::new(
657 board.iter().map(|(_, t)| t),
658 board.size(),
659 board.tessellation(),
660 None
661 )?
662 .playable_area()
663 .iter()
664 .map(|p| p.idx(board.size()))
665 ),
666 expected_playable
667 );
668
669 let expected_walls: HashSet<usize> = HashSet::from_iter([
670 0, 1, 2, 3, 4, 19, 23, 24, 25, 26, 27, 38, 42, 46, 57, 61, 65, 76, 77, 78, 79, 80, 84,
671 85, 86, 97, 105, 106, 114, 115, 116, 118, 120, 121, 125, 128, 129, 130, 131, 132, 133,
672 137, 139, 140, 143, 144, 145, 146, 147, 151, 152, 170, 171, 172, 173, 174, 175, 177,
673 178, 179, 181, 189, 194, 200, 201, 202, 203, 204, 205, 206, 207, 208, 213, 214, 215,
674 216, 219, 225, 227, 228, 229, 230, 231, 232, 233, 235, 238, 244, 246, 247, 252, 253,
675 254, 257, 258, 259, 260, 261, 262, 263, 265, 266, 268, 269, 284, 285, 286, 287, 288,
676 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303,
677 ]);
678 assert_eq!(
679 HashSet::from_iter(
680 StaticBoardAnalysis::new(
681 board.iter().map(|(_, t)| t),
682 board.size(),
683 board.tessellation(),
684 None
685 )?
686 .significant_walls()
687 .iter()
688 .map(|p| p.idx(board.size()))
689 ),
690 expected_walls
691 );
692
693 Ok(())
694 }
695
696 #[test]
697 fn it_calculates_hexoban_outer_cells() -> Result<(), BoardSyntaxError> {
698 let src = indoc! {"-----------#-#-#---
699 ----------#-----#--
700 ---------#-$-.---#-
701 ----#-#-#---*---#--
702 ---#-#---$-.-$-#---
703 --#-@-.-*-.-.---#--
704 ---#---$---$-*-$-#-
705 ----#-#-#-#---.---#
706 -----------#-----#-
707 ------------#-#-#--"};
708
709 let board = GameBoard::parsed(src, Tessellation::Hexoban)?;
710
711 let expected: HashSet<usize> = HashSet::from_iter([
712 0, 1, 2, 3, 4, 8, 9, 10, 11, 12, 17, 18, 19, 20, 21, 27, 28, 35, 36, 44, 53, 54, 63,
713 72, 73, 74, 75, 76, 81, 82, 83, 84, 85, 89,
714 ]);
715 assert_eq!(
716 HashSet::from_iter(
717 StaticBoardAnalysis::new(
718 board.iter().map(|(_, t)| t),
719 board.size(),
720 board.tessellation(),
721 None
722 )?
723 .outer_area()
724 .iter()
725 .map(|p| p.idx(board.size()))
726 ),
727 expected
728 );
729
730 Ok(())
731 }
732}