diff --git a/src/debug.rs b/src/debug.rs index 1588806..853de4f 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -222,7 +222,17 @@ fn selected_position( } fn debug_piece( - query: Query<(Entity, Option<&Side>, Option<&Piece>, Option<&BoardIndex>, Option<&display3d::Animating>, Option<&Selected>), With>, + query: Query< + ( + Entity, + Option<&Side>, + Option<&Piece>, + Option<&BoardIndex>, + Option<&display3d::Animating>, + Option<&Selected>, + ), + With, + >, pointer: Res, mut root: Query<(&mut Text, &mut Visibility), With>, // mut commands: Commands, @@ -239,11 +249,11 @@ fn debug_piece( *vis = Visibility::Inherited; }); } - }, + } _ => { root.iter_mut().for_each(|(_, mut vis)| { *vis = Visibility::Hidden; }); } } -} \ No newline at end of file +} diff --git a/src/display3d.rs b/src/display3d.rs index 2306f5e..cd8dadf 100644 --- a/src/display3d.rs +++ b/src/display3d.rs @@ -78,8 +78,9 @@ impl Plugin for Display3dPlugin { .run_if( in_state(debug::DebugState::Enabled) .and_then(on_event::()) - .or_else(just_pressed(MouseButton::Left)) - ).before(select), + .or_else(just_pressed(MouseButton::Left)), + ) + .before(select), select .run_if(in_state(GameState::Play)) .run_if(in_state(DisplayState::Display3d)) @@ -118,7 +119,7 @@ impl Plugin for Display3dPlugin { ( animate_title_light_in.run_if(in_state(DissolvingAnimation::In)), animate_title_light_out.run_if(in_state(DissolvingAnimation::Out)), - ) + ), ) .add_systems( OnEnter(GameState::Intro), @@ -133,11 +134,12 @@ impl Plugin for Display3dPlugin { ) .add_systems( Update, - setup_dissolve_materials.run_if(in_state(GameState::Intro)) + setup_dissolve_materials + .run_if(in_state(GameState::Intro)) .run_if( - any_component_added::>() - .or_else(any_component_changed::>()), - ), + any_component_added::>() + .or_else(any_component_changed::>()), + ), ) .add_systems( OnEnter(GameState::Play), @@ -186,7 +188,7 @@ enum DissolvingAnimation { #[default] None, In, - Out + Out, } #[derive(Debug, Resource, Clone)] @@ -487,7 +489,10 @@ fn set_board_model( } fn set_title_model( - mut titles: Query<(&mut Handle, &mut Transform, &mut Visibility), (Added, With)>, + mut titles: Query< + (&mut Handle, &mut Transform, &mut Visibility), + (Added, With), + >, gltfs: Res>, tweaks: Res>, tweaks_file: Res, @@ -495,29 +500,31 @@ fn set_title_model( let tweak = tweaks .get(tweaks_file.handle.clone()) .expect("Load tweakfile"); - titles.iter_mut().for_each(|(mut handle, mut transform, mut visibility)| { - info!("Setting title model"); - let assets_handle = tweak - .get_handle::("display3d_models_assets_file") - .unwrap(); - let gltf = gltfs.get(assets_handle).expect("Load GLTF content"); - *handle = gltf - .named_scenes - .get( - tweak - .get::("display3d_models_scenes_title") - .unwrap() - .as_str(), - ) - .expect("Game title model") - .clone(); + titles + .iter_mut() + .for_each(|(mut handle, mut transform, mut visibility)| { + info!("Setting title model"); + let assets_handle = tweak + .get_handle::("display3d_models_assets_file") + .unwrap(); + let gltf = gltfs.get(assets_handle).expect("Load GLTF content"); + *handle = gltf + .named_scenes + .get( + tweak + .get::("display3d_models_scenes_title") + .unwrap() + .as_str(), + ) + .expect("Game title model") + .clone(); - transform.translation -= Vec3::Y * 0.5; - transform.rotate_local_z(std::f32::consts::PI); - transform.rotate_local_y(std::f32::consts::PI / 2.0); + transform.translation -= Vec3::Y * 0.5; + transform.rotate_local_z(std::f32::consts::PI); + transform.rotate_local_y(std::f32::consts::PI / 2.0); - *visibility = Visibility::Hidden; - }); + *visibility = Visibility::Hidden; + }); } /// Given a board index returns the Vec3 location in space @@ -682,8 +689,8 @@ fn select( selections.send(game::Selection(**board_index)); } }); - }, - _ => () + } + _ => (), } } @@ -724,24 +731,21 @@ fn entity_pointer( }) .filter_map(|(entity, hit)| { // Find this entity in the set of selectable entities - selectable - .iter() - .find_map(|e| { - let hit_check = { - // This entity was hit (tile hitboxes) - let primary = entity == e; - - // A child was hit (pieces) - let secondary = children - .iter_descendants(e) - .any(|child| child == entity); - - primary || secondary - }; - - // Return the board index of this piece - hit_check.then_some((e, hit.clone())) - }) + selectable.iter().find_map(|e| { + let hit_check = { + // This entity was hit (tile hitboxes) + let primary = entity == e; + + // A child was hit (pieces) + let secondary = + children.iter_descendants(e).any(|child| child == entity); + + primary || secondary + }; + + // Return the board index of this piece + hit_check.then_some((e, hit.clone())) + }) }) // Compare the distance of all hits, choosing the closest one .min_by(|(_, hit_a), (_, hit_b)| { @@ -840,11 +844,12 @@ fn pick_up( children.iter_descendants(entity).for_each(|child| { info!(" Child: {:?}", child); if let Ok((name, mut player)) = players.get_mut(child) { - let pickup_animation = format!( - "display3d_models_animations_pick_up_{}", - name.as_str(), + let pickup_animation = + format!("display3d_models_animations_pick_up_{}", name.as_str(),); + info!( + "Picking up {:?} ({:?}) {:?} {:?}", + name, entity, piece, pickup_animation ); - info!("Picking up {:?} ({:?}) {:?} {:?}", name, entity, piece, pickup_animation); let pickup_handle = gltf .named_animations .get( @@ -854,10 +859,7 @@ fn pick_up( .as_str(), ) .expect("Pickup Animation"); - let idle_animation = format!( - "display3d_models_animations_idle_{}", - name.as_str() - ); + let idle_animation = format!("display3d_models_animations_idle_{}", name.as_str()); let idle_handle = gltf .named_animations .get( @@ -890,10 +892,7 @@ fn pick_up( fn put_down( mut events: RemovedComponents, - mut query: Query< - Entity, - (With, With, With), - >, + mut query: Query, With, With)>, gltfs: Res>, children: Query<&Children>, mut players: Query<(&Name, &mut AnimationPlayer)>, @@ -915,7 +914,8 @@ fn put_down( children.iter_descendants(entity).for_each(|child| { if let Ok((name, mut player)) = players.get_mut(child) { info!("Putting down {:?}", entity); - let putdown_animation = format!("display3d_models_animations_put_down_{}", name.as_str()); + let putdown_animation = + format!("display3d_models_animations_put_down_{}", name.as_str()); let putdown_handle = gltf .named_animations .get( @@ -935,7 +935,10 @@ fn put_down( ) .set_repeat(RepeatAnimation::Never); } else { - info!("Clip {:?}({:?}) not compatible with {:?}", putdown_animation, putdown_clip, name); + info!( + "Clip {:?}({:?}) not compatible with {:?}", + putdown_animation, putdown_clip, name + ); } } else { info!("Clip not found"); @@ -1481,4 +1484,4 @@ fn animate_title_light_out( } }) }); -} \ No newline at end of file +} diff --git a/src/game.rs b/src/game.rs index 1b40c79..bb2f754 100644 --- a/src/game.rs +++ b/src/game.rs @@ -118,6 +118,13 @@ impl Piece { .iter(), } } + + fn moves_at(&self, from: &BoardIndex) -> Vec { + self.moves().map(|(x, y)| { + *from + (*x, *y) + }) + .collect() + } } #[derive(Debug, Component, Clone, PartialEq)] @@ -280,12 +287,29 @@ impl std::ops::Add<(isize, isize)> for BoardIndex { fn add(self, (a, b): (isize, isize)) -> Self { BoardIndex { - x: self.x.saturating_add_signed(a), - y: self.y.saturating_add_signed(b), + x: self.x.saturating_add_signed(a).max(7), + y: self.y.saturating_add_signed(b).max(3), } } } +impl std::ops::Add<(usize, usize)> for BoardIndex { + type Output = BoardIndex; + + fn add(self, (a, b): (usize, usize)) -> Self { + BoardIndex { + x: self.x.saturating_add(a).max(7), + y: self.y.saturating_add(b).max(3), + } + } +} + +impl From<(usize, usize)> for BoardIndex { + fn from((x, y): (usize, usize)) -> BoardIndex { + BoardIndex { x, y } + } +} + #[derive(Debug)] pub(crate) enum MoveType { Valid, @@ -320,24 +344,33 @@ impl Board { fn from_ascii(art: &str) -> Board { use Piece::*; - let mut inner: Vec>> = vec![Vec::with_capacity(8), Vec::with_capacity(8), Vec::with_capacity(8), Vec::with_capacity(8)]; + let mut inner: Vec>> = vec![ + Vec::with_capacity(8), + Vec::with_capacity(8), + Vec::with_capacity(8), + Vec::with_capacity(8), + ]; let mut index = BoardIndex { x: 0, y: 3 }; art.chars().for_each(|c| { match c { - 'q' => { + 'q' | 'Q' => { inner[index.y].push(Some(Queen)); } - 'd' => { + 'd' | 'D' => { inner[index.y].push(Some(Drone)); - }, - 'p' => { + } + 'p' | 'P' => { inner[index.y].push(Some(Pawn)); - }, - '.' => { + } + '.' | '#' => { inner[index.y].push(None); - }, + } '\n' => { - assert_eq!(inner[index.y].len(), 8, "Each row must be 8 characters long!"); + assert_eq!( + inner[index.y].len(), + 8, + "Each row must be 8 characters long!" + ); index.y -= 1; } _ => { @@ -356,7 +389,8 @@ impl Board { r#".....dqq dpp..pdq qdp..ppd - qqd....."#) + qqd....."#, + ) } /// Show all pieces on one side of the board @@ -501,9 +535,10 @@ impl Board { ) -> Option { // Iterate over the piece's moves piece - .moves() + .moves_at(&from) + .iter() // Find if the given `to` move is one of those - .find(|(x, y)| to == from + (*x, *y)) + .find(|BoardIndex { x, y }| to == from + (*x, *y)) // Determine if this is valid/legal in this situation .and_then(|_| { let dest_at = self.at(to); @@ -584,17 +619,18 @@ impl Board { /// Returns the possible moves the piece at this tile can make. pub(crate) fn valid_moves(&self, current_board_index: BoardIndex) -> HashSet { - tiles() - .filter_map(|(board_index, _)| { + if let Some(piece) = self.at(current_board_index) { + piece.moves_at(¤t_board_index).iter().filter_map(|move_index| { // Get the move type (or none if totally invalid) - self.at(current_board_index).and_then(|piece| { - match self.move_type(*piece, current_board_index, board_index) { - None | Some(MoveType::Invalid) => None, - _ => Some(board_index), - } - }) + match self.move_type(*piece, current_board_index, *move_index) { + None | Some(MoveType::Invalid) => None, + _ => Some(*move_index), + } }) .collect() + } else { + HashSet::new() + } } pub(crate) fn current_epoch(&self) -> usize { @@ -606,14 +642,161 @@ mod test { use super::*; #[test] - fn test_01() { - let mut board = Board::new(); - print!("{}", board); - println!("{:?}", board.at(BoardIndex { x: 0, y: 0 })); - println!("{:?}", board.at(BoardIndex { x: 1, y: 0 })); - println!("{:?}", board.at(BoardIndex { x: 2, y: 0 })); - println!("{:?}", board.at(BoardIndex { x: 3, y: 0 })); - todo!() + fn pawn_simple_moves() { + let board = Board::from_ascii( + r#"........ + .#.#.... + ..p..... + .#.#...."#, + ); + let given = board.valid_moves((2, 1).into()); + let expected: HashSet = + HashSet::from([(1, 0).into(), (3, 0).into(), (1, 2).into(), (3, 2).into()]); + + assert_eq!(expected, given, "Basic pawn moves"); + } + + #[test] + fn drone_simple_moves() { + let board = Board::from_ascii( + r#"..#..... + ..#..... + ##d##... + ..#....."#, + ); + let given = board.valid_moves((2, 1).into()); + let expected: HashSet = HashSet::from([ + (2, 0).into(), + (2, 2).into(), + (2, 3).into(), + (0, 1).into(), + (1, 1).into(), + (3, 1).into(), + (4, 1).into(), + ]); + + assert_eq!(expected, given, "Basic drone moves"); + } + + #[test] + fn queen_simple_moves() { + let board = Board::from_ascii( + r#"...###.. + ####q### + ...###.. + ..#.#.#."#, + ); + let given = board.valid_moves((4, 2).into()); + let expected: HashSet = HashSet::from([ + (0, 2).into(), + (1, 0).into(), + (1, 2).into(), + (2, 0).into(), + (2, 2).into(), + (3, 1).into(), + (3, 2).into(), + (3, 3).into(), + (4, 0).into(), + (4, 1).into(), + (4, 3).into(), + (5, 1).into(), + (5, 2).into(), + (5, 3).into(), + (6, 0).into(), + (6, 2).into(), + (7, 0).into(), + (7, 2).into(), + ]); + + assert_eq!(expected, given, "Basic queen moves"); + } + + #[test] + fn empty_moves() { + let board = Board::from_ascii( + r#"........ + .p....q. + ........ + ...d...."#, + ); + let given = board.valid_moves((2, 1).into()); + let expected: HashSet = HashSet::from([]); + + assert_eq!(expected, given, "Empty moves"); + } + + /// When a piece is blocked on all sides by friendly, cannot move + #[test] + fn blocking_friendly() { + let board = Board::from_ascii( + r#"p....... + ..dp.... + .pqp.... + .ppp...."#, + ); + // Check queen + { + let given = board.valid_moves((2, 1).into()); + let expected: HashSet = HashSet::from([(1, 2).into()]); + assert_eq!(expected, given, "Mostly surrounded queen, some moves"); + } + // Check pawn + { + let given = board.valid_moves((3, 1).into()); + let expected: HashSet = HashSet::from([(4, 0).into(), (4, 2).into()]); + assert_eq!(expected, given, "Partially blocked pawn, some moves"); + } + // Check drone + { + let given = board.valid_moves((2, 2).into()); + let expected: HashSet = + HashSet::from([(0, 2).into(), (1, 2).into(), (2, 3).into()]); + assert_eq!(expected, given, "Partially blocked drone, some moves"); + } + } + + /// When an enemy is on one side, can move to capture just that piece + #[test] + fn case_03() { + let board = Board::from_ascii( + r#"........ + ........ + ppp..... + pq..p..p"#, + ); + + { + let given = board.valid_moves((1, 0).into()); + let expected: HashSet = + HashSet::from([(2, 0).into(), (3, 0).into(), (4, 0).into()]); + assert_eq!( + expected, given, + "Mostly blocked queen, moves include captures" + ); + } + } + + /// Can move up to but not over friendlies + #[test] + fn case_02() { + let board = Board::from_ascii( + r#"......qq + dpp#d#d. + qd##qppd + qq.###.."#, + ); + + { + let given = board.valid_moves((4, 1).into()); + let expected: HashSet = + HashSet::from([ + (3,0).into(), (4,0).into(), (5,0).into(), (2, 1).into(), (2, 3).into(), (3, 1).into(), (5, 1).into(), (3, 2).into(), (5, 2).into() + ]); + assert_eq!( + expected, given, + "Mostly blocked queen, moves include captures" + ); + } } } diff --git a/src/hit3d.rs b/src/hit3d.rs index 70bc991..f881869 100644 --- a/src/hit3d.rs +++ b/src/hit3d.rs @@ -99,9 +99,7 @@ pub(crate) fn intersects3d(ray: &Ray3d, mesh: &Mesh, gt: &GlobalTransform) -> Op && triangle.normal().dot(c1) > 0.0 && triangle.normal().dot(c2) > 0.0 }; - hit.then_some(Hit3d { - distance: d, - }) + hit.then_some(Hit3d { distance: d }) } else { None } diff --git a/src/ui.rs b/src/ui.rs index 0f9f63b..8ab2ed3 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -22,8 +22,7 @@ impl Plugin for UiPlugin { ), ) .add_systems(OnEnter(GameState::Intro), show_click_prompt) - .add_systems(OnExit(GameState::Title), hide_click_prompt) - ; + .add_systems(OnExit(GameState::Title), hide_click_prompt); } } @@ -189,14 +188,14 @@ fn init_prompts(mut commands: Commands) { }); } -fn show_click_prompt( - mut query: Query<(&mut Visibility, &Prompt)>, -) { - query.iter_mut().for_each(|(mut vis, _)| *vis = Visibility::Inherited); +fn show_click_prompt(mut query: Query<(&mut Visibility, &Prompt)>) { + query + .iter_mut() + .for_each(|(mut vis, _)| *vis = Visibility::Inherited); } -fn hide_click_prompt( - mut query: Query<(&mut Visibility, &Prompt)>, -) { - query.iter_mut().for_each(|(mut vis, _)| *vis = Visibility::Hidden); -} \ No newline at end of file +fn hide_click_prompt(mut query: Query<(&mut Visibility, &Prompt)>) { + query + .iter_mut() + .for_each(|(mut vis, _)| *vis = Visibility::Hidden); +}