diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index aefc00b..dab9da1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,7 +35,7 @@ jobs: uses: taiki-e/install-action@cargo-llvm-cov - name: Install cargo-audit - run: cargo install cargo-audit + run: cargo install --force cargo-audit - name: Audit run: cargo audit diff --git a/src/board.rs b/src/board.rs index 5b57799..13f22e8 100644 --- a/src/board.rs +++ b/src/board.rs @@ -1,8 +1,5 @@ -use crate::level::Level; -use crate::Tile; - use nalgebra::Vector2; -use soukoban::{direction::Direction, Action, Actions}; +use soukoban::{direction::Direction, Action, Actions, Level, Tiles}; #[derive(Clone)] pub struct Board { @@ -23,21 +20,13 @@ impl Board { /// Checks if the player can move or push in the specified direction. pub fn moveable(&self, direction: Direction) -> bool { - let player_next_position = self.level.player_position + &direction.into(); - if self.level.get(&player_next_position).intersects(Tile::Wall) { + let player_next_position = self.level.player_position() + &direction.into(); + if self.level[player_next_position].intersects(Tiles::Wall) { return false; } - if self - .level - .get(&player_next_position) - .intersects(Tile::Crate) - { + if self.level[player_next_position].intersects(Tiles::Box) { let crate_next_position = player_next_position + &direction.into(); - if self - .level - .get(&crate_next_position) - .intersects(Tile::Crate | Tile::Wall) - { + if self.level[crate_next_position].intersects(Tiles::Box | Tiles::Wall) { return false; } } @@ -47,21 +36,13 @@ impl Board { /// Moves the player or pushes a crate in the specified direction. pub fn move_or_push(&mut self, direction: Direction) { let direction_vector = &direction.into(); - let player_next_position = self.level.player_position + direction_vector; - if self.level.get(&player_next_position).intersects(Tile::Wall) { + let player_next_position = self.level.player_position() + direction_vector; + if self.level[player_next_position].intersects(Tiles::Wall) { return; } - if self - .level - .get(&player_next_position) - .intersects(Tile::Crate) - { + if self.level[player_next_position].intersects(Tiles::Box) { let crate_next_position = player_next_position + direction_vector; - if self - .level - .get(&crate_next_position) - .intersects(Tile::Wall | Tile::Crate) - { + if self.level[crate_next_position].intersects(Tiles::Wall | Tiles::Box) { return; } self.move_crate(player_next_position, crate_next_position); @@ -91,10 +72,10 @@ impl Board { let history = self.actions.pop().unwrap(); let direction = history.direction(); if history.is_push() { - let crate_position = self.level.player_position + &direction.into(); - self.move_crate(crate_position, self.level.player_position); + let crate_position = self.level.player_position() + &direction.into(); + self.move_crate(crate_position, self.level.player_position()); } - let player_prev_position = self.level.player_position - &direction.into(); + let player_prev_position = self.level.player_position() - &direction.into(); self.move_player(player_prev_position); self.undone_actions.push(history); } @@ -121,7 +102,7 @@ impl Board { /// Checks if the level is solved. pub fn is_solved(&self) -> bool { - self.level.crate_positions == self.level.target_positions + self.level.box_positions() == self.level.goal_positions() } pub fn actions(&self) -> &Actions { @@ -137,17 +118,15 @@ impl Board { } fn move_player(&mut self, to: Vector2) { - self.level - .get_mut(&self.level.player_position.clone()) - .remove(Tile::Player); - self.level.get_mut(&to).insert(Tile::Player); - self.level.player_position = to; + let player_position = self.level.player_position(); + self.level[player_position].remove(Tiles::Player); + self.level[to].insert(Tiles::Player); + self.level.set_player_position(to); } fn move_crate(&mut self, from: Vector2, to: Vector2) { - self.level.get_mut(&from).remove(Tile::Crate); - self.level.get_mut(&to).insert(Tile::Crate); - self.level.crate_positions.remove(&from); - self.level.crate_positions.insert(to); + self.level[from].remove(Tiles::Box); + self.level[to].insert(Tiles::Box); + self.level.set_box_position(from, to); } } diff --git a/src/database.rs b/src/database.rs index f09717d..77c6998 100644 --- a/src/database.rs +++ b/src/database.rs @@ -1,14 +1,10 @@ -use std::collections::HashMap; use std::hash::{Hash, Hasher}; use std::path::Path; use std::str::FromStr; -use crate::level::Level; - -use nalgebra::Vector2; use rusqlite::Connection; use siphasher::sip::SipHasher24; -use soukoban::Actions; +use soukoban::{Actions, Level}; pub struct Database { connection: Connection, @@ -75,14 +71,14 @@ impl Database { /// Imports a single level into the database. pub fn import_level(&self, level: &Level) { - let title = level.metadata.get("title"); - let author = level.metadata.get("author"); - let comments = level.metadata.get("comments"); + let title = level.metadata().get("title"); + let author = level.metadata().get("author"); + let comments = level.metadata().get("comments"); let hash = Database::normalized_hash(level); let _ = self.connection.execute( "INSERT INTO tb_level(title, author, comments, map, width, height, hash, date) VALUES (?, ?, ?, ?, ?, ?, ?, DATE('now'))", - (title, author, comments, level.export_map(), level.dimensions().x, level.dimensions().y, hash), + (title, author, comments, level.map().to_string(), level.dimensions().x, level.dimensions().y, hash), ); } @@ -110,24 +106,18 @@ impl Database { let mut rows = statement.query([id]).unwrap(); let row = rows.next().unwrap()?; - let map = row - .get::<_, String>(0) - .unwrap() - .split('\n') - .map(|x| x.to_string()) - .collect(); - let size = Vector2::new(row.get(1).unwrap(), row.get(2).unwrap()); - let mut metadata = HashMap::new(); - if let Ok(title) = row.get(3) { - metadata.insert("title".to_string(), title); + let map = row.get::<_, String>(0).unwrap(); + let mut metadata = String::new(); + if let Ok(title) = row.get::<_, String>(3) { + metadata.push_str(&format!("title: {title}\n")); } - if let Ok(author) = row.get(4) { - metadata.insert("author".to_string(), author); + if let Ok(author) = row.get::<_, String>(4) { + metadata.push_str(&format!("author: {author}\n")); } - if let Ok(comments) = row.get(5) { - metadata.insert("comments".to_string(), comments); + if let Ok(comments) = row.get::<_, String>(5) { + metadata.push_str(&format!("comments: {comments}\n")); } - let level = Level::new(map, size, metadata).unwrap(); + let level = Level::from_str(&(map + &metadata)).unwrap(); Some(level) } diff --git a/src/level.rs b/src/level.rs index bb1bab6..1efd832 100644 --- a/src/level.rs +++ b/src/level.rs @@ -1,769 +1,106 @@ -use bitflags::bitflags; use nalgebra::Vector2; -use siphasher::sip::SipHasher24; -use soukoban::run_length::rle_decode; +use soukoban::{direction::Direction, path_finding::reachable_area, Level, Tiles}; -use soukoban::direction::Direction; - -use std::cmp; -use std::collections::{HashMap, HashSet, VecDeque}; -use std::hash::{Hash, Hasher}; -use std::path::Path; -use std::{fmt, fs}; +use std::{ + collections::{HashMap, HashSet, VecDeque}, + hash::Hash, +}; #[derive(Clone, PartialEq, Eq, Hash, Debug)] pub struct PushState { pub push_direction: Direction, - pub crate_position: Vector2, -} - -bitflags! { - #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] - pub struct Tile: u8 { - const Void = 1 << 0; - const Floor = 1 << 1; - const Wall = 1 << 2; - const Crate = 1 << 3; - const Target = 1 << 4; - const Player = 1 << 5; - - const Deadlock = 1 << 6; - } -} - -#[derive(Clone)] -pub struct Level { - data: Vec, - dimensions: Vector2, - pub metadata: HashMap, - - pub player_position: Vector2, - pub crate_positions: HashSet>, - pub target_positions: HashSet>, -} - -impl Hash for Level { - fn hash(&self, state: &mut H) { - self.data.hash(state); - } + pub box_position: Vector2, } -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum ParseMapError { - NoPlayer, - MoreThanOnePlayer, - MismatchBetweenCratesAndTargets, - InvalidCharacter(char), -} - -impl fmt::Display for ParseMapError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - ParseMapError::MoreThanOnePlayer => write!(f, "more than one player"), - ParseMapError::NoPlayer => write!(f, "no player"), - ParseMapError::MismatchBetweenCratesAndTargets => { - write!(f, "mismatch between number of crates and targets") - } - ParseMapError::InvalidCharacter(c) => write!(f, "invalid character: {}", c), - } - } -} - -type Result = std::result::Result; - -impl Level { - /// Creates a new level. - pub fn new( - map: Vec, - dimensions: Vector2, - metadata: HashMap, - ) -> Result { - let mut data = vec![Tile::Void; (dimensions.x * dimensions.y) as usize]; - let mut player_position: Option> = None; - let mut crate_positions = HashSet::>::new(); - let mut target_positions = HashSet::>::new(); - - for (y, line) in map.iter().enumerate() { - for (x, char) in line.chars().enumerate() { - let position = Vector2::::new(x as i32, y as i32); - data[y * dimensions.x as usize + x] = match char { - ' ' | '-' | '_' => Tile::Void, - '#' => Tile::Wall, - '$' => { - crate_positions.insert(position); - Tile::Crate - } - '.' => { - target_positions.insert(position); - Tile::Target - } - '@' => { - if player_position.is_some() { - return Err(ParseMapError::MoreThanOnePlayer); - } - player_position = Some(position); - Tile::Player - } - '*' => { - crate_positions.insert(position); - target_positions.insert(position); - Tile::Crate | Tile::Target - } - '+' => { - if player_position.is_some() { - return Err(ParseMapError::MoreThanOnePlayer); - } - player_position = Some(position); - target_positions.insert(position); - Tile::Player | Tile::Target - } - _ => return Err(ParseMapError::InvalidCharacter(char)), - }; - } - } - if player_position.is_none() { - return Err(ParseMapError::NoPlayer); - } - if crate_positions.len() != target_positions.len() { - return Err(ParseMapError::MismatchBetweenCratesAndTargets); - } - - crate_positions.shrink_to_fit(); - target_positions.shrink_to_fit(); - - let mut instance = Self { - data, - dimensions, - metadata, - player_position: player_position.unwrap(), - crate_positions, - target_positions, +pub fn crate_pushable_paths_with_crate_positions( + level: &Level, + crate_position: &Vector2, + initial_crate_positions: &HashSet>, +) -> HashMap>> { + let mut paths = HashMap::>>::new(); + let mut visited = HashSet::new(); + let mut queue = VecDeque::new(); + + let player_reachable_area = reachable_area(level.player_position(), |position| { + !level[position].intersects(Tiles::Wall) && !initial_crate_positions.contains(&position) + }); + for push_direction in [ + Direction::Up, + Direction::Down, + Direction::Left, + Direction::Right, + ] { + let player_position = crate_position - &push_direction.into(); + if level[player_position].intersects(Tiles::Wall) + || !player_reachable_area.contains(&player_position) + { + continue; + } + let new_state = PushState { + push_direction, + box_position: *crate_position, }; - instance.flood_fill(&instance.player_position.clone(), Tile::Floor, Tile::Wall); - instance.calculate_dead_positions(); - Ok(instance) + paths.insert(new_state.clone(), vec![*crate_position]); + queue.push_front(new_state); } - /// Creates a new empty level. - pub fn empty() -> Self { - Self { - data: vec![Tile::Void; 0], - dimensions: Vector2::::zeros(), - metadata: HashMap::::new(), - player_position: Vector2::::zeros(), - crate_positions: HashSet::>::new(), - target_positions: HashSet::>::new(), - } - } - - /// Loads levels from string. - pub fn load_from_memory(buffer: String) -> Result> { - let buffer = buffer.replace('\r', "") + "\n"; - - let mut levels = Vec::new(); - - let mut map_data = Vec::::new(); - let mut map_dimensions = Vector2::::zeros(); - let mut metadata = HashMap::::new(); - let mut comments = String::new(); - - let mut in_comment_block = false; - for line in buffer.split(&['\n', '|']) { - let trimmed_line = line.trim(); - - // comment - if in_comment_block { - if trimmed_line.to_lowercase().starts_with("comment-end") - || trimmed_line.to_lowercase().starts_with("comment_end") - { - in_comment_block = false; - continue; - } - comments += &(trimmed_line.to_string() + "\n"); - continue; - } - if let Some(comment) = trimmed_line.strip_prefix(';') { - comments += &(comment.trim_start().to_string() + "\n"); - continue; - } - - if trimmed_line.is_empty() { - // multiple empty lines - if map_data.is_empty() { - metadata.clear(); - comments.clear(); - continue; - } - - debug_assert!(!metadata.contains_key("comments")); - if !comments.is_empty() { - metadata.insert("comments".to_string(), comments.clone()); - } - levels.push(Level::new( - map_data.clone(), - map_dimensions, - metadata.clone(), - )?); - map_data.clear(); - map_dimensions = Vector2::::zeros(); - metadata.clear(); - comments.clear(); - continue; - } - - // metadata - if trimmed_line.starts_with('\'') { - metadata.insert( - "title".to_string(), - trimmed_line[1..trimmed_line.len() - 1].to_string(), - ); - continue; - } - if trimmed_line.contains(':') { - let (key, value) = trimmed_line.split_once(':').unwrap(); - let key = key.trim().to_lowercase(); - let value = value.trim(); - - if key == "comment" { - if value.is_empty() { - in_comment_block = true; - } else { - comments += &(value.to_string() + "\n"); - } - continue; - } - - metadata.insert(key, value.to_string()); - continue; - } - - // if line is not map data, discard - if !line.chars().all(|c| { - matches!( - c, - '0'..='9' | ' ' | '-' | '_' | '#' | '$' | '.' | '@' | '*' | '+' - ) - }) { - continue; - } - - // if line contains numbers, perform RLE decoding - if line.chars().any(|c| c.is_ascii_digit()) { - map_data.push(rle_decode(line).unwrap()); - } else { - map_data.push(line.to_string()); - } - - map_dimensions.x = cmp::max(line.len() as i32, map_dimensions.x); - map_dimensions.y += 1; - } - - Ok(levels) - } - - /// Loads levels from file. - pub fn load_from_file(file_path: &Path) -> Result> { - Self::load_from_memory(fs::read_to_string(file_path).unwrap()) - } - - /// Returns the dimensions of the level. - pub fn dimensions(&self) -> &Vector2 { - &self.dimensions - } - - /// Returns the tile at the specified position without bounds checking. - pub fn get(&self, position: &Vector2) -> Tile { - debug_assert!(self.in_bounds(position)); - self.data[(position.y * self.dimensions.x + position.x) as usize] - } - - /// Returns a mutable reference to the tile at the specified position without bounds checking. - pub fn get_mut(&mut self, position: &Vector2) -> &mut Tile { - debug_assert!(self.in_bounds(position)); - &mut self.data[(position.y * self.dimensions.x + position.x) as usize] - } - - /// Exports the map layout as a XSB format string. - pub fn export_map(&self) -> String { - let mut result = String::new(); - for y in 0..self.dimensions.y { - for x in 0..self.dimensions.x { - let tiles = self.get(&Vector2::::new(x, y)); - if tiles.contains(Tile::Crate | Tile::Target) { - result.push('*'); - } else if tiles.contains(Tile::Player | Tile::Target) { - result.push('+'); - } else if tiles.contains(Tile::Wall) { - result.push('#'); - } else if tiles.contains(Tile::Crate) { - result.push('$'); - } else if tiles.contains(Tile::Target) { - result.push('.'); - } else if tiles.contains(Tile::Player) { - result.push('@'); - } else { - result.push(' '); - } - } - result.push('\n'); - } - result - } - - /// Exports metadata as a XSB format string. - pub fn export_metadata(&self) -> String { - let mut result = String::new(); - for (key, value) in self.metadata.iter() { - if key == "comments" { - result.push_str("comment:\n"); - for line in value.lines() { - result.push_str(&format!("{}\n", line)); - } - result.push_str("comment-end:\n"); - continue; - } - debug_assert!(!value.contains('\n')); - result.push_str(&format!("{}: {}\n", key, value)); - } - result - } - - /// Normalizes the level. - pub fn normalize(&mut self) { - assert!(self.get(&self.player_position).contains(Tile::Floor)); - self.clear(Tile::Wall); - self.clear(Tile::Void); - for x in 0..self.dimensions.x { - for y in 0..self.dimensions.y { - let position = Vector2::::new(x, y); - if self.get(&position).intersects(Tile::Floor) { - let directions = [ - Vector2::::y(), - -Vector2::::y(), - Vector2::::x(), - -Vector2::::x(), - Vector2::::new(1, 1), - Vector2::::new(-1, -1), - Vector2::::new(1, -1), - Vector2::::new(-1, 1), - ]; - for direction in directions { - let neighbor_position = position + direction; - if !self.get(&neighbor_position).contains(Tile::Floor) { - self.get_mut(&neighbor_position).insert(Tile::Wall); - } - } - } - } - } - - let mut min_hash = u64::MAX; - for i in 1..=8 { - self.rotate(); - if i == 5 { - self.flip(); - } - - self.set_player_position(&normalized_area( - &self.reachable_area(&self.player_position, |position| { - self.get(position).intersects(Tile::Wall | Tile::Crate) - }), - )); - - let mut hasher = SipHasher24::new(); - self.hash(&mut hasher); - let hash = hasher.finish(); - - min_hash = cmp::min(min_hash, hash); - } + while let Some(state) = queue.pop_back() { + let mut crate_positions = initial_crate_positions.clone(); + crate_positions.remove(crate_position); + crate_positions.insert(state.box_position); - for i in 1..=8 { - self.rotate(); - if i == 5 { - self.flip(); - } - - self.set_player_position(&normalized_area( - &self.reachable_area(&self.player_position, |position| { - self.get(position).intersects(Tile::Wall | Tile::Crate) - }), - )); - - let mut hasher = SipHasher24::new(); - self.hash(&mut hasher); - let hash = hasher.finish(); - - if hash == min_hash { - return; - } - } - unreachable!(); - } - - /// Calculates dead square positions - fn calculate_dead_positions(&mut self) { - for x in 1..self.dimensions.x - 1 { - for y in 1..self.dimensions.y - 1 { - let position = Vector2::new(x, y); - if !self.get(&position).intersects(Tile::Floor) - || self - .get(&position) - .intersects(Tile::Target | Tile::Deadlock) - { - continue; - } - - for directions in [ - Direction::Up, - Direction::Right, - Direction::Down, - Direction::Left, - Direction::Up, - ] - .windows(2) - { - let neighbor = [ - position + &directions[0].into(), - position + &directions[1].into(), - ]; - if !(self.get(&neighbor[0]).intersects(Tile::Wall) - && self.get(&neighbor[1]).intersects(Tile::Wall)) - { - continue; - } - - self.get_mut(&position).insert(Tile::Deadlock); - - let mut dead_positions = HashSet::new(); - let mut next_position = position; - while !self.get(&next_position).intersects(Tile::Wall) - && self - .get(&(next_position + &directions[1].into())) - .intersects(Tile::Wall) - { - dead_positions.insert(next_position); - next_position += -Into::>::into(directions[0]); - if self.get(&next_position).intersects(Tile::Target) { - break; - } - if self.get(&next_position).intersects(Tile::Wall) { - for dead_position in dead_positions { - self.get_mut(&dead_position).insert(Tile::Deadlock); - } - break; - } - } - - let mut dead_positions = HashSet::new(); - let mut next_position = position; - while !self.get(&next_position).intersects(Tile::Wall) - && self - .get(&(next_position + &directions[0].into())) - .intersects(Tile::Wall) - { - dead_positions.insert(next_position); - next_position += -Into::>::into(directions[1]); - if self.get(&next_position).intersects(Tile::Target) { - break; - } - if self.get(&next_position).intersects(Tile::Wall) { - for dead_position in dead_positions { - self.get_mut(&dead_position).insert(Tile::Deadlock); - } - break; - } - } - } - } - } - } - - pub fn reachable_area( - &self, - position: &Vector2, - is_block: impl Fn(&Vector2) -> bool, - ) -> HashSet> { - let mut reachable = HashSet::new(); - let mut queue = VecDeque::>::new(); - queue.push_back(*position); - - while let Some(position) = queue.pop_front() { - if !reachable.insert(position) { - continue; - } - - for direction in [ - Direction::Up, - Direction::Down, - Direction::Left, - Direction::Right, - ] { - let next_position = position + &direction.into(); - if is_block(&next_position) { - continue; - } - queue.push_back(next_position); - } - } - - reachable - } - - pub fn crate_pushable_paths_with_crate_positions( - &self, - crate_position: &Vector2, - initial_crate_positions: &HashSet>, - ) -> HashMap>> { - let mut paths = HashMap::>>::new(); - let mut visited = HashSet::new(); - let mut queue = VecDeque::new(); - - let player_reachable_area = self.reachable_area(&self.player_position, |position| { - self.get(position).intersects(Tile::Wall) || initial_crate_positions.contains(position) + let player_position = state.box_position - &state.push_direction.into(); + let player_reachable_area = reachable_area(player_position, |position| { + !level[position].intersects(Tiles::Wall) && !crate_positions.contains(&position) }); + for push_direction in [ Direction::Up, Direction::Down, Direction::Left, Direction::Right, ] { - let player_position = crate_position - &push_direction.into(); - if self.get(&player_position).intersects(Tile::Wall) + let new_crate_position = state.box_position + &push_direction.into(); + let player_position = state.box_position - &push_direction.into(); + + if level[new_crate_position].intersects(Tiles::Wall /* | Tiles::Deadlock */) + || crate_positions.contains(&new_crate_position) + { + continue; + } + + if level[player_position].intersects(Tiles::Wall) || !player_reachable_area.contains(&player_position) { continue; } + let new_state = PushState { push_direction, - crate_position: *crate_position, + box_position: new_crate_position, }; - paths.insert(new_state.clone(), vec![*crate_position]); - queue.push_front(new_state); - } - - while let Some(state) = queue.pop_back() { - let mut crate_positions = initial_crate_positions.clone(); - crate_positions.remove(crate_position); - crate_positions.insert(state.crate_position); - - let player_position = state.crate_position - &state.push_direction.into(); - let player_reachable_area = self.reachable_area(&player_position, |position| { - self.get(position).intersects(Tile::Wall) || crate_positions.contains(position) - }); - - for push_direction in [ - Direction::Up, - Direction::Down, - Direction::Left, - Direction::Right, - ] { - let new_crate_position = state.crate_position + &push_direction.into(); - let player_position = state.crate_position - &push_direction.into(); - - if self - .get(&new_crate_position) - .intersects(Tile::Wall | Tile::Deadlock) - || crate_positions.contains(&new_crate_position) - { - continue; - } - - if self.get(&player_position).intersects(Tile::Wall) - || !player_reachable_area.contains(&player_position) - { - continue; - } - - let new_state = PushState { - push_direction, - crate_position: new_crate_position, - }; - if !visited.insert(new_state.clone()) { - continue; - } - - let mut new_path = paths[&state].clone(); - new_path.push(new_crate_position); - paths.insert(new_state.clone(), new_path); - - queue.push_front(new_state); - } - } - - paths.retain(|state, _| state.crate_position != *crate_position); - paths - } - - /// Finds paths for pushing a crate from `crate_position` to other positions. - pub fn crate_pushable_paths( - &self, - crate_position: &Vector2, - ) -> HashMap>> { - debug_assert!(self.crate_positions.contains(crate_position)); - self.crate_pushable_paths_with_crate_positions(crate_position, &self.crate_positions) - } - - fn set_player_position(&mut self, position: &Vector2) { - self.get_mut(&self.player_position.clone()) - .remove(Tile::Player); - self.player_position = *position; - self.get_mut(&self.player_position.clone()) - .insert(Tile::Player); - } - - /// Rotates the level 90° clockwise. - fn rotate(&mut self) { - let rotate_position = - |position: &Vector2| Vector2::new(position.y, self.dimensions.x - 1 - position.x); - - let mut rotated_data = vec![Tile::Void; (self.dimensions.x * self.dimensions.y) as usize]; - for x in 0..self.dimensions.x { - for y in 0..self.dimensions.y { - let position = Vector2::new(x, y); - let rotated_position = rotate_position(&position); - rotated_data - [(rotated_position.x + rotated_position.y * self.dimensions.y) as usize] = - self.get(&position); - } - } - - self.data = rotated_data; - self.player_position = rotate_position(&self.player_position); - self.crate_positions = self.crate_positions.iter().map(rotate_position).collect(); - self.dimensions = self.dimensions.yx(); - } - - /// Flips the level horizontally. - fn flip(&mut self) { - let flip_position = - |position: &Vector2| Vector2::new(self.dimensions.x - 1 - position.x, position.y); - - let mut flipped_data = vec![Tile::Void; (self.dimensions.x * self.dimensions.y) as usize]; - for x in 0..self.dimensions.x { - for y in 0..self.dimensions.y { - let position = Vector2::new(x, y); - let flipped_position = flip_position(&position); - flipped_data - [(flipped_position.x + flipped_position.y * self.dimensions.x) as usize] = - self.get(&position); - } - } - - self.data = flipped_data; - self.player_position = flip_position(&self.player_position); - self.crate_positions = self.crate_positions.iter().map(flip_position).collect(); - } - - /// Checks if a position is within the bounds of the level. - pub fn in_bounds(&self, position: &Vector2) -> bool { - 0 <= position.x - && position.x < self.dimensions.x - && 0 <= position.y - && position.y < self.dimensions.y - } - - pub fn clear(&mut self, value: Tile) { - for x in 0..self.dimensions.x { - for y in 0..self.dimensions.y { - self.get_mut(&Vector2::::new(x, y)).remove(value); - } - } - } - - fn flood_fill(&mut self, start_position: &Vector2, value: Tile, border: Tile) { - let mut visited = HashSet::new(); - let mut queue = VecDeque::new(); - - if !self.in_bounds(start_position) { - return; - } - - queue.push_back(*start_position); - while let Some(position) = queue.pop_front() { - if visited.contains(&position) { + if !visited.insert(new_state.clone()) { continue; } - visited.insert(position); - - if self.get(&position).contains(value) { - continue; - } - self.get_mut(&position).insert(value); - - let directions = [ - Vector2::::y(), - -Vector2::::y(), - Vector2::::x(), - -Vector2::::x(), - ]; - for direction in directions { - let neighbor_position = position + direction; - if !self.in_bounds(&neighbor_position) { - continue; - } - if self.get(&neighbor_position).intersects(value | border) - || visited.contains(&neighbor_position) - { - continue; - } + let mut new_path = paths[&state].clone(); + new_path.push(new_crate_position); + paths.insert(new_state.clone(), new_path); - queue.push_back(neighbor_position); - } + queue.push_front(new_state); } } -} -pub fn normalized_area(area: &HashSet>) -> Vector2 { - *area - .iter() - .min_by(|a, b| a.x.cmp(&b.x).then_with(|| a.y.cmp(&b.y))) - .unwrap() + paths.retain(|state, _| state.box_position != *crate_position); + paths } -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_level_load_from_memory() { - let no_player_level = String::from( - r#" - ##### - # $.# - ##### - "#, - ); - let more_than_one_player_level = String::from( - r#" - ###### - #@@$.# - ###### - "#, - ); - let mismatch_between_crates_and_targets_level = String::from( - r#" - ###### - #@$$.# - ###### - "#, - ); - assert_eq!( - Level::load_from_memory(no_player_level).err().unwrap(), - ParseMapError::NoPlayer - ); - assert_eq!( - Level::load_from_memory(more_than_one_player_level) - .err() - .unwrap(), - ParseMapError::MoreThanOnePlayer - ); - assert_eq!( - Level::load_from_memory(mismatch_between_crates_and_targets_level) - .err() - .unwrap(), - ParseMapError::MismatchBetweenCratesAndTargets - ); - } +/// Finds paths for pushing a crate from `crate_position` to other positions. +pub fn crate_pushable_paths( + level: &Level, + crate_position: &Vector2, +) -> HashMap>> { + debug_assert!(level.box_positions().contains(crate_position)); + crate_pushable_paths_with_crate_positions(level, crate_position, level.box_positions()) } diff --git a/src/resources.rs b/src/resources.rs index 76db6ad..a847b4e 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -2,10 +2,11 @@ use bevy::prelude::*; use bevy::time::Stopwatch; use nalgebra::Vector2; use serde::{Deserialize, Serialize}; +use soukoban::{Level, Map}; +use crate::database; use crate::level::PushState; use crate::solve::solver::*; -use crate::{database, Level}; use soukoban::direction::Direction; use std::collections::{HashMap, VecDeque}; @@ -87,11 +88,11 @@ impl Default for SolverState { fn default() -> Self { Self { solver: Mutex::new(Solver::new( - Level::empty(), + Level::from_map(Map::with_dimensions(Vector2::new(0, 0))), Strategy::default(), LowerBoundMethod::default(), )), - level: Level::empty(), + level: Level::from_map(Map::with_dimensions(Vector2::new(0, 0))), stopwatch: Stopwatch::new(), } } diff --git a/src/solve/solver.rs b/src/solve/solver.rs index d82c179..9dece4e 100644 --- a/src/solve/solver.rs +++ b/src/solve/solver.rs @@ -1,17 +1,19 @@ -use std::cell::OnceCell; -use std::cmp::Ordering; -use std::collections::{BinaryHeap, HashMap, HashSet}; -use std::hash::Hash; -use std::time::{Duration, Instant}; - -use crate::level::{Level, Tile}; +use std::{ + cell::OnceCell, + cmp::Ordering, + collections::{BinaryHeap, HashMap, HashSet}, + hash::Hash, + time::{Duration, Instant}, +}; + +use crate::crate_pushable_paths_with_crate_positions; use crate::solve::state::*; -use soukoban::direction::Direction; +use soukoban::{direction::Direction, path_finding::reachable_area, Level}; use itertools::Itertools; use nalgebra::Vector2; use serde::{Deserialize, Serialize}; -use soukoban::Actions; +use soukoban::{Actions, Tiles}; #[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize)] pub enum Strategy { @@ -62,8 +64,7 @@ type Result = std::result::Result; impl Solver { /// Creates a new solver. - pub fn new(mut level: Level, strategy: Strategy, lower_bound_method: LowerBoundMethod) -> Self { - level.clear(Tile::Player | Tile::Crate); + pub fn new(level: Level, strategy: Strategy, lower_bound_method: LowerBoundMethod) -> Self { let mut instance = Self { level, strategy, @@ -74,9 +75,8 @@ impl Solver { heap: BinaryHeap::new(), }; instance.heap.push(State::new( - instance.level.player_position, - instance.level.crate_positions.clone(), - // TODO: from_str("") -> new() or default() + instance.level.player_position(), + instance.level.box_positions().clone(), Actions::new(), &instance, )); @@ -129,7 +129,7 @@ impl Solver { for x in 1..self.level.dimensions().x - 1 { for y in 1..self.level.dimensions().y - 1 { let player_position = Vector2::new(x, y); - if !self.level.get(&player_position).intersects(Tile::Floor) { + if !self.level[player_position].intersects(Tiles::Floor) { continue; } @@ -147,68 +147,32 @@ impl Solver { { // #$# // #@# - if self - .level - .get(&(player_position + &left.into())) - .intersects(Tile::Wall) - && self - .level - .get(&(player_position + &right.into())) - .intersects(Tile::Wall) - && self - .level - .get(&(player_position + &up.into() + &left.into())) - .intersects(Tile::Wall) - && self - .level - .get(&(player_position + &up.into() + &right.into())) - .intersects(Tile::Wall) - && self - .level - .get(&(player_position + &up.into())) - .intersects(Tile::Floor) - && !self - .level - .get(&(player_position + &up.into())) - .intersects(Tile::Target) + if self.level[player_position + &left.into()].intersects(Tiles::Wall) + && self.level[player_position + &right.into()].intersects(Tiles::Wall) + && self.level[player_position + &up.into() + &left.into()] + .intersects(Tiles::Wall) + && self.level[player_position + &up.into() + &right.into()] + .intersects(Tiles::Wall) + && self.level[player_position + &up.into()].intersects(Tiles::Floor) + && !self.level[player_position + &up.into()].intersects(Tiles::Goal) { tunnels.insert((player_position, up)); } // #$_ _$# // #@# #@# - if self - .level - .get(&(player_position + &left.into())) - .intersects(Tile::Wall) - && self - .level - .get(&(player_position + &right.into())) - .intersects(Tile::Wall) - && (self - .level - .get(&(player_position + &up.into() + &right.into())) - .intersects(Tile::Wall) - && self - .level - .get(&(player_position + &up.into() + &left.into())) - .intersects(Tile::Floor) - || self - .level - .get(&(player_position + &up.into() + &right.into())) - .intersects(Tile::Floor) - && self - .level - .get(&(player_position + &up.into() + &left.into())) - .intersects(Tile::Wall)) - && self - .level - .get(&(player_position + &up.into())) - .intersects(Tile::Floor) - && !self - .level - .get(&(player_position + &up.into())) - .intersects(Tile::Target) + if self.level[player_position + &left.into()].intersects(Tiles::Wall) + && self.level[player_position + &right.into()].intersects(Tiles::Wall) + && (self.level[player_position + &up.into() + &right.into()] + .intersects(Tiles::Wall) + && self.level[player_position + &up.into() + &left.into()] + .intersects(Tiles::Floor) + || self.level[player_position + &up.into() + &right.into()] + .intersects(Tiles::Floor) + && self.level[player_position + &up.into() + &left.into()] + .intersects(Tiles::Wall)) + && self.level[player_position + &up.into()].intersects(Tiles::Floor) + && !self.level[player_position + &up.into()].intersects(Tiles::Goal) { tunnels.insert((player_position, up)); } @@ -237,7 +201,7 @@ impl Solver { /// Calculates and returns the lower bounds using the minimum push method. fn minimum_push_lower_bounds(&self) -> HashMap, usize> { let mut lower_bounds = HashMap::new(); - for target_position in &self.level.target_positions { + for target_position in self.level.goal_positions() { lower_bounds.insert(*target_position, 0); let mut player_position = None; for pull_direction in [ @@ -248,9 +212,9 @@ impl Solver { ] { let next_crate_position = target_position + &pull_direction.into(); let next_player_position = next_crate_position + &pull_direction.into(); - if self.level.in_bounds(&next_player_position) - && !self.level.get(&next_player_position).intersects(Tile::Wall) - && !self.level.get(&next_crate_position).intersects(Tile::Wall) + if self.level.in_bounds(next_player_position) + && !self.level[next_player_position].intersects(Tiles::Wall) + && !self.level[next_crate_position].intersects(Tiles::Wall) { player_position = Some(next_player_position); break; @@ -258,8 +222,8 @@ impl Solver { } if let Some(player_position) = player_position { self.minimum_push_to( - target_position, - &player_position, + *target_position, + player_position, &mut lower_bounds, &mut HashSet::new(), ); @@ -272,13 +236,13 @@ impl Solver { fn minimum_push_to( &self, - crate_position: &Vector2, - player_position: &Vector2, + box_position: Vector2, + player_position: Vector2, lower_bounds: &mut HashMap, usize>, visited: &mut HashSet<(Vector2, Direction)>, ) { - let player_reachable_area = self.level.reachable_area(player_position, |position| { - self.level.get(position).intersects(Tile::Wall) || position == crate_position + let player_reachable_area = reachable_area(player_position, |position| { + !self.level[position].intersects(Tiles::Wall) && position != box_position }); for pull_direction in [ Direction::Up, @@ -286,14 +250,14 @@ impl Solver { Direction::Down, Direction::Left, ] { - let next_crate_position = crate_position + &pull_direction.into(); - if self.level.get(&next_crate_position).intersects(Tile::Wall) { + let next_crate_position = box_position + &pull_direction.into(); + if self.level[next_crate_position].intersects(Tiles::Wall) { continue; } let next_player_position = next_crate_position + &pull_direction.into(); - if !self.level.in_bounds(&next_player_position) - || self.level.get(&next_player_position).intersects(Tile::Wall) + if !self.level.in_bounds(next_player_position) + || self.level[next_player_position].intersects(Tiles::Wall) { continue; } @@ -304,7 +268,7 @@ impl Solver { let lower_bound = *lower_bounds .get(&next_crate_position) .unwrap_or(&usize::MAX); - let new_lower_bound = lower_bounds[crate_position] + 1; + let new_lower_bound = lower_bounds[&box_position] + 1; if !visited.insert((next_crate_position, pull_direction)) { continue; } @@ -312,8 +276,8 @@ impl Solver { lower_bounds.insert(next_crate_position, new_lower_bound); } self.minimum_push_to( - &next_crate_position, - &next_player_position, + next_crate_position, + next_player_position, lower_bounds, visited, ); @@ -328,26 +292,24 @@ impl Solver { let position = Vector2::new(x, y); // There may be situations in the level where the box is // already on the target and cannot be reached by the player. - if self.level.get(&position).intersects(Tile::Target) { + if self.level[position].intersects(Tiles::Goal) { lower_bounds.insert(position, 0); continue; } - if !self.level.get(&position).intersects(Tile::Floor) - || self.level.get(&position).intersects(Tile::Deadlock) + if !self.level[position].intersects(Tiles::Floor) + // || self.level[position].intersects(Tiles::Deadlock) { continue; } - let paths = self - .level - .crate_pushable_paths_with_crate_positions(&position, &HashSet::new()); + let paths = crate_pushable_paths_with_crate_positions( + &self.level, + &position, + &HashSet::new(), + ); if let Some(lower_bound) = paths .iter() - .filter(|path| { - self.level - .get(&path.0.crate_position) - .intersects(Tile::Target) - }) + .filter(|path| self.level[path.0.box_position].intersects(Tiles::Goal)) .map(|path| path.1.len() - 1) .min() { @@ -366,18 +328,18 @@ impl Solver { let position = Vector2::new(x, y); // There may be situations in the level where the box is // already on the target and cannot be reached by the player. - if self.level.get(&position).intersects(Tile::Target) { + if self.level[position].intersects(Tiles::Goal) { lower_bounds.insert(position, 0); continue; } - if !self.level.get(&position).intersects(Tile::Floor) - || self.level.get(&position).intersects(Tile::Deadlock) + if !self.level[position].intersects(Tiles::Floor) + // || self.level.get(&position).intersects(Tiles::Deadlock) { continue; } let lower_bound = self .level - .target_positions + .goal_positions() .iter() .map(|crate_pos| manhattan_distance(crate_pos, &position)) .min() diff --git a/src/solve/state.rs b/src/solve/state.rs index 3eed815..27f01e9 100644 --- a/src/solve/state.rs +++ b/src/solve/state.rs @@ -1,20 +1,24 @@ -use crate::level::{normalized_area, Tile}; -use crate::solve::solver::*; -use soukoban::direction::Direction; +use std::{ + cell::OnceCell, + cmp::Ordering, + collections::HashSet, + hash::{Hash, Hasher}, +}; -use std::cell::OnceCell; -use std::cmp::Ordering; -use std::collections::HashSet; -use std::hash::{Hash, Hasher}; +use crate::solve::solver::*; use nalgebra::Vector2; use siphasher::sip::SipHasher24; -use soukoban::{Action, Actions}; +use soukoban::{ + direction::Direction, + path_finding::{normalized_area, reachable_area}, + Action, Actions, Tiles, +}; #[derive(Clone, Eq)] pub struct State { pub player_position: Vector2, - pub crate_positions: HashSet>, + pub box_positions: HashSet>, pub movements: Actions, heuristic: usize, lower_bound: OnceCell, @@ -22,15 +26,14 @@ pub struct State { impl PartialEq for State { fn eq(&self, other: &Self) -> bool { - self.player_position == other.player_position - && self.crate_positions == other.crate_positions + self.player_position == other.player_position && self.box_positions == other.box_positions } } impl Hash for State { fn hash(&self, state: &mut H) { self.player_position.hash(state); - for position in &self.crate_positions { + for position in &self.box_positions { position.hash(state); } } @@ -57,7 +60,7 @@ impl State { ) -> Self { let mut instance = Self { player_position, - crate_positions, + box_positions: crate_positions, movements, heuristic: 0, lower_bound: OnceCell::new(), @@ -79,7 +82,7 @@ impl State { + instance.lower_bound(solver) } }; - instance.crate_positions.shrink_to_fit(); + instance.box_positions.shrink_to_fit(); instance.movements.shrink_to_fit(); instance } @@ -88,7 +91,7 @@ impl State { pub fn successors(&self, solver: &Solver) -> Vec { let mut successors = Vec::new(); let player_reachable_area = self.player_reachable_area(solver); - for crate_position in &self.crate_positions { + for crate_position in &self.box_positions { for push_direction in [ Direction::Up, Direction::Down, @@ -96,12 +99,12 @@ impl State { Direction::Right, ] { let mut new_crate_position = crate_position + &push_direction.into(); - if self.can_block_crate(&new_crate_position, solver) { + if self.can_block_crate(new_crate_position, solver) { continue; } let next_player_position = crate_position - &push_direction.into(); - if self.can_block_player(&next_player_position, solver) + if self.can_block_player(next_player_position, solver) || !player_reachable_area.contains(&next_player_position) { continue; @@ -109,7 +112,7 @@ impl State { let mut new_movements = self.movements.clone(); let path = find_path(&self.player_position, &next_player_position, |position| { - self.can_block_player(position, solver) + self.can_block_player(*position, solver) }) .unwrap(); new_movements.extend( @@ -124,23 +127,19 @@ impl State { (new_crate_position - &push_direction.into()), push_direction, )) { - if self.can_block_crate(&(new_crate_position + &push_direction.into()), solver) - { + if self.can_block_crate(new_crate_position + &push_direction.into(), solver) { break; } new_crate_position += &push_direction.into(); new_movements.push(Action::Push(push_direction)); } - let mut new_crate_positions = self.crate_positions.clone(); + let mut new_crate_positions = self.box_positions.clone(); new_crate_positions.remove(crate_position); new_crate_positions.insert(new_crate_position); // skip deadlocks - if !solver - .level - .get(&new_crate_position) - .intersects(Tile::Target) + if !solver.level[new_crate_position].intersects(Tiles::Goal) && Self::is_freeze_deadlock( &new_crate_position, &new_crate_positions, @@ -214,8 +213,8 @@ impl State { ]; // Checks if any immovable walls on the axis. - if solver.level.get(&neighbors[0]).intersects(Tile::Wall) - || solver.level.get(&neighbors[1]).intersects(Tile::Wall) + if solver.level[neighbors[0]].intersects(Tiles::Wall) + || solver.level[neighbors[1]].intersects(Tiles::Wall) { continue; } @@ -244,7 +243,7 @@ impl State { /// Calculates and returns the lower bound value for the current state. fn calculate_lower_bound(&self, solver: &Solver) -> usize { let mut sum: usize = 0; - for crate_position in &self.crate_positions { + for crate_position in &self.box_positions { match solver.lower_bounds().get(crate_position) { Some(lower_bound) => sum += lower_bound, None => return 10_000 - 1, @@ -254,31 +253,26 @@ impl State { } /// Checks if a position can block the player's movement. - fn can_block_player(&self, position: &Vector2, solver: &Solver) -> bool { - solver.level.get(position).intersects(Tile::Wall) || self.crate_positions.contains(position) + fn can_block_player(&self, position: Vector2, solver: &Solver) -> bool { + solver.level[position].intersects(Tiles::Wall) || self.box_positions.contains(&position) } /// Checks if a position can block a crate's movement. - fn can_block_crate(&self, position: &Vector2, solver: &Solver) -> bool { - solver - .level - .get(position) - .intersects(Tile::Wall | Tile::Deadlock) - || !solver.lower_bounds().contains_key(position) - || self.crate_positions.contains(position) + fn can_block_crate(&self, position: Vector2, solver: &Solver) -> bool { + solver.level[position].intersects(Tiles::Wall /* | Tiles::Deadlock */) + || !solver.lower_bounds().contains_key(&position) + || self.box_positions.contains(&position) } /// Returns the normalized player position based on reachable area. fn normalized_player_position(&self, solver: &Solver) -> Vector2 { - normalized_area(&self.player_reachable_area(solver)) + normalized_area(&self.player_reachable_area(solver)).unwrap() } /// Returns the reachable area for the player in the current state. fn player_reachable_area(&self, solver: &Solver) -> HashSet> { - solver - .level - .reachable_area(&self.player_position, |position| { - self.can_block_player(position, solver) - }) + reachable_area(self.player_position, |position| { + !self.can_block_player(position, solver) + }) } } diff --git a/src/systems/auto_move.rs b/src/systems/auto_move.rs index 7d677e0..e065efc 100644 --- a/src/systems/auto_move.rs +++ b/src/systems/auto_move.rs @@ -1,8 +1,10 @@ use bevy::prelude::*; use itertools::Itertools; +use soukoban::path_finding::reachable_area; +use soukoban::Tiles; use crate::components::*; -use crate::level::Tile; +use crate::crate_pushable_paths; use crate::resources::*; use crate::AppState; @@ -25,10 +27,10 @@ pub fn spawn_auto_move_marks( crate_position, paths, } => { - *paths = board.level.crate_pushable_paths(crate_position); + *paths = crate_pushable_paths(&board.level, crate_position); // spawn crate pushable marks - for crate_position in paths.keys().map(|state| state.crate_position).unique() { + for crate_position in paths.keys().map(|state| state.box_position).unique() { commands.spawn(( SpriteBundle { sprite: Sprite { @@ -59,14 +61,11 @@ pub fn spawn_auto_move_marks( .for_each(|(_, mut sprite)| sprite.color = HIGHLIGHT_COLOR); } AutoMoveState::Player => { - let mut reachable_area = - board - .level - .reachable_area(&board.level.player_position, |position| { - board.level.get(position).intersects(Tile::Wall) - || board.level.crate_positions.contains(position) - }); - reachable_area.remove(&board.level.player_position); + let mut reachable_area = reachable_area(board.level.player_position(), |position| { + !board.level[position].intersects(Tiles::Wall) + && !board.level.box_positions().contains(&position) + }); + reachable_area.remove(&board.level.player_position()); // spawn player movable marks for crate_position in reachable_area { diff --git a/src/systems/auto_solve.rs b/src/systems/auto_solve.rs index d426e75..6724921 100644 --- a/src/systems/auto_solve.rs +++ b/src/systems/auto_solve.rs @@ -174,11 +174,11 @@ pub fn update_tile_grid_position( ) { let board = &board.single().board; let mut player_grid_positions = player_grid_positions.single_mut(); - **player_grid_positions = board.level.player_position; + **player_grid_positions = board.level.player_position(); for (mut crate_grid_position, crate_position) in crate_grid_positions .iter_mut() - .zip(board.level.crate_positions.iter()) + .zip(board.level.box_positions().iter()) { **crate_grid_position = *crate_position; } diff --git a/src/systems/input.rs b/src/systems/input.rs index 2443e4b..d7c497b 100644 --- a/src/systems/input.rs +++ b/src/systems/input.rs @@ -1,13 +1,15 @@ use std::collections::HashMap; +use std::fs; use bevy::input::mouse::MouseMotion; use bevy::prelude::*; use bevy::window::WindowMode; use leafwing_input_manager::prelude::*; use nalgebra::Vector2; +use soukoban::{Level, Tiles}; use crate::events::*; -use crate::level::{Level, PushState, Tile}; +use crate::level::PushState; use crate::resources::*; use crate::solve::solver::*; use crate::systems::level::*; @@ -102,11 +104,8 @@ fn player_move_to( player_movement: &mut PlayerMovement, board: &crate::board::Board, ) { - if let Some(path) = find_path(&board.level.player_position, target, |position| { - board - .level - .get(position) - .intersects(Tile::Wall | Tile::Crate) + if let Some(path) = find_path(&board.level.player_position(), target, |position| { + board.level[*position].intersects(Tiles::Wall | Tiles::Box) }) { let directions = path .windows(2) @@ -134,11 +133,8 @@ fn instant_player_move_to( board_clone: &mut crate::board::Board, player_movement: &mut PlayerMovement, ) { - if let Some(path) = find_path(&board_clone.level.player_position, target, |position| { - board_clone - .level - .get(position) - .intersects(Tile::Wall | Tile::Crate) + if let Some(path) = find_path(&board_clone.level.player_position(), target, |position| { + board_clone.level[*position].intersects(Tiles::Wall | Tiles::Box) }) { let directions = path .windows(2) @@ -314,14 +310,14 @@ pub fn mouse_input( match state.get() { AppState::Main => { - if board.level.crate_positions.contains(&grid_position) { + if board.level.box_positions().contains(&grid_position) { *auto_move_state = AutoMoveState::Crate { crate_position: grid_position, paths: HashMap::new(), }; next_state.set(AppState::AutoMove); return; - } else if board.level.player_position == grid_position { + } else if board.level.player_position() == grid_position { *auto_move_state = AutoMoveState::Player; next_state.set(AppState::AutoMove); return; @@ -342,7 +338,7 @@ pub fn mouse_input( ] { let push_state = PushState { push_direction, - crate_position: grid_position, + box_position: grid_position, }; if paths.contains_key(&push_state) { if *crate_position == grid_position { @@ -374,7 +370,7 @@ pub fn mouse_input( ); } } else if grid_position != *crate_position - && board.level.crate_positions.contains(&grid_position) + && board.level.box_positions().contains(&grid_position) { // crate_position = grid_position; // FIXME: Re-entering AppState::AutoCratePush https://github.com/bevyengine/bevy/issues/9130 @@ -441,7 +437,9 @@ pub fn file_drag_and_drop( if let FileDragAndDrop::DroppedFile { path_buf, .. } = event { let database = database.lock().unwrap(); info!("Load levels from file {:?}", path_buf); - match Level::load_from_file(path_buf) { + match Level::load_from_string(&fs::read_to_string(path_buf).unwrap()) + .collect::, _>>() + { Ok(levels) => { info!("Done, {} levels loaded", levels.len()); database.import_levels(&levels); diff --git a/src/systems/level.rs b/src/systems/level.rs index 4c97494..b5f24ca 100644 --- a/src/systems/level.rs +++ b/src/systems/level.rs @@ -1,12 +1,12 @@ use arboard::Clipboard; use bevy::prelude::*; use nalgebra::Vector2; +use soukoban::Level; +use soukoban::Tiles; use crate::board; use crate::components::*; use crate::database; -use crate::level::Level; -use crate::level::Tile; use crate::resources::*; use std::collections::HashMap; @@ -25,7 +25,9 @@ pub fn setup_database(mut commands: Commands) { continue; } info!(" {:?}", path); - let levels = Level::load_from_file(&path).unwrap(); + let levels: Vec<_> = Level::load_from_string(&fs::read_to_string(path).unwrap()) + .filter_map(Result::ok) + .collect(); database.import_levels(&levels); } info!("Done"); @@ -99,21 +101,21 @@ pub fn spawn_board( for y in 0..level.dimensions().y { for x in 0..level.dimensions().x { let position = Vector2::::new(x, y); - if level.get(&position) == Tile::Void { + if level[position].is_empty() { continue; } let tiles = HashMap::from([ - (Tile::Floor, (0, 0.0)), - (Tile::Wall, (3, 1.0)), - (Tile::Crate, (1, 2.0)), - (Tile::Target, (2, 3.0)), - (Tile::Player, (0, 4.0)), + (Tiles::Floor, (0, 0.0)), + (Tiles::Wall, (3, 1.0)), + (Tiles::Box, (1, 2.0)), + (Tiles::Goal, (2, 3.0)), + (Tiles::Player, (0, 4.0)), ]); for (tile, (sprite_index, z_order)) in tiles.into_iter() { - if level.get(&position).intersects(tile) { + if level[position].intersects(tile) { let mut sprite = Sprite::default(); if config.even_square_shades > 0.0 - && tile == Tile::Floor + && tile == Tiles::Floor && (x + y) % 2 == 0 { sprite.color = Color::WHITE * (1.0 - config.even_square_shades); @@ -131,9 +133,9 @@ pub fn spawn_board( }, GridPosition(position), )); - if tile == Tile::Player { + if tile == Tiles::Player { entity.insert((Player, AnimationState::default())); - } else if tile == Tile::Crate { + } else if tile == Tiles::Box { entity.insert(Crate); } } @@ -167,7 +169,7 @@ pub fn auto_switch_to_next_unsolved_level( /// Imports levels from the system clipboard. pub fn import_from_clipboard(level_id: &mut LevelId, database: &database::Database) { let mut clipboard = Clipboard::new().unwrap(); - match Level::load_from_memory(clipboard.get_text().unwrap()) { + match Level::load_from_string(&clipboard.get_text().unwrap()).collect::, _>>() { Ok(levels) => { if levels.is_empty() { error!("failed to import any level from clipboard"); @@ -183,9 +185,7 @@ pub fn import_from_clipboard(level_id: &mut LevelId, database: &database::Databa pub fn export_to_clipboard(board: &crate::board::Board) { let mut clipboard = Clipboard::new().unwrap(); - clipboard - .set_text(board.level.export_map() + &board.level.export_metadata()) - .unwrap(); + clipboard.set_text(board.level.to_string()).unwrap(); } /// Switches to the next unsolved level based on the current level ID. diff --git a/src/systems/render.rs b/src/systems/render.rs index 52f9f08..be913f3 100644 --- a/src/systems/render.rs +++ b/src/systems/render.rs @@ -104,14 +104,14 @@ pub fn handle_player_movement( if let Some(direction) = player_movement.directions.pop_back() { let occupied_targets_count = board .level - .target_positions - .intersection(&board.level.crate_positions) + .goal_positions() + .intersection(board.level.box_positions()) .count(); board.move_or_push(direction); let new_occupied_targets_count = board .level - .target_positions - .intersection(&board.level.crate_positions) + .goal_positions() + .intersection(board.level.box_positions()) .count(); match new_occupied_targets_count.cmp(&occupied_targets_count) { Ordering::Greater => drop(crate_enter_target_events.send_default()), @@ -219,24 +219,24 @@ pub fn update_grid_position_from_board( let board = &board.single().board; let player_grid_position = &mut player.single_mut().0; - player_grid_position.x = board.level.player_position.x; - player_grid_position.y = board.level.player_position.y; + player_grid_position.x = board.level.player_position().x; + player_grid_position.y = board.level.player_position().y; let crate_grid_positions: HashSet<_> = crates.iter().map(|x| x.0).collect(); debug_assert!( crate_grid_positions - .difference(&board.level.crate_positions) + .difference(board.level.box_positions()) .count() <= 1 ); if let Some(old_position) = crate_grid_positions - .difference(&board.level.crate_positions) + .difference(board.level.box_positions()) .collect::>() .first() { let new_position = *board .level - .crate_positions + .box_positions() .difference(&crate_grid_positions) .collect::>() .first() diff --git a/src/test.rs b/src/test.rs index ddc5c29..6356253 100644 --- a/src/test.rs +++ b/src/test.rs @@ -4,21 +4,13 @@ mod tests { // use super::test::Bencher; use crate::board::Board; - use crate::level::Level; use crate::solve::solver::*; - use std::fs; + use soukoban::Level; use std::ops::RangeBounds; use std::time::Duration; #[cfg(not(debug_assertions))] - use std::path::Path; - - #[test] - fn load_levels_from_file() { - for path in fs::read_dir("assets/levels/").unwrap() { - assert!(Level::load_from_file(&path.unwrap().path()).is_ok()); - } - } + use std::fs; #[allow(dead_code)] fn solve + IntoIterator>( @@ -38,7 +30,7 @@ mod tests { Solver::new(level.clone(), Strategy::Fast, LowerBoundMethod::MinimumMove); let solution = solver.search(Duration::from_secs(time_limit)); if solution.is_err() { - println!("{}", level.export_map()); + println!("{}", level.map()); println!("{:?}\n\n", solution.clone().err()); failed += 1; continue; @@ -57,7 +49,11 @@ mod tests { #[test] #[cfg(not(debug_assertions))] fn solve_microban_2() { - let levels = Level::load_from_file(Path::new("assets/levels/microban_II_135.xsb")).unwrap(); + let levels = Level::load_from_string( + &fs::read_to_string("assets/levels/microban_II_135.xsb").unwrap(), + ) + .collect::, _>>() + .unwrap(); assert!( solve( &levels, @@ -74,7 +70,10 @@ mod tests { #[test] #[cfg(not(debug_assertions))] fn solve_microban() { - let levels = Level::load_from_file(Path::new("assets/levels/microban_155.xsb")).unwrap(); + let levels = + Level::load_from_string(&fs::read_to_string("assets/levels/microban_155.xsb").unwrap()) + .collect::, _>>() + .unwrap(); assert!( solve( &levels, @@ -88,7 +87,11 @@ mod tests { #[test] #[cfg(not(debug_assertions))] fn solve_box_world() { - let levels = Level::load_from_file(Path::new("assets/levels/box_world_100.xsb")).unwrap(); + let levels = Level::load_from_string( + &fs::read_to_string("assets/levels/box_world_100.xsb").unwrap(), + ) + .collect::, _>>() + .unwrap(); assert!( solve( &levels,