diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..0e25254 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,236 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'soukoban'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=soukoban" + ], + "filter": { + "name": "soukoban", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug integration test 'action'", + "cargo": { + "args": [ + "test", + "--no-run", + "--test=action", + "--package=soukoban" + ], + "filter": { + "name": "action", + "kind": "test" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug integration test 'actions'", + "cargo": { + "args": [ + "test", + "--no-run", + "--test=actions", + "--package=soukoban" + ], + "filter": { + "name": "actions", + "kind": "test" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug integration test 'deadlock'", + "cargo": { + "args": [ + "test", + "--no-run", + "--test=deadlock", + "--package=soukoban" + ], + "filter": { + "name": "deadlock", + "kind": "test" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug integration test 'direction'", + "cargo": { + "args": [ + "test", + "--no-run", + "--test=direction", + "--package=soukoban" + ], + "filter": { + "name": "direction", + "kind": "test" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug integration test 'level'", + "cargo": { + "args": [ + "test", + "--no-run", + "--test=level", + "--package=soukoban" + ], + "filter": { + "name": "level", + "kind": "test" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug integration test 'map'", + "cargo": { + "args": [ + "test", + "--no-run", + "--test=map", + "--package=soukoban" + ], + "filter": { + "name": "map", + "kind": "test" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug integration test 'path_finding'", + "cargo": { + "args": [ + "test", + "--no-run", + "--test=path_finding", + "--package=soukoban" + ], + "filter": { + "name": "path_finding", + "kind": "test" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug integration test 'run_length'", + "cargo": { + "args": [ + "test", + "--no-run", + "--test=run_length", + "--package=soukoban" + ], + "filter": { + "name": "run_length", + "kind": "test" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug integration test 'solver'", + "cargo": { + "args": [ + "test", + "--no-run", + "--test=solver", + "--package=soukoban" + ], + "filter": { + "name": "solver", + "kind": "test" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug integration test 'utils'", + "cargo": { + "args": [ + "test", + "--no-run", + "--test=utils", + "--package=soukoban" + ], + "filter": { + "name": "utils", + "kind": "test" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug benchmark 'benchmark'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bench=benchmark", + "--package=soukoban" + ], + "filter": { + "name": "benchmark", + "kind": "bench" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/src/deadlock.rs b/src/deadlock.rs index c8c3171..da6ab5b 100644 --- a/src/deadlock.rs +++ b/src/deadlock.rs @@ -95,51 +95,6 @@ pub fn is_freeze_deadlock( true } -/// Calculates unused floors. -pub fn calculate_unused_floors(mut map: Map) -> HashSet> { - let mut unused_floors = HashSet::new(); - - // Add all floors to `unchecked_floors` - let mut unchecked_floors = VecDeque::new(); - for y in 1..map.dimensions().y - 1 { - for x in 1..map.dimensions().x - 1 { - let position = Vector2::new(x, y); - if map[position] == Tiles::Floor { - unchecked_floors.push_back(position); - } - } - } - - while let Some(position) = unchecked_floors.pop_front() { - // Check if the current floor is in a dead end and store the exit position in - // `neighbor_floor` - let mut neighbor_floor = None; - for direction in Direction::iter() { - let neighbor = position + &direction.into(); - if !map[neighbor].intersects(Tiles::Wall) { - if neighbor_floor.is_some() { - neighbor_floor = None; - break; - } - neighbor_floor = Some(neighbor); - } - } - // If the current floor is in a dead end - if let Some(free_neighbor) = neighbor_floor { - unused_floors.insert(position); - map[position].remove(Tiles::Floor); - map[position].insert(Tiles::Wall); - // As the only floor affected by terrain changes, `free_neighbor` may become a - // new unused floor and needs to be rechecked. - if map[free_neighbor] == Tiles::Floor && !map[free_neighbor].intersects(Tiles::Wall) { - unchecked_floors.push_back(free_neighbor); - } - } - } - - unused_floors -} - /// Calculates static deadlock positions independent of the player's position. /// /// This function returns an **incomplete** set of dead positions independent @@ -195,3 +150,57 @@ pub fn calculate_static_deadlocks(map: &Map) -> HashSet> { } dead_positions } + +/// Calculates unused floors. +pub fn calculate_unused_floors(mut map: Map) -> HashSet> { + let mut unused_floors = HashSet::new(); + + // Add all floors to `unchecked_floors` + let mut unchecked_floors = VecDeque::new(); + for y in 1..map.dimensions().y - 1 { + for x in 1..map.dimensions().x - 1 { + let position = Vector2::new(x, y); + if map[position] == Tiles::Floor { + unchecked_floors.push_back(position); + } + } + } + + while let Some(position) = unchecked_floors.pop_front() { + // Check if the current floor is in a dead end and store the exit position in + // `neighbor_floor` + let mut neighbor_floor = None; + for direction in Direction::iter() { + let neighbor = position + &direction.into(); + if !map[neighbor].intersects(Tiles::Wall) { + if neighbor_floor.is_some() { + neighbor_floor = None; + break; + } + neighbor_floor = Some(neighbor); + } + } + // If the current floor is in a dead end + if let Some(free_neighbor) = neighbor_floor { + unused_floors.insert(position); + map[position].remove(Tiles::Floor); + map[position].insert(Tiles::Wall); + // As the only floor affected by terrain changes, `free_neighbor` may become a + // new unused floor and needs to be rechecked. + if map[free_neighbor] == Tiles::Floor && !map[free_neighbor].intersects(Tiles::Wall) { + unchecked_floors.push_back(free_neighbor); + } + } + } + + unused_floors +} + +// pub fn calculate_deadlocked_boxes(mut map: Map) -> Vec> { +// map.box_positions() +// .iter() +// .filter(|&position| { +// is_freeze_deadlock(map, position, map.box_positions(), &mut HashSet::new()) +// }) +// .collect::>() +// } diff --git a/src/map.rs b/src/map.rs index 3b532a0..38de6e2 100644 --- a/src/map.rs +++ b/src/map.rs @@ -158,7 +158,7 @@ impl Map { /// This method can make different maps with the same solution more similar. /// Therefore, it can be used for map deduplication. pub fn normalize(&mut self) { - self.set_immovable_boxes_to_walls(); + self.set_deadlocked_boxes_to_walls(); self.set_unused_floors_to_walls(); self.remove_unreachable_walls(); self.remove_unreachable_boxes(); @@ -357,26 +357,33 @@ impl Map { } } - /// Sets immovable boxes to walls. - fn set_immovable_boxes_to_walls(&mut self) { + /// Sets deadlocked boxes to walls. + fn set_deadlocked_boxes_to_walls(&mut self) { for position in self .box_positions .intersection(&self.goal_positions) .copied() - .collect::>() + .filter(|&position| { + is_freeze_deadlock(self, position, &self.box_positions, &mut HashSet::new()) + }) + .collect::>() { - // If the current box is deadlocked - if is_freeze_deadlock(self, position, &self.box_positions, &mut HashSet::new()) { - debug_assert!( - self[position].contains(Tiles::Box | Tiles::Goal), - "map has no solution" - ); - self.remove_goal_position(position); - self.remove_box_position(position); - self[position].remove(Tiles::Floor); - self[position].insert(Tiles::Wall); - } + self.remove_goal_position(position); + self.remove_box_position(position); + self[position].remove(Tiles::Floor); + self[position].insert(Tiles::Wall); } + + debug_assert!( + !self + .box_positions + .difference(&self.goal_positions) + .copied() + .any(|position| { + is_freeze_deadlock(self, position, &self.box_positions, &mut HashSet::new()) + }), + "map has no solution" + ); } /// Removes unused walls. @@ -394,7 +401,7 @@ impl Map { .iter() .filter(|position| !self[**position].intersects(Tiles::Floor)) .copied() - .collect::>() + .collect::>() { debug_assert!( self[position].contains(Tiles::Box | Tiles::Goal), diff --git a/src/node.rs b/src/node.rs index 59c3856..ae6743f 100644 --- a/src/node.rs +++ b/src/node.rs @@ -1,7 +1,7 @@ use std::{cmp::Ordering, collections::HashSet}; use crate::{ - deadlock::is_freeze_deadlock, + deadlock::*, direction::Direction, path_finding::{find_path, reachable_area}, solver::{Solver, Strategy}, diff --git a/src/state.rs b/src/state.rs index 31cea6e..cd3aa3c 100644 --- a/src/state.rs +++ b/src/state.rs @@ -58,7 +58,7 @@ impl From for State { fn from(map: Map) -> Self { Self { player_position: map.player_position(), - box_positions: map.box_positions().clone(), + box_positions: map.box_positions().clone(), // TODO: Avoid unnecessary cloning. } } } diff --git a/tests/deadlock.rs b/tests/deadlock.rs index ab7ba61..2c7c46a 100644 --- a/tests/deadlock.rs +++ b/tests/deadlock.rs @@ -1,13 +1,13 @@ -use soukoban::deadlock::*; +use soukoban::deadlock; mod utils; use utils::*; #[test] -fn calculate_dead_positions() { +fn calculate_static_deadlocks() { let map = load_level_from_file("assets/Microban_155.xsb", 3).into(); - assert_eq!(calculate_static_deadlocks(&map).len(), 9); + assert_eq!(deadlock::calculate_static_deadlocks(&map).len(), 9); let map = load_level_from_file("assets/BoxWorld_100.xsb", 9).into(); - assert_eq!(calculate_static_deadlocks(&map).len(), 17); + assert_eq!(deadlock::calculate_static_deadlocks(&map).len(), 17); }