Skip to content

Commit

Permalink
feat(solver): add new solver lower bound method MinimumPush
Browse files Browse the repository at this point in the history
  • Loading branch information
ShenMian committed Jan 22, 2024
1 parent e4bd58b commit d9dc8ce
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 81 deletions.
8 changes: 4 additions & 4 deletions docs/solver.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ The solver can automatically solve simple levels.

## Lower bound calculation method

- `PushCount`: Minimum push count to nearest target.
- `MoveCount`: Minimum move count to nearest target.
- `ManhattanDistance`: Manhattan distance to nearest target.
- `MinimumPush`: Minimum push count to nearest target.
- `MinimumMove`: Minimum move count to nearest target. (Slow, especially on maps with many crates or large areas)
- `ManhattanDistance`: Manhattan distance to nearest target. (Fast, suitable for maps with many crates or large areas)

## Optimization

Expand All @@ -31,4 +31,4 @@ Supports visualization of the automatic solution process. This feature can be us
- Displays the optimal state obtained by the current solver.
- Display the lower bound as a heat map.

![Visualization](solver_visualization.png)
<p align="center"><img src="solver_visualization.png" width=70%></p>
2 changes: 1 addition & 1 deletion src/resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ impl Default for SolverSettings {
fn default() -> Self {
Self {
strategy: Strategy::Fast,
lower_bound_method: LowerBoundMethod::PushCount,
lower_bound_method: LowerBoundMethod::MinimumMove,
}
}
}
Expand Down
169 changes: 112 additions & 57 deletions src/solver/solver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ pub enum Strategy {
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub enum LowerBoundMethod {
/// Minimum push count to nearest target
PushCount,
MinimumPush,

/// Minimum move count to nearest target
MoveCount,
MinimumMove,

/// Manhattan distance to nearest target
ManhattanDistance,
Expand Down Expand Up @@ -66,7 +66,7 @@ impl Solver {
Self {
level,
strategy: Strategy::Fast,
lower_bound_method: LowerBoundMethod::PushCount,
lower_bound_method: LowerBoundMethod::MinimumMove,
lower_bounds: OnceCell::new(),
tunnels: OnceCell::new(),
visited: HashSet::new(),
Expand Down Expand Up @@ -116,61 +116,73 @@ impl Solver {
self.strategy
}

pub fn best_state(&self) -> Option<&State> {
self.heap.peek()
}

pub fn lower_bounds(&self) -> &HashMap<Vector2<i32>, usize> {
self.lower_bounds
.get_or_init(|| self.calculate_lower_bounds())
}

pub fn best_state(&self) -> Option<&State> {
self.heap.peek()
fn calculate_lower_bounds(&self) -> HashMap<Vector2<i32>, usize> {
match self.lower_bound_method {
LowerBoundMethod::MinimumPush => self.minimum_push_lower_bounds(),
LowerBoundMethod::MinimumMove => self.minimum_move_lower_bounds(),
LowerBoundMethod::ManhattanDistance => self.manhattan_distance_lower_bounds(),
}
}

fn minimum_push_count_to_nearest_target(&self, position: &Vector2<i32>) -> Option<usize> {
// TODO: 优化求解器最小推动数计算方法
// 从目标开始逆推, 得到全部可达位置的下界.
// 若以存在下界, 取最小值. 若已经为更小的值, 停止搜索.

if self.level.target_positions.contains(position) {
return Some(0);
fn minimum_push_lower_bounds(&self) -> HashMap<Vector2<i32>, usize> {
let mut lower_bounds = HashMap::new();
for target_position in &self.level.target_positions {
lower_bounds.insert(*target_position, 0);
self.minimum_push_to(target_position, &mut lower_bounds);
}

let paths = self
.level
.crate_pushable_paths_with_crate_positions(position, &HashSet::new());

paths
.iter()
.filter(|path| self.level.target_positions.contains(&path.0.crate_position))
.map(|path| path.1.len() - 1)
.min()
lower_bounds
}

fn minimum_move_count_to_nearest_target(&self, position: &Vector2<i32>) -> Option<usize> {
let nearest_target_position = self
.level
.target_positions
.iter()
.min_by_key(|crate_pos| manhattan_distance(crate_pos, &position))
.unwrap();
let movements = find_path(&position, &nearest_target_position, |position| {
self.level.get_unchecked(&position).intersects(Tile::Wall)
})
.unwrap();
Some(movements.len() - 1)
}
fn minimum_push_to(
&self,
position: &Vector2<i32>,
lower_bounds: &mut HashMap<Vector2<i32>, usize>,
) {
for direction in [
Direction::Up,
Direction::Right,
Direction::Down,
Direction::Left,
] {
let crate_position = position + direction.to_vector();
if self
.level
.get_unchecked(&crate_position)
.intersects(Tile::Wall)
{
continue;
}

fn manhattan_distance_to_nearest_target(&self, position: &Vector2<i32>) -> Option<usize> {
Some(
self.level
.target_positions
.iter()
.map(|crate_pos| manhattan_distance(crate_pos, &position))
.min()
.unwrap() as usize,
)
let player_position = crate_position + direction.to_vector();
if !self.level.in_bounds(&player_position)
|| self
.level
.get_unchecked(&player_position)
.intersects(Tile::Wall)
{
continue;
}

let lower_bound = *lower_bounds.get(&crate_position).unwrap_or(&usize::MAX);
let new_lower_bound = lower_bounds[&position] + 1;
if new_lower_bound > lower_bound {
continue;
}
lower_bounds.insert(crate_position, new_lower_bound);
self.minimum_push_to(&crate_position, lower_bounds);
}
}

fn calculate_lower_bounds(&self) -> HashMap<Vector2<i32>, usize> {
fn minimum_move_lower_bounds(&self) -> HashMap<Vector2<i32>, usize> {
let mut lower_bounds = HashMap::new();
for x in 1..self.level.dimensions.x - 1 {
for y in 1..self.level.dimensions.y - 1 {
Expand All @@ -183,25 +195,53 @@ impl Solver {
{
continue;
}
let lower_bound = match self.lower_bound_method {
LowerBoundMethod::PushCount => {
self.minimum_push_count_to_nearest_target(&position)
}
LowerBoundMethod::MoveCount => {
self.minimum_move_count_to_nearest_target(&position)
}
LowerBoundMethod::ManhattanDistance => {
self.manhattan_distance_to_nearest_target(&position)
}
};
if let Some(lower_bound) = lower_bound {
if self.level.target_positions.contains(&position) {
lower_bounds.insert(position, 0);
continue;
}

let paths = self
.level
.crate_pushable_paths_with_crate_positions(&position, &HashSet::new());
if let Some(lower_bound) = paths
.iter()
.filter(|path| self.level.target_positions.contains(&path.0.crate_position))
.map(|path| path.1.len() - 1)
.min()
{
lower_bounds.insert(position, lower_bound);
}
}
}
lower_bounds
}

fn manhattan_distance_lower_bounds(&self) -> HashMap<Vector2<i32>, usize> {
let mut lower_bounds = HashMap::new();
for x in 1..self.level.dimensions.x - 1 {
for y in 1..self.level.dimensions.y - 1 {
let position = Vector2::new(x, y);
if !self.level.get_unchecked(&position).intersects(Tile::Floor)
|| self
.level
.get_unchecked(&position)
.intersects(Tile::Deadlock)
{
continue;
}
let lower_bound = self
.level
.target_positions
.iter()
.map(|crate_pos| manhattan_distance(crate_pos, &position))
.min()
.unwrap() as usize;
lower_bounds.insert(position, lower_bound);
}
}
lower_bounds
}

pub fn tunnels(&self) -> &HashSet<(Vector2<i32>, Direction)> {
self.tunnels.get_or_init(|| self.calculate_tunnels())
}
Expand Down Expand Up @@ -328,6 +368,21 @@ impl Solver {
}
}

#[allow(dead_code)]
pub fn print_lower_bounds(&self) {
for y in 0..self.level.dimensions.y {
for x in 0..self.level.dimensions.x {
let position = Vector2::new(x, y);
if let Some(lower_bound) = self.lower_bounds().get(&position) {
print!("{:3} ", lower_bound);
} else {
print!("{:3} ", "###");
}
}
println!();
}
}

#[allow(dead_code)]
fn print_info(visited: &HashSet<State>, heap: &BinaryHeap<State>, state: &State) {
print!(
Expand Down
36 changes: 18 additions & 18 deletions src/systems/auto_solve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,24 @@ use crate::AppState;

use std::time::{Duration, Instant};

pub fn setup_solver(
mut solver_state: ResMut<SolverState>,
board: Query<&Board>,
settings: Res<Settings>,
) {
let board = &board.single().board;
let SolverState {
solver,
level,
stopwatch,
} = &mut *solver_state;
*level = board.level.clone();
let mut solver = solver.lock().unwrap();
*solver = Solver::new(level.clone());
solver.initial(settings.solver.strategy, settings.solver.lower_bound_method);
stopwatch.reset();
}

pub fn spawn_lowerbound_marks(
solver_state: Res<SolverState>,
mut commands: Commands,
Expand Down Expand Up @@ -51,24 +69,6 @@ pub fn despawn_lowerbound_marks(
marks.for_each(|entity| commands.entity(entity).despawn());
}

pub fn setup_solver(
mut solver_state: ResMut<SolverState>,
board: Query<&Board>,
settings: Res<Settings>,
) {
let board = &board.single().board;
let SolverState {
solver,
level,
stopwatch,
} = &mut *solver_state;
*level = board.level.clone();
let mut solver = solver.lock().unwrap();
*solver = Solver::new(level.clone());
solver.initial(settings.solver.strategy, settings.solver.lower_bound_method);
stopwatch.reset();
}

pub fn reset_board(mut board: Query<&mut Board>, solver_state: Res<SolverState>) {
let board = &mut board.single_mut().board;
*board = crate::board::Board::with_level(solver_state.level.clone());
Expand Down
2 changes: 1 addition & 1 deletion src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ mod tests {
println!("#{} ({})", id + 1, id);
let level = levels[id].clone();
let mut solver = Solver::new(level.clone());
solver.initial(Strategy::Fast, LowerBoundMethod::PushCount);
solver.initial(Strategy::Fast, LowerBoundMethod::MinimumMove);
let solution = solver.solve(Duration::from_secs(time_limit));
if solution.is_err() {
println!("{}", level.export_map());
Expand Down

0 comments on commit d9dc8ce

Please sign in to comment.