use bevy::utils::HashSet; use crate::prelude::*; pub(crate) struct GamePlugin; impl Plugin for GamePlugin { fn build(&self, app: &mut App) { app.add_event::() .add_systems(Startup, setup_board) .add_systems( Update, ( menu::exit_to_menu.run_if(in_state(GameState::Play)), update_board.run_if(on_event::()).after(select_sync), set_side.run_if(on_event::()), // TODO: correct run_if? cancel_place.run_if(|buttons: Res>| -> bool { buttons.just_pressed(MouseButton::Right) }), select_sync .run_if(any_component_added::) .after(deselect_sync), deselect_sync.run_if(any_component_removed::()), move_piece.run_if(any_component_added::), capture_piece.run_if(any_component_added::), null_selections.run_if(any_component_added::), pick_up_audio:: .run_if(in_state(DisplayState::Display2d)) .run_if(any_component_added::), pick_up_audio:: .run_if(in_state(DisplayState::Display3d)) .run_if(any_component_added::), ), ) .add_systems( PreUpdate, ( asserts::, asserts::, ), ) .add_systems( PostUpdate, (debug_board.run_if(resource_exists::()),), ); } } #[derive(Debug, Component, Clone, PartialEq, Copy)] pub(crate) enum Piece { Pawn, Drone, Queen, } #[derive(Debug, Component, Clone, PartialEq)] pub(crate) enum Tile { Dark, Light, } pub(crate) fn tiles() -> impl Iterator { (0..32).map(|i| { let x = i % 8; let y = i / 8; let s = (x % 2) ^ (y % 2); let index = BoardIndex { x, y }; let tile = if s == 0 { Tile::Dark } else { Tile::Light }; (index, tile) }) } #[derive(Debug, Component)] pub(crate) struct BoardComponent; #[derive(Debug, Component)] pub(crate) struct Captured; // manually for the type. impl std::fmt::Display for Piece { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Piece::Queen => write!(f, "@"), Piece::Drone => write!(f, "^"), Piece::Pawn => write!(f, "*"), } } } #[derive(Debug)] pub(crate) enum GameError { NullMove, InvalidIndex, } /// The board is setup like this: /// ```text /// 0 1 2 3 4 5 6 7 /// +--+--+--+--+--+--+--+--+ /// a | | | | I | d| Q| Q| /// +--+--+--+--+--+--+--+--+ /// b |d |p |p | I | p| d| Q| /// +--+--+--+--+--+--+--+--+ /// c |Q |d |p | I | p| p| d| /// +--+--+--+--+--+--+--+--+ /// d |Q |Q |d | I | | | | /// +--+--+--+--+--+--+--+--+ /// ```` #[derive(Debug, Resource)] pub(crate) struct Board { inner: Vec>>, moves: Vec, } #[derive(Debug, Default, Event, Clone)] pub(crate) struct Move { pub epoch: usize, pub from: BoardIndex, pub to: Option, } #[derive(Debug, Component, PartialEq, Clone, Default, Copy, Eq, Hash)] pub(crate) struct BoardIndex { pub x: usize, pub y: usize, } #[derive(Debug, Component, PartialEq)] pub(crate) enum Side { A, B, } impl Board { /// Returns the piece at the given location pub(crate) fn at(&self, BoardIndex { x, y }: BoardIndex) -> Option<&Piece> { self.inner[y][x].as_ref() } /// Returns a list of all pieces on the board with their location pub(crate) fn pieces(&self) -> Vec<(BoardIndex, Piece)> { self.inner .iter() .enumerate() .flat_map(|(y, nested)| { nested.iter().enumerate().filter_map(move |(x, p)| { p.as_ref().map(|val| (BoardIndex { x, y }, val.clone())) }) }) .collect() } /// Moves a piece from -> to pub(crate) fn move_piece( &mut self, from: BoardIndex, to: BoardIndex, ) -> Result, GameError> { if from == to { Err(GameError::NullMove) } else { match self.at(from.clone()) { Some(from_val) => { // The current epoch is the last epoch + 1 let epoch = self.current_epoch(); // Local moves vec we can return let mut moves = vec![]; // If the position we are moving to is occupied, capture the removal in the ledger if self.inner[to.y][to.x].is_some() { moves.push(Move { epoch, from: to.clone(), to: None, }); } // Capture the intened move in the moves ledger moves.push(Move { epoch, from: from.clone(), to: Some(to.clone()), }); self.inner[to.y][to.x] = Some(*from_val); self.inner[from.y][from.x] = None; self.moves.extend(moves.clone()); Ok(moves) } None => Err(GameError::NullMove), } } } /// Returns the Side of a piece pub(crate) fn side(BoardIndex { x, .. }: BoardIndex) -> Result { match x { 0..=3 => Ok(Side::A), 4..=7 => Ok(Side::B), _ => Err(GameError::InvalidIndex), } } /// Returns the possible moves the piece at this tile can make. pub(crate) fn possible_moves(&self, BoardIndex { x, y }: BoardIndex) -> HashSet { let f = |(a, b): (Option, Option)| { if let (Some(x), Some(y)) = (a, b) { if (0..=7).contains(&x) && (0..=3).contains(&y) { Some(BoardIndex { x, y }) } else { None } } else { None } }; match self.at(BoardIndex { x, y }) { // One space in any diagonal Some(Piece::Pawn) => std::iter::empty() .chain( (-1..=1) .zip(-1..=1) .map(move |(a, b)| (x.checked_add_signed(a), y.checked_add_signed(b))), ) .chain( (-1..=1) .zip((-1..=1).rev()) .map(move |(a, b)| (x.checked_add_signed(a), y.checked_add_signed(b))), ) .filter_map(f) .collect(), // One or two spaces in either horizontal Some(Piece::Drone) => std::iter::empty() .chain((-2..=2).map(|i| (x.checked_add_signed(i), Some(y)))) .chain((-2..=2).map(|i| (Some(x), y.checked_add_signed(i)))) .filter_map(f) .collect(), // Any distance in any straight line Some(Piece::Queen) => std::iter::empty() .chain((-7..=7).map(|i| (x.checked_add_signed(i), Some(y)))) .chain((-3..=3).map(|i| (Some(x), y.checked_add_signed(i)))) .chain( (-3..=3) .zip(-3..=3) .map(move |(a, b)| (x.checked_add_signed(a), y.checked_add_signed(b))), ) .chain( (-3..=3) .zip((-3..=3).rev()) .map(move |(a, b)| (x.checked_add_signed(a), y.checked_add_signed(b))), ) .filter_map(f) .collect(), None => std::iter::empty().collect(), } } pub(crate) fn current_epoch(&self) -> usize { self.moves.last().unwrap_or(&Move { ..default() }).epoch + 1 } } impl std::fmt::Display for Board { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { self.inner.iter().rev().for_each(|row| { let _ = write!(f, "+--+--+--+--+--+--+--+--+\n"); let _ = write!(f, "|"); row.iter().for_each(|piece| { let _ = match piece { Some(p) => write!(f, "{} |", p), None => write!(f, " |"), }; }); let _ = write!(f, "\n"); }); let _ = write!(f, "+--+--+--+--+--+--+--+--+"); Ok(()) } } /// Marker component for currently selected entities #[derive(Debug, Default, Component)] pub(crate) struct Selected; /// Marker component for selectable entities #[derive(Debug, Default, Component)] pub(crate) struct Selectable; fn setup_board(mut commands: Commands) { use Piece::*; commands.insert_resource(Board { moves: vec![], inner: vec![ vec![ Some(Queen), Some(Queen), Some(Drone), None, None, None, None, None, ], vec![ Some(Queen), Some(Drone), Some(Pawn), None, None, Some(Pawn), Some(Pawn), Some(Drone), ], vec![ Some(Drone), Some(Pawn), Some(Pawn), None, None, Some(Pawn), Some(Drone), Some(Queen), ], vec![ None, None, None, None, None, Some(Drone), Some(Queen), Some(Queen), ], ], }); } fn debug_board(board: Res, mut debug_info: ResMut) { debug_info.set("board".into(), format!("\n{}", *board)); } /// Update this method to use a diff between the board and the state of the 2d/3d worlds pub(crate) fn update_board( mut events: EventReader, mut pieces: Query<(Entity, &mut BoardIndex), With>, selected: Query>, mut commands: Commands, ) { events.iter().for_each(|Move { from, to, .. }| { pieces.iter_mut().for_each(|(entity, mut index)| { if *index == *from { match to { Some(to_idx) => { info!("Moving piece {:?} to {:?}", entity, to_idx); *index = to_idx.clone(); } None => { info!("Capturing piece {:?}", entity); commands .entity(entity) .remove::() .insert(Captured); } } } }); selected.iter().for_each(|entity| { info!("De-selecting selected piece {:?}", entity); commands.entity(entity).remove::(); }); }) } pub(crate) fn set_side(mut events: Query<(&mut Side, &BoardIndex), Changed>) { events .iter_mut() .for_each(|(mut side, idx)| match Board::side(*idx) { Ok(s) => { debug!("Set side event {:?} {:?} -> {:?}", idx, side, s); if *side != s { *side = s } } Err(e) => warn!("{:?}", e), }); } fn select_sync( events: Query>, pieces: Query<(Entity, &BoardIndex), (With, With)>, mut commands: Commands, ) { events.iter().for_each(|entity| { if let Ok((this_e, this_idx)) = pieces.get(entity) { if let Some((entangled, _)) = pieces .iter() .find(|(e, idx)| *idx == this_idx && *e != this_e) { commands.entity(entangled).insert(Selected); } } }) } fn deselect_sync( mut events: RemovedComponents, pieces: Query<(Entity, &BoardIndex), (With, With)>, mut commands: Commands, ) { events.iter().for_each(|entity| { if let Ok((this_e, this_idx)) = pieces.get(entity) { if let Some((entangled, _)) = pieces .iter() .find(|(e, idx)| *idx == this_idx && *e != this_e) { info!("De-selecting entangled piece {:?}", entity); commands.entity(entangled).remove::(); } } }) } /// Triggered when right-mouse-button clicked fn cancel_place(current: Query>, mut commands: Commands) { current.iter().for_each(|entity| { info!("De-selecting {:?}", entity); commands.entity(entity).remove::(); }); } /// When a tile is selected, move all selected pieces to that index fn move_piece( events: Query<&BoardIndex, (With, Added)>, selected_pieces: Query<&BoardIndex, (With, With)>, mut board: ResMut, mut move_events: EventWriter, mut writer: EventWriter, ) { events.iter().for_each(|to| { selected_pieces.iter().for_each(|from| { if from != to { info!("Applying move {:?} -> {:?}", from, to); // Move piece match board.move_piece(*from, *to) { Ok(moves) => { // De-select the piece info!("Applying moves {:?}", moves); moves.iter().for_each(|m| move_events.send(m.clone())); writer.send(audio::AudioEvent::PutDown) } Err(GameError::NullMove) => warn!("Null move!"), Err(GameError::InvalidIndex) => warn!("Invalid index!"), } } }); }); } fn pick_up_audio( selected: Query, With, With)>, mut writer: EventWriter, ) { if selected.iter().len() == 1 { writer.send(audio::AudioEvent::PickUp); } } /// De-select anything that shouldn't be selected /// Namely tiles when there are not selected pieces fn null_selections( events: Query>, selected_pieces: Query, With)>, mut commands: Commands, mut writer: EventWriter, ) { events.iter().for_each(|entity| { if selected_pieces.is_empty() { info!( "De-selecting piece that should not be selected {:?}", entity ); commands.entity(entity).remove::(); writer.send(audio::AudioEvent::PutDown); } }); } /// When a piece's _BoardIndex_ is removed, we hide that entity from the viewer fn capture_piece(mut events: Query<&mut Visibility, Added>) { events.iter_mut().for_each(|mut vis| { info!("Hiding captured piece"); *vis = Visibility::Hidden }); } /// Panics if more than two pieces are selected at a time fn asserts( selected_pieces: Query, With, With)>, selected_tiles: Query, With, With)>, ) { if selected_pieces.iter().len() > 2 { panic!("More than two pieces selected"); } if selected_tiles.iter().len() > 2 { panic!( "More than two tiles selected {:?}", selected_tiles.iter().len() ); } }