Skip to content

Commit

Permalink
refactor(board): replace Level with Map in Board structure
Browse files Browse the repository at this point in the history
- Remove Level from Board and use Map directly
- Update related systems and modules to use Map instead of Level
- Adjust function names and logic to reflect the new structure
- Improve code readability and reduce indirection
  • Loading branch information
ShenMian committed Jan 20, 2025
1 parent 3bffe32 commit 8125389
Show file tree
Hide file tree
Showing 11 changed files with 130 additions and 146 deletions.
48 changes: 23 additions & 25 deletions src/board.rs
Original file line number Diff line number Diff line change
@@ -1,58 +1,57 @@
use soukoban::{direction::Direction, Action, Actions, Level, Tiles};
use soukoban::{direction::Direction, Action, Actions, Map, Tiles};

#[derive(Clone)]
pub struct Board {
pub level: Level,
pub map: Map,
actions: Actions,
undone_actions: Actions,
}

impl Board {
/// Creates a new board with the specified level.
pub fn with_level(level: Level) -> Self {
pub fn with_map(map: Map) -> Self {
Self {
level,
map,
actions: Actions::new(),
undone_actions: Actions::new(),
}
}

/// Checks if the player can move or push in the specified direction.
pub fn moveable(&self, direction: Direction) -> bool {
let map = self.level.map();
let player_next_position = map.player_position() + &direction.into();
if map[player_next_position].intersects(Tiles::Wall) {
let player_next_position = self.map.player_position() + &direction.into();
if self.map[player_next_position].intersects(Tiles::Wall) {
return false;
}
if map[player_next_position].intersects(Tiles::Box) {
if self.map[player_next_position].intersects(Tiles::Box) {
let box_next_position = player_next_position + &direction.into();
if map[box_next_position].intersects(Tiles::Box | Tiles::Wall) {
if self.map[box_next_position].intersects(Tiles::Box | Tiles::Wall) {
return false;
}
}
true
}

/// Moves the player or pushes a box in the specified direction.
pub fn move_or_push(&mut self, direction: Direction) {
let map = self.level.map_mut();
pub fn do_action(&mut self, direction: Direction) {
let direction_vector = &direction.into();
let player_next_position = map.player_position() + direction_vector;
if map[player_next_position].intersects(Tiles::Wall) {
let player_next_position = self.map.player_position() + direction_vector;
if self.map[player_next_position].intersects(Tiles::Wall) {
return;
}
if map[player_next_position].intersects(Tiles::Box) {
if self.map[player_next_position].intersects(Tiles::Box) {
let box_next_position = player_next_position + direction_vector;
if map[box_next_position].intersects(Tiles::Wall | Tiles::Box) {
if self.map[box_next_position].intersects(Tiles::Wall | Tiles::Box) {
return;
}
map.set_box_position(player_next_position, box_next_position);
self.map
.set_box_position(player_next_position, box_next_position);

self.actions.push(Action::Push(direction));
} else {
self.actions.push(Action::Move(direction));
}
map.set_player_position(player_next_position);
self.map.set_player_position(player_next_position);
self.undone_actions.clear();
}

Expand All @@ -70,16 +69,15 @@ impl Board {
/// Undoes the last move.
pub fn undo_move(&mut self) {
debug_assert!(!self.actions.is_empty());
let map = self.level.map_mut();
let history = self.actions.pop().unwrap();
let direction = history.direction();
if history.is_push() {
let box_position = map.player_position() + &direction.into();
let player_position = map.player_position();
map.set_box_position(box_position, player_position);
let box_position = self.map.player_position() + &direction.into();
let player_position = self.map.player_position();
self.map.set_box_position(box_position, player_position);
}
let player_prev_position = map.player_position() - &direction.into();
map.set_player_position(player_prev_position);
let player_prev_position = self.map.player_position() - &direction.into();
self.map.set_player_position(player_prev_position);
self.undone_actions.push(history);
}

Expand All @@ -99,13 +97,13 @@ impl Board {
debug_assert!(!self.undone_actions.is_empty());
let history = self.undone_actions.pop().unwrap();
let undone_actions = self.undone_actions.clone();
self.move_or_push(history.direction());
self.do_action(history.direction());
self.undone_actions = undone_actions;
}

/// Checks if the level is solved.
pub fn is_solved(&self) -> bool {
self.level.map().box_positions() == self.level.map().goal_positions()
self.map.box_positions() == self.map.goal_positions()
}

pub fn actions(&self) -> &Actions {
Expand Down
8 changes: 3 additions & 5 deletions src/resources.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use bevy::{prelude::*, time::Stopwatch};
use nalgebra::Vector2;
use serde::{Deserialize, Serialize};
use soukoban::{direction::Direction, Level, Map};
use soukoban::{direction::Direction, Map};

use crate::{board::Board, database, solve::solver::*, utils::PushState};

Expand Down Expand Up @@ -86,14 +86,12 @@ impl Default for SolverState {
fn default() -> Self {
Self {
solver: Mutex::new(Solver::new(
Level::from_map(Map::with_dimensions(Vector2::new(0, 0))),
Map::with_dimensions(Vector2::new(0, 0)),
Strategy::default(),
LowerBoundMethod::default(),
)),
stopwatch: Stopwatch::new(),
origin_board: Board::with_level(Level::from_map(Map::with_dimensions(Vector2::new(
0, 0,
)))),
origin_board: Board::with_map(Map::with_dimensions(Vector2::new(0, 0))),
}
}
}
91 changes: 43 additions & 48 deletions src/solve/solver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::{box_pushable_paths_with_positions, solve::state::*};
use itertools::Itertools;
use nalgebra::Vector2;
use serde::{Deserialize, Serialize};
use soukoban::{direction::Direction, path_finding::reachable_area, Actions, Level, Tiles};
use soukoban::{direction::Direction, path_finding::reachable_area, Actions, Map, Tiles};

#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
pub enum Strategy {
Expand Down Expand Up @@ -41,7 +41,7 @@ pub enum LowerBoundMethod {
}

pub struct Solver {
pub level: Level,
pub map: Map,
strategy: Strategy,
lower_bound_method: LowerBoundMethod,
lower_bounds: OnceCell<HashMap<Vector2<i32>, usize>>,
Expand All @@ -60,9 +60,9 @@ type Result<T> = std::result::Result<T, SolveError>;

impl Solver {
/// Creates a new solver.
pub fn new(level: Level, strategy: Strategy, lower_bound_method: LowerBoundMethod) -> Self {
pub fn new(map: Map, strategy: Strategy, lower_bound_method: LowerBoundMethod) -> Self {
let mut instance = Self {
level,
map,
strategy,
lower_bound_method,
lower_bounds: OnceCell::new(),
Expand All @@ -71,8 +71,8 @@ impl Solver {
heap: BinaryHeap::new(),
};
instance.heap.push(State::new(
instance.level.map().player_position(),
instance.level.map().box_positions().clone(),
instance.map.player_position(),
instance.map.box_positions().clone(),
Actions::new(),
&instance,
));
Expand Down Expand Up @@ -121,12 +121,11 @@ impl Solver {

/// Calculates and returns the set of tunnels in the level.
fn calculate_tunnels(&self) -> HashSet<(Vector2<i32>, Direction)> {
let map = self.level.map();
let mut tunnels = HashSet::new();
for x in 1..map.dimensions().x - 1 {
for y in 1..map.dimensions().y - 1 {
for x in 1..self.map.dimensions().x - 1 {
for y in 1..self.map.dimensions().y - 1 {
let box_position = Vector2::new(x, y);
if !map[box_position].intersects(Tiles::Floor) {
if !self.map[box_position].intersects(Tiles::Floor) {
continue;
}

Expand All @@ -147,19 +146,19 @@ impl Solver {
// . . .
// #$# or #$_ or _$#
// #@# #@# #@#
if map[player_position + &left.into()].intersects(Tiles::Wall)
&& map[player_position + &right.into()].intersects(Tiles::Wall)
&& (map[box_position + &left.into()].intersects(Tiles::Wall)
&& map[box_position + &right.into()].intersects(Tiles::Wall)
|| map[box_position + &right.into()].intersects(Tiles::Wall)
&& map[box_position + &left.into()].intersects(Tiles::Floor)
|| map[box_position + &right.into()].intersects(Tiles::Floor)
&& map[box_position + &left.into()].intersects(Tiles::Wall))
&& map[box_position].intersects(Tiles::Floor)
if self.map[player_position + &left.into()].intersects(Tiles::Wall)
&& self.map[player_position + &right.into()].intersects(Tiles::Wall)
&& (self.map[box_position + &left.into()].intersects(Tiles::Wall)
&& self.map[box_position + &right.into()].intersects(Tiles::Wall)
|| self.map[box_position + &right.into()].intersects(Tiles::Wall)
&& self.map[box_position + &left.into()].intersects(Tiles::Floor)
|| self.map[box_position + &right.into()].intersects(Tiles::Floor)
&& self.map[box_position + &left.into()].intersects(Tiles::Wall))
&& self.map[box_position].intersects(Tiles::Floor)
&& self
.lower_bounds()
.contains_key(&(box_position + &up.into()))
&& !map[box_position].intersects(Tiles::Goal)
&& !self.map[box_position].intersects(Tiles::Goal)
{
tunnels.insert((player_position, up));
}
Expand Down Expand Up @@ -187,8 +186,7 @@ impl Solver {
/// Calculates and returns the lower bounds using the minimum push method.
fn minimum_push_lower_bounds(&self) -> HashMap<Vector2<i32>, usize> {
let mut lower_bounds = HashMap::new();
let map = self.level.map();
for goal_position in map.goal_positions() {
for goal_position in self.map.goal_positions() {
lower_bounds.insert(*goal_position, 0);
let mut player_position = None;
for pull_direction in [
Expand All @@ -199,9 +197,9 @@ impl Solver {
] {
let next_box_position = goal_position + &pull_direction.into();
let next_player_position = next_box_position + &pull_direction.into();
if map.in_bounds(next_player_position)
&& !map[next_player_position].intersects(Tiles::Wall)
&& !map[next_box_position].intersects(Tiles::Wall)
if self.map.in_bounds(next_player_position)
&& !self.map[next_player_position].intersects(Tiles::Wall)
&& !self.map[next_box_position].intersects(Tiles::Wall)
{
player_position = Some(next_player_position);
break;
Expand All @@ -228,9 +226,8 @@ impl Solver {
lower_bounds: &mut HashMap<Vector2<i32>, usize>,
visited: &mut HashSet<(Vector2<i32>, Direction)>,
) {
let map = self.level.map();
let player_reachable_area = reachable_area(player_position, |position| {
!map[position].intersects(Tiles::Wall) && position != box_position
!self.map[position].intersects(Tiles::Wall) && position != box_position
});
for pull_direction in [
Direction::Up,
Expand All @@ -239,13 +236,13 @@ impl Solver {
Direction::Left,
] {
let next_box_position = box_position + &pull_direction.into();
if map[next_box_position].intersects(Tiles::Wall) {
if self.map[next_box_position].intersects(Tiles::Wall) {
continue;
}

let next_player_position = next_box_position + &pull_direction.into();
if !map.in_bounds(next_player_position)
|| map[next_player_position].intersects(Tiles::Wall)
if !self.map.in_bounds(next_player_position)
|| self.map[next_player_position].intersects(Tiles::Wall)
{
continue;
}
Expand Down Expand Up @@ -273,27 +270,26 @@ impl Solver {
/// Calculates and returns the lower bounds using the minimum move method.
fn minimum_move_lower_bounds(&self) -> HashMap<Vector2<i32>, usize> {
let mut lower_bounds = HashMap::new();
let map = self.level.map();
for x in 1..map.dimensions().x - 1 {
for y in 1..map.dimensions().y - 1 {
for x in 1..self.map.dimensions().x - 1 {
for y in 1..self.map.dimensions().y - 1 {
let position = Vector2::new(x, y);
// There may be situations in the level where the box is
// already on the goal and cannot be reached by the player.
if map[position].intersects(Tiles::Goal) {
if self.map[position].intersects(Tiles::Goal) {
lower_bounds.insert(position, 0);
continue;
}
if !map[position].intersects(Tiles::Floor)
// || map[position].intersects(Tiles::Deadlock)
if !self.map[position].intersects(Tiles::Floor)
// || self.map[position].intersects(Tiles::Deadlock)
{
continue;
}

let paths =
box_pushable_paths_with_positions(&self.level, &position, &HashSet::new());
box_pushable_paths_with_positions(&self.map, &position, &HashSet::new());
if let Some(lower_bound) = paths
.iter()
.filter(|path| map[path.0.box_position].intersects(Tiles::Goal))
.filter(|path| self.map[path.0.box_position].intersects(Tiles::Goal))
.map(|path| path.1.len() - 1)
.min()
{
Expand All @@ -307,22 +303,22 @@ impl Solver {
/// Calculates and returns the lower bounds using the Manhattan distance method.
fn manhattan_distance_lower_bounds(&self) -> HashMap<Vector2<i32>, usize> {
let mut lower_bounds = HashMap::new();
let map = self.level.map();
for x in 1..map.dimensions().x - 1 {
for y in 1..map.dimensions().y - 1 {
for x in 1..self.map.dimensions().x - 1 {
for y in 1..self.map.dimensions().y - 1 {
let position = Vector2::new(x, y);
// There may be situations in the level where the box is
// already on the goal and cannot be reached by the player.
if map[position].intersects(Tiles::Goal) {
if self.map[position].intersects(Tiles::Goal) {
lower_bounds.insert(position, 0);
continue;
}
if !map[position].intersects(Tiles::Floor)
// || map.get(&position).intersects(Tiles::Deadlock)
if !self.map[position].intersects(Tiles::Floor)
// || self.map.get(&position).intersects(Tiles::Deadlock)
{
continue;
}
let lower_bound = map
let lower_bound = self
.map
.goal_positions()
.iter()
.map(|box_pos| manhattan_distance(box_pos, &position))
Expand Down Expand Up @@ -351,9 +347,8 @@ impl Solver {
/// Prints the lower bounds for each position in the level.
#[expect(dead_code)]
pub fn print_lower_bounds(&self) {
let map = self.level.map();
for y in 0..map.dimensions().y {
for x in 0..map.dimensions().x {
for y in 0..self.map.dimensions().y {
for x in 0..self.map.dimensions().x {
let position = Vector2::new(x, y);
if let Some(lower_bound) = self.lower_bounds().get(&position) {
print!("{:3} ", lower_bound);
Expand Down
9 changes: 4 additions & 5 deletions src/solve/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,9 @@ impl State {
new_box_positions.insert(new_box_position);

// skip deadlocks
if !solver.level.map()[new_box_position].intersects(Tiles::Goal)
if !solver.map[new_box_position].intersects(Tiles::Goal)
&& deadlock::is_freeze_deadlock(
solver.level.map(),
&solver.map,
new_box_position,
&new_box_positions,
&mut HashSet::new(),
Expand Down Expand Up @@ -205,13 +205,12 @@ impl State {

/// Checks if a position can block the player's movement.
fn can_block_player(&self, position: Vector2<i32>, solver: &Solver) -> bool {
solver.level.map()[position].intersects(Tiles::Wall)
|| self.box_positions.contains(&position)
solver.map[position].intersects(Tiles::Wall) || self.box_positions.contains(&position)
}

/// Checks if a position can block a box's movement.
fn can_block_box(&self, position: Vector2<i32>, solver: &Solver) -> bool {
solver.level.map()[position].intersects(Tiles::Wall /* | Tiles::Deadlock */)
solver.map[position].intersects(Tiles::Wall /* | Tiles::Deadlock */)
|| !solver.lower_bounds().contains_key(&position)
|| self.box_positions.contains(&position)
}
Expand Down
4 changes: 2 additions & 2 deletions src/systems/auto_move.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pub fn spawn_auto_move_marks(
mut next_state: ResMut<NextState<AppState>>,
) {
let Board { board, tile_size } = board.single();
let map = board.level.map();
let map = &board.map;

const MARK_COLOR: Srgba = LIME;
const HIGHLIGHT_COLOR: Srgba = TURQUOISE;
Expand All @@ -24,7 +24,7 @@ pub fn spawn_auto_move_marks(
position: box_position,
paths,
} => {
*paths = box_pushable_paths(&board.level, box_position);
*paths = box_pushable_paths(map, box_position);

// remove static deadlock positions
let static_deadlocks = calculate_static_deadlocks(map);
Expand Down
Loading

0 comments on commit 8125389

Please sign in to comment.