From d5c84cba7cb37f35ec73542200f2bb5225f2ee8d Mon Sep 17 00:00:00 2001 From: ShenMian Date: Wed, 17 Jan 2024 17:31:47 +0800 Subject: [PATCH] feat: support visualization of automatic solution process --- src/main.rs | 32 ++++--- src/resources.rs | 4 +- src/solver/solver.rs | 6 +- src/systems/auto_crate_push.rs | 52 ++++++++++ src/systems/{solver.rs => auto_solve.rs} | 63 ++++++++++++- src/systems/input.rs | 115 +++++++++++------------ src/systems/mod.rs | 3 +- src/systems/render.rs | 7 ++ 8 files changed, 203 insertions(+), 79 deletions(-) create mode 100644 src/systems/auto_crate_push.rs rename src/systems/{solver.rs => auto_solve.rs} (61%) diff --git a/src/main.rs b/src/main.rs index c763ce4..c441026 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,10 +15,11 @@ mod level; use level::*; mod systems; +use systems::auto_crate_push::*; +use systems::auto_solve::*; use systems::input::*; use systems::level::*; use systems::render::*; -use systems::solver::*; use systems::ui::*; mod plugins; @@ -94,14 +95,7 @@ fn main() { .chain(), ), ) - .add_systems( - FixedUpdate, - ( - animate_tiles_movement, - animate_player_movement, - animate_camera_zoom, - ), - ); + .add_systems(FixedUpdate, animate_camera_zoom); app.add_systems( Update, @@ -118,6 +112,10 @@ fn main() { file_drag_and_drop, ) .run_if(in_state(AppState::Main)), + ) + .add_systems( + FixedUpdate, + (animate_player_movement, animate_tiles_movement).run_if(in_state(AppState::Main)), ); app.add_systems( @@ -127,8 +125,20 @@ fn main() { clear_action_state, ), ) - .add_systems(Update, update_solver.run_if(in_state(AppState::AutoSolve))) - .add_systems(OnExit(AppState::AutoSolve), despawn_lowerbound_marks) + .add_systems( + Update, + (update_solver, update_grid_position, move_tiles).run_if(in_state(AppState::AutoSolve)), + ) + .add_systems( + OnExit(AppState::AutoSolve), + ( + reset_board, + update_grid_position, + move_tiles, + despawn_lowerbound_marks, + ) + .chain(), + ) .insert_resource(SolverState::default()); app.add_systems(OnEnter(AppState::AutoCratePush), spawn_crate_pushable_marks) diff --git a/src/resources.rs b/src/resources.rs index f2a96aa..bd8b01a 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -22,7 +22,7 @@ impl Default for Settings { fn default() -> Self { Self { instant_move: false, - player_move_speed: 0.05, + player_move_speed: 0.1, solver: SolverSettings::default(), } } @@ -73,6 +73,7 @@ impl Default for AutoCratePushState { #[derive(Resource)] pub struct SolverState { pub solver: Mutex, + pub level: Level, pub stopwatch: Stopwatch, } @@ -80,6 +81,7 @@ impl Default for SolverState { fn default() -> Self { Self { solver: Mutex::new(Solver::new(Level::empty())), + level: Level::empty(), stopwatch: Stopwatch::new(), } } diff --git a/src/solver/solver.rs b/src/solver/solver.rs index ebac569..401d25f 100644 --- a/src/solver/solver.rs +++ b/src/solver/solver.rs @@ -90,7 +90,7 @@ impl Solver { } // Solver::shrink_heap(&mut self.heap); - Solver::print_info(&self.visited, &self.heap, &state); + // Solver::print_info(&self.visited, &self.heap, &state); for successor in state.successors(&self) { if self.visited.contains(&successor.normalized(&self)) { @@ -115,6 +115,10 @@ impl Solver { .get_or_init(|| self.calculate_lower_bounds()) } + pub fn best_state(&self) -> Option<&State> { + self.heap.peek() + } + fn minimum_push_count_to_nearest_target(&self, position: &Vector2) -> Option { if self.level.target_positions.contains(position) { return Some(0); diff --git a/src/systems/auto_crate_push.rs b/src/systems/auto_crate_push.rs new file mode 100644 index 0000000..16449e2 --- /dev/null +++ b/src/systems/auto_crate_push.rs @@ -0,0 +1,52 @@ +use bevy::prelude::*; +use itertools::Itertools; + +use crate::components::*; +use crate::resources::*; +use crate::AppState; + +pub fn spawn_crate_pushable_marks( + mut commands: Commands, + mut auto_crate_push_state: ResMut, + board: Query<&Board>, + mut next_state: ResMut>, +) { + let crate_position = &auto_crate_push_state.selected_crate; + let Board { board, tile_size } = board.single(); + + let paths = board.level.crate_pushable_paths(crate_position); + + // spawn crate pushable marks + for crate_position in paths.keys().map(|state| state.crate_position).unique() { + commands.spawn(( + SpriteBundle { + sprite: Sprite { + color: Color::GREEN.with_a(0.8), + custom_size: Some(Vec2::new(tile_size.x / 4.0, tile_size.y / 4.0)), + ..default() + }, + transform: Transform::from_xyz( + crate_position.x as f32 * tile_size.x, + (board.level.dimensions.y - crate_position.y) as f32 * tile_size.y, + 10.0, + ), + ..default() + }, + CratePushableMark, + )); + } + + if paths.is_empty() { + next_state.set(AppState::Main); + return; + } + + auto_crate_push_state.paths = paths; +} + +pub fn despawn_crate_pushable_marks( + mut commands: Commands, + marks: Query>, +) { + marks.for_each(|entity| commands.entity(entity).despawn()); +} diff --git a/src/systems/solver.rs b/src/systems/auto_solve.rs similarity index 61% rename from src/systems/solver.rs rename to src/systems/auto_solve.rs index 7bdc299..f0e8103 100644 --- a/src/systems/solver.rs +++ b/src/systems/auto_solve.rs @@ -54,13 +54,32 @@ pub fn setup_solver( settings: Res, ) { let board = &board.single().board; - let SolverState { solver, stopwatch } = &mut *solver_state; + let SolverState { + solver, + level, + stopwatch, + } = &mut *solver_state; + *level = board.level.clone(); let mut solver = solver.lock().unwrap(); - *solver = Solver::new(board.level.clone()); + *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) { + let board = &mut board.single_mut().board; + *board = crate::board::Board::with_level(solver_state.level.clone()); +} + +pub fn move_tiles(mut tiles: Query<(&mut Transform, &GridPosition)>, board: Query<&Board>) { + let Board { board, tile_size } = &board.single(); + for (mut transform, grid_position) in tiles.iter_mut() { + transform.translation.x = grid_position.x as f32 * tile_size.x; + transform.translation.y = + board.level.dimensions.y as f32 * tile_size.y - grid_position.y as f32 * tile_size.y; + } +} + pub fn update_solver( mut solver_state: ResMut, mut board: Query<&mut Board>, @@ -69,7 +88,13 @@ pub fn update_solver( mut next_state: ResMut>, ) { let board = &mut board.single_mut().board; - let SolverState { solver, stopwatch } = &mut *solver_state; + let SolverState { + solver, + level, + stopwatch, + } = &mut *solver_state; + + *board = crate::board::Board::with_level(level.clone()); let mut solver = solver.lock().unwrap(); let timeout = std::time::Duration::from_millis(50); @@ -95,9 +120,10 @@ pub fn update_solver( info!(" Solution: {}", solution.lurd()); for movement in &*solution { - player_move_or_push(movement.direction, board, &mut player_movement); + player_move_or_push(movement.direction, &mut player_movement); } next_state.set(AppState::Main); + return; } Err(SolveError::NoSolution) => { stopwatch.tick(timer.elapsed()); @@ -106,9 +132,38 @@ pub fn update_solver( stopwatch.elapsed().as_millis() as f32 / 1000.0 ); next_state.set(AppState::Main); + return; } Err(SolveError::Timeout) => { let _ = stopwatch.tick(timer.elapsed()); } } + if let Some(best_state) = solver.best_state() { + // println!( + // "lower bound: {:3}, moves: {:3}, pushes: {:3}", + // best_state.lower_bound(&solver), + // best_state.movements.move_count(), + // best_state.movements.push_count() + // ); + for movement in &*best_state.movements { + board.move_or_push(movement.direction); + } + } +} + +pub fn update_grid_position( + mut player_grid_positions: Query<&mut GridPosition, With>, + mut crate_grid_positions: Query<&mut GridPosition, (With, Without)>, + board: Query<&Board>, +) { + let board = &board.single().board; + let mut player_grid_positions = player_grid_positions.single_mut(); + **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()) + { + **crate_grid_position = *crate_position; + } } diff --git a/src/systems/input.rs b/src/systems/input.rs index 41229e9..9563c56 100644 --- a/src/systems/input.rs +++ b/src/systems/input.rs @@ -1,16 +1,16 @@ use bevy::input::mouse::{MouseMotion, MouseWheel}; use bevy::prelude::*; -use itertools::Itertools; use leafwing_input_manager::{prelude::*, user_input::InputKind}; use nalgebra::Vector2; +use crate::components::*; use crate::direction::Direction; use crate::events::*; use crate::level::{Level, PushState, Tile}; use crate::resources::*; use crate::solver::solver::*; use crate::systems::level::*; -use crate::{components::*, AppState}; +use crate::AppState; #[derive(Actionlike, Reflect, Clone, Hash, PartialEq, Eq)] pub enum Action { @@ -215,21 +215,44 @@ pub fn player_move_to( .windows(2) .map(|pos| Direction::from_vector(pos[1] - pos[0]).unwrap()); for direction in directions { - player_move_or_push(direction, board, player_movement); + player_move_or_push(direction, player_movement); } } } -pub fn player_move_or_push( - direction: Direction, +pub fn player_move_or_push(direction: Direction, player_movement: &mut PlayerMovement) { + player_movement.directions.push_front(direction); +} + +pub fn instant_player_move_to( + target: &Vector2, board: &mut crate::board::Board, player_movement: &mut PlayerMovement, ) { - if board.move_or_push(direction) { - player_movement.directions.push_front(direction); + if let Some(path) = find_path(&board.level.player_position, target, |position| { + board + .level + .get_unchecked(&position) + .intersects(Tile::Wall | Tile::Crate) + }) { + let directions = path + .windows(2) + .map(|pos| Direction::from_vector(pos[1] - pos[0]).unwrap()); + for direction in directions { + instant_player_move_or_push(direction, board, player_movement); + } } } +pub fn instant_player_move_or_push( + direction: Direction, + board: &mut crate::board::Board, + player_movement: &mut PlayerMovement, +) { + board.move_or_push(direction); + player_movement.directions.push_front(direction); +} + pub fn clear_action_state(mut action_state: ResMut>) { action_state.consume_all(); } @@ -248,26 +271,26 @@ pub fn handle_other_action( let board = &mut board.single_mut().board; if action_state.just_pressed(Action::MoveUp) { - player_move_or_push(Direction::Up, board, &mut player_movement); + player_move_or_push(Direction::Up, &mut player_movement); } if action_state.just_pressed(Action::MoveDown) { - player_move_or_push(Direction::Down, board, &mut player_movement); + player_move_or_push(Direction::Down, &mut player_movement); } if action_state.just_pressed(Action::MoveLeft) { - player_move_or_push(Direction::Left, board, &mut player_movement); + player_move_or_push(Direction::Left, &mut player_movement); } if action_state.just_pressed(Action::MoveRight) { - player_move_or_push(Direction::Right, board, &mut player_movement); + player_move_or_push(Direction::Right, &mut player_movement); } if action_state.just_pressed(Action::Undo) { - board.undo_push(); player_movement.directions.clear(); + board.undo_push(); update_grid_position_events.send(UpdateGridPositionEvent); } if action_state.just_pressed(Action::Redo) { - board.redo_push(); player_movement.directions.clear(); + board.redo_push(); update_grid_position_events.send(UpdateGridPositionEvent); } @@ -312,8 +335,10 @@ pub fn handle_automatic_solution_action( action_state: Res>, state: Res>, mut next_state: ResMut>, + mut player_movement: ResMut, ) { if action_state.just_pressed(Action::AutomaticSolution) { + player_movement.directions.clear(); if *state == AppState::Main { next_state.set(AppState::AutoSolve); } else { @@ -322,50 +347,6 @@ pub fn handle_automatic_solution_action( } } -pub fn spawn_crate_pushable_marks( - mut commands: Commands, - mut auto_crate_push_state: ResMut, - board: Query<&Board>, - mut next_state: ResMut>, -) { - let crate_position = &auto_crate_push_state.selected_crate; - let Board { board, tile_size } = board.single(); - - let paths = board.level.crate_pushable_paths(crate_position); - - // spawn crate pushable marks - for crate_position in paths.keys().map(|state| state.crate_position).unique() { - commands.spawn(( - SpriteBundle { - sprite: Sprite { - color: Color::GREEN.with_a(0.8), - custom_size: Some(Vec2::new(tile_size.x / 4.0, tile_size.y / 4.0)), - ..default() - }, - transform: Transform::from_xyz( - crate_position.x as f32 * tile_size.x, - (board.level.dimensions.y - crate_position.y) as f32 * tile_size.y, - 10.0, - ), - ..default() - }, - CratePushableMark, - )); - } - - if paths.is_empty() { - next_state.set(AppState::Main); - } - auto_crate_push_state.paths = paths; -} - -pub fn despawn_crate_pushable_marks( - mut commands: Commands, - marks: Query>, -) { - marks.for_each(|entity| commands.entity(entity).despawn()); -} - pub fn mouse_input_to_action( mut mouse_wheel_events: EventReader, mut action_state: ResMut>, @@ -409,7 +390,9 @@ pub fn mouse_input( match state.get() { AppState::Main => { - if board.level.crate_positions.contains(&grid_position) { + if player_movement.directions.is_empty() + && board.level.crate_positions.contains(&grid_position) + { auto_crate_push_state.selected_crate = grid_position; next_state.set(AppState::AutoCratePush); return; @@ -445,19 +428,29 @@ pub fn mouse_input( if let Some(min_crate_path) = crate_paths.iter().min_by_key(|crate_path| crate_path.len()) { + let mut board_clone = board.clone(); for (crate_position, push_direction) in min_crate_path .windows(2) .map(|pos| (pos[0], Direction::from_vector(pos[1] - pos[0]).unwrap())) { let player_position = crate_position - push_direction.to_vector(); - player_move_to(&player_position, board, &mut player_movement); - player_move_or_push(push_direction, board, &mut player_movement); + instant_player_move_to( + &player_position, + &mut board_clone, + &mut player_movement, + ); + instant_player_move_or_push( + push_direction, + &mut board_clone, + &mut player_movement, + ); } } else if grid_position != *selected_crate && board.level.crate_positions.contains(&grid_position) { - // FIXME: https://github.com/bevyengine/bevy/issues/9130 // auto_crate_push_state.selected_crate = grid_position; + // FIXME: https://github.com/bevyengine/bevy/issues/9130 + // Re-entering AppState::AutoCratePush // next_state.set(AppState::AutoCratePush); next_state.set(AppState::Main); return; diff --git a/src/systems/mod.rs b/src/systems/mod.rs index f33bd6f..6a6af4c 100644 --- a/src/systems/mod.rs +++ b/src/systems/mod.rs @@ -1,5 +1,6 @@ +pub mod auto_crate_push; +pub mod auto_solve; pub mod input; pub mod level; pub mod render; -pub mod solver; pub mod ui; diff --git a/src/systems/render.rs b/src/systems/render.rs index afc7fbd..a360fd5 100644 --- a/src/systems/render.rs +++ b/src/systems/render.rs @@ -34,10 +34,13 @@ pub fn setup_camera(mut commands: Commands) { pub fn animate_player_movement( mut player: Query<(&mut GridPosition, &mut TextureAtlasSprite), With>, mut crates: Query<&mut GridPosition, (With, Without)>, + mut board: Query<&mut Board>, mut player_movement: ResMut, time: Res