use crate::prelude::*; pub(crate) struct GamePlugin; impl Plugin for GamePlugin { fn build(&self, app: &mut App) { app.add_event::() .add_event::() .init_state::() .insert_resource(Score { ..default() }) .add_systems(Startup, setup_board) .add_systems(OnEnter(GameState::Play), hide_valid_moves) .add_systems( Update, ( manage_state_entities::().run_if(state_changed::), undo_move.run_if(just_pressed(KeyCode::KeyU)), ), ) .add_systems( Update, ( update_board .run_if(on_event::()) .after(handle_selection), set_side.run_if(any_component_changed::()), handle_selection.run_if(on_event::()), show_valid_moves.run_if(any_component_added::()), hide_valid_moves.run_if(any_component_removed::()), manage_score.run_if(any_component_added::()), check_endgame.run_if(resource_changed::), ) .run_if(in_state(GameState::Play)), ) .add_systems( Update, assert_piece_consistency .run_if(in_state(GameState::Play)) .run_if(resource_changed::), ) .add_systems(Update, reset_game.run_if(in_state(GameState::Restart))) .add_systems( OnEnter(GameState::Endgame), (manage_score, set_endgame.after(manage_score)), ) .add_systems(OnExit(GameState::Endgame), clear_endgame) .add_systems( PreUpdate, asserts:: .run_if(in_state(DisplayState::Display3d)) .run_if(in_state(GameState::Play)), ) .add_systems( PostUpdate, (debug_board.run_if(in_state(debug::DebugState::Enabled)),), ) .add_systems(OnEnter(GameState::Restart), handle_restart) .add_systems(OnEnter(GameState::Quit), handle_quit); } } #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] pub(crate) struct TurnState(pub Side); impl std::ops::Not for TurnState { type Output = Self; fn not(self) -> Self::Output { let TurnState(side) = self; TurnState(!side) } } #[derive(Debug, Component, Clone, PartialEq, Copy, Hash)] pub(crate) enum Piece { Pawn, Drone, Queen, } impl Piece { fn moves<'a>(&self) -> std::slice::Iter<'_, (isize, isize)> { match self { Piece::Pawn => [(1, 1), (1, -1), (-1, 1), (-1, -1)].iter(), Piece::Drone => [ (0, 2), (0, 1), (-2, 0), (-1, 0), (1, 0), (2, 0), (0, -1), (0, -2), ] .iter(), Piece::Queen => [ (-3, 3), (0, 3), (3, 3), (-2, 2), (0, 2), (2, 2), (-1, 1), (0, 1), (1, 1), (-7, 0), (-6, 0), (-5, 0), (-4, 0), (-3, 0), (-2, 0), (-1, 0), (1, 0), (2, 0), (3, 0), (4, 0), (5, 0), (6, 0), (7, 0), (-1, -1), (0, -1), (1, -1), (-2, -2), (0, -2), (2, -2), (-3, -3), (0, -3), (3, -3), ] .iter(), } } fn moves_at(&self, from: &BoardIndex) -> HashSet { self.moves() .filter_map(|(x, y)| { let bi = (from.x as isize + x, from.y as isize + y); // Check if this goes out of bounds, if so exclude from the list of possible moves (bi.0 <= 7 && bi.0 >= 0 && bi.1 <= 3 && bi.1 >= 0).then_some(BoardIndex { x: bi.0 as usize, y: bi.1 as usize, }) }) .collect() } fn value(&self) -> usize { match self { Piece::Pawn => 1, Piece::Drone => 2, Piece::Queen => 3, } } } pub(crate) fn piece_model_key(piece: Piece, side: Side) -> &'static str { match (piece, side) { (Piece::Pawn, Side::A) => "display3d_models_scenes_pawn_red", (Piece::Pawn, Side::B) => "display3d_models_scenes_pawn_blue", (Piece::Drone, Side::A) => "display3d_models_scenes_drone_red", (Piece::Drone, Side::B) => "display3d_models_scenes_drone_blue", (Piece::Queen, Side::A) => "display3d_models_scenes_queen_red", (Piece::Queen, Side::B) => "display3d_models_scenes_queen_blue", } } #[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 ValidMove; /// Marker for entity being captured #[derive(Debug, Component)] pub(crate) struct BeingCaptured; /// Marker for component which is captured #[derive(Debug, Component)] pub(crate) struct Captured { pub epoch: usize, } #[derive(Debug, Component)] pub(crate) struct Promoted; // 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, PartialEq)] pub(crate) enum GameError { NullMove, InvalidIndex, InvalidMove, } /// Tracks the score of each side of the game #[derive(Debug, Resource, Default)] pub(crate) struct Score { score_a: usize, score_b: usize, captures_a: usize, captures_b: usize, } impl Score { pub(crate) fn _get(&self, side: Side) -> usize { match side { Side::A => self.score_a, Side::B => self.score_b, } } pub(crate) fn captures(&self, side: Side) -> usize { match side { Side::A => self.captures_a, Side::B => self.captures_b, } } } /// The board is setup like this: /// ```text /// 0 1 2 3 4 5 6 7 /// +--+--+--+-----+--+--+--+ /// a | | | | l | d| Q| Q| /// +--+--+--+--l--+--+--+--+ /// b |d |p |p | l | p| d| Q| /// +--+--+--+--l--+--+--+--+ /// c |Q |d |p | l | p| p| d| /// +--+--+--+--l--+--+--+--+ /// d |Q |Q |d | l | | | | /// +--+--+--+-----+--+--+--+ /// ```` #[derive(Debug, Resource)] pub(crate) struct Board { inner: Vec>>, moves: Vec, } #[derive(Debug, Default, Event, Clone)] pub(crate) struct Move { pub epoch: usize, pub piece: Option, pub from: BoardIndex, pub to: Option, pub move_type: MoveType, } #[derive(Debug, Component, PartialEq, Clone, Default, Copy, Eq, Hash)] pub(crate) struct BoardIndex { pub x: usize, pub y: usize, } impl std::ops::Add for BoardIndex { type Output = BoardIndex; fn add(self, other: Self) -> Self { BoardIndex { x: self.x + other.x, y: self.y + other.y, } } } impl From<(usize, usize)> for BoardIndex { fn from((x, y): (usize, usize)) -> BoardIndex { BoardIndex { x, y } } } #[derive(Debug, PartialEq, Clone, Default)] pub(crate) enum MoveType { Valid, #[default] Invalid, Capture, Promotion(Piece), } #[derive(Debug, Component, PartialEq, Clone, Copy, Eq, Hash, Default)] pub(crate) enum Side { A, // HACK: Opening animation starts on B side #[default] B, } impl std::ops::Not for Side { type Output = Self; fn not(self) -> Self::Output { match self { Side::A => Side::B, Side::B => Side::A, } } } 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() } 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 index = BoardIndex { x: 0, y: 3 }; art.chars().for_each(|c| { match c { 'q' | 'Q' => { inner[index.y].push(Some(Queen)); } 'd' | 'D' => { inner[index.y].push(Some(Drone)); } '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!" ); index.y -= 1; } _ => { // Ignore all other characters } } }); Board { inner, moves: vec![], } } fn new() -> Board { Board::from_ascii( r#".....dqq dpp..pdq qdp..ppd qqd....."#, ) } /// Show all pieces on one side of the board pub(crate) fn on(&self, side: Side) -> Vec<(&Piece, BoardIndex)> { match side { Side::A => { // X: 0..3, Y: 0..3 (0..=3) .flat_map(|x| { (0..=3).map(move |y| { self.at(BoardIndex { x, y }) .map(|p| (p, BoardIndex { x, y })) }) }) .flatten() .collect() } Side::B => { // X: 4..7, Y: 0..3 (4..=7) .flat_map(|x| { (0..=3).map(move |y| { self.at(BoardIndex { x, y }) .map(|p| (p, BoardIndex { x, y })) }) }) .flatten() .collect() } } } /// 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))) }) .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) { Some(from_piece) => { let move_type = self.move_type(from, to); match move_type { MoveType::Invalid => Err(GameError::InvalidMove), MoveType::Valid | MoveType::Capture | MoveType::Promotion(..) => { // 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, piece: self.at(to).copied(), from: to, to: None, move_type: move_type.clone(), }); } // Capture the intended move in the moves ledger moves.push(Move { epoch, piece: self.at(from).copied(), from, to: Some(to), move_type: move_type.clone(), }); self.inner[to.y][to.x] = match move_type { MoveType::Promotion(_) => match (self.at(from), self.at(to)) { (Some(Piece::Pawn), Some(Piece::Pawn)) => Some(Piece::Drone), (Some(Piece::Pawn), Some(Piece::Drone)) | (Some(Piece::Drone), Some(Piece::Pawn)) => Some(Piece::Queen), _ => panic!( "Merges can only happen between pawn+pawn or pawn+drone!" ), }, _ => Some(*from_piece), }; self.inner[from.y][from.x] = None; self.moves.extend(moves.clone()); Ok(moves) } } } None => Err(GameError::NullMove), } } } /// Undo the last move fn undo_move(&mut self) -> Vec { let latest_epoch = self.current_epoch() - 1; let mut moves = vec![]; while let Some(last) = self.moves.last() { if last.epoch == latest_epoch { let last = self.moves.pop().unwrap(); self.inner[last.from.y][last.from.x] = last.piece; if let Some(to) = last.to { self.inner[to.y][to.x] = None; } moves.push(last); } else { break; } } debug!("Remaining moves: {:?}", self.moves); return moves; } /// 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 a list of indexes between two points, excluding the start and end fn line(&self, from: BoardIndex, to: BoardIndex) -> impl Iterator { let mut curr = from; // Longest possible move is 10, so we create a generator over 12 (0..12) .map(move |_| { let x = if curr.x > to.x { curr.x.saturating_sub(1) } else if curr.x < to.x { curr.x.saturating_add(1) } else { to.x }; let y = if curr.y > to.y { curr.y.saturating_sub(1) } else if curr.y < to.y { curr.y.saturating_add(1) } else { to.y }; curr = BoardIndex { x, y }; (curr != to).then_some(curr) }) .filter_map(|bi| bi) } /// Determine given a piece, a to, and a from, what type of move this would be pub(crate) fn move_type(&self, from: BoardIndex, to: BoardIndex) -> MoveType { self.line(from, to) .all(|board_index| self.at(board_index).is_none()) .then(|| { self.at(from).map(|piece| { // Given that the side does not have a queen||drone // And the piece is a drone||pawn // We can do field promotions let side = Board::side(from).expect("Piece has valid index"); let side_has_queen = self .on(side) .iter() .any(|(piece, _)| **piece == Piece::Queen); let side_has_drone = self .on(side) .iter() .any(|(piece, _)| **piece == Piece::Drone); // Iterate over the piece's moves piece .moves_at(&from) .iter() // Find if the given `to` move is one of those .find(|idx| to == **idx) // Determine if this is valid/legal in this situation .and_then(|_| { let dest_at = self.at(to); let curr_side = Board::side(from).unwrap(); let dest_side = Board::side(to).unwrap(); match (curr_side, dest_side) { (Side::A, Side::A) | (Side::B, Side::B) => { match dest_at { // Cannot move on top of a friendly Some(to_piece) => match (piece, to_piece) { (Piece::Pawn, Piece::Pawn) => (!side_has_drone) .then_some(MoveType::Promotion(Piece::Drone)), (Piece::Drone, Piece::Pawn) | (Piece::Pawn, Piece::Drone) => (!side_has_queen) .then_some(MoveType::Promotion(Piece::Queen)), _ => Some(MoveType::Invalid), }, // Any other spot is valid None => Some(MoveType::Valid), } } // Check for moving across the canal (Side::A, Side::B) | (Side::B, Side::A) => { match dest_at { Some(_) => Some(MoveType::Capture), None => { debug!("Last move: {:?}", self.moves.last()); // move is valid if it does not un-do the previous move for this piece match self.moves.last() { Some(previous) => { let is_undo = previous.from == to && previous.to == Some(from); (!is_undo).then_some(MoveType::Valid) } // First move in the game, this is valid (and impossible) None => { // the move is valid Some(MoveType::Valid) } } } } } } }) }) }) .flatten() .flatten() .or(Some(MoveType::Invalid)) .unwrap() } /// Returns the possible moves the piece at this tile can make. pub(crate) fn valid_moves(&self, current_board_index: BoardIndex) -> HashSet { 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) let result = self.move_type(current_board_index, *move_index); match result { MoveType::Invalid => None, MoveType::Capture | MoveType::Promotion(..) | MoveType::Valid => { Some(*move_index) } } }) .collect() } else { HashSet::new() } } pub(crate) fn current_epoch(&self) -> usize { self.moves.last().unwrap_or(&Move { ..default() }).epoch + 1 } } mod test { #[test] fn pawn_simple_moves() { use super::*; 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() { use super::*; 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() { use super::*; let board = Board::from_ascii( r#"...###.. ####q### ...###.. ..#.#.#."#, ); let given = board.valid_moves((4, 2).into()); let expected: HashSet = HashSet::from([ (0, 2).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, 2).into(), ]); assert_eq!( expected, Piece::Queen.moves_at(&(4, 2).into()), "Generated queen moves" ); assert_eq!(expected, given, "Basic queen moves"); } #[test] fn empty_moves() { use super::*; 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"); } #[test] fn moves_multi_step() { use super::*; let mut board = Board::from_ascii( r#"........ ........ ....p... ...p...."#, ); // First assert that the piece can move { // The left side pawn can move to capture the right side pawn let given = board.valid_moves((3, 0).into()); let expected: HashSet = HashSet::from([(4, 1).into(), (2, 1).into()]); assert_eq!(expected, given, "Sanity check"); } // Next move the right side pawn let _ = board.move_piece((4, 1).into(), (3, 2).into()); // Assert the spot is now empty assert_eq!(board.at((4, 1).into()), None); // Now assert that the left side pawn can move into the spot taken by the right side pawn { // The left side pawn can move to capture the right side pawn let given = board.valid_moves((3, 0).into()); let expected: HashSet = HashSet::from([(4, 1).into(), (2, 1).into()]); assert_eq!(expected, given, "Second step move"); } } /// When a piece is blocked on all sides by friendly, cannot move #[test] fn blocking_friendly() { use super::*; 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_01() { use super::*; 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" ); } // Pawn can merge with pawn { let given = board.valid_moves((0, 0).into()); let expected: HashSet = HashSet::from([(1, 1).into()]); assert_eq!(expected, given, "Pawn can merge to make a drone"); let move_type = board.move_type((0, 0).into(), (1, 1).into()); assert_eq!(MoveType::Promotion(Piece::Drone), move_type); } // Pawn cannot merge with queen { let given = board.valid_moves((2, 1).into()); let expected: HashSet = HashSet::from([(3, 0).into(), (3, 2).into(), (1, 2).into()]); assert_eq!(expected, given, "Pawn cannot merge with queen"); } } /// Can move up to but not over friendlies #[test] fn case_02() { use super::*; 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([ (1, 1).into(), (2, 1).into(), (2, 3).into(), (3, 0).into(), (3, 1).into(), (3, 2).into(), (4, 0).into(), (5, 0).into(), (5, 2).into(), ]); assert_eq!( expected, given, "Mostly blocked queen, moves include captures" ); } } #[test] fn case_03() { use super::*; let board = Board::from_ascii( r#".p.....q dp.p.pdq qd..dppd qqdq...."#, ); { let given = board.valid_moves((0, 0).into()); let expected: HashSet = HashSet::from([]); assert_eq!(expected, given); } { let given = board.valid_moves((1, 0).into()); let expected: HashSet = HashSet::from([(2, 1).into()]); assert_eq!(expected, given); } { let given = board.valid_moves((2, 0).into()); let expected: HashSet = HashSet::from([(2, 1).into(), (2, 2).into()]); assert_eq!(expected, given); } } // A strange failure mode in game which I suspect comes from // a disconnect between board logic and display plumbing #[test] fn case_04() { use super::*; let mut board = Board::from_ascii( r#".....dqq dpp..pdq qdp....d qqd....."#, ); { board.move_piece((5, 3).into(), (4, 3).into()).unwrap(); board.move_piece((2, 2).into(), (1, 3).into()).unwrap(); let expected = Board::from_ascii( r#".p..d#qq dp...pdq qdp....d qqd....."#, ); assert_eq!(expected.inner, board.inner); } { let given = board.valid_moves((6, 3).into()); let expected: HashSet = HashSet::from([(5, 3).into()]); assert_eq!(expected, given); } } #[test] fn test_piece_moves_at() { use super::*; let given = Piece::Drone.moves_at(&(4, 1).into()); let expected: HashSet = HashSet::from([ (4, 0).into(), (2, 1).into(), (3, 1).into(), (5, 1).into(), (6, 1).into(), (4, 2).into(), (4, 3).into(), ]); assert_eq!(expected, given, "Drone moves at"); } #[test] fn test_capture_01() { use super::*; let board = Board::from_ascii( r#"...p.... ........ .ppd.q.. ........"#, ); // Drone can move in all expected ways { let expected: HashSet = HashSet::from([ (2, 1).into(), (3, 0).into(), (3, 2).into(), (3, 3).into(), (4, 1).into(), (5, 1).into(), ]); let actual = board.valid_moves((3, 1).into()); assert_eq!(expected, actual); let capture_move = board.move_type((3, 1).into(), (5, 1).into()); assert_eq!(MoveType::Capture, capture_move); let merge_move = board.move_type((3, 1).into(), (2, 1).into()); assert_eq!(MoveType::Promotion(Piece::Queen), merge_move); let long_merge_move = board.move_type((3, 1).into(), (3, 3).into()); assert_eq!(MoveType::Promotion(Piece::Queen), long_merge_move); let jump_move = board.move_type((3, 1).into(), (1, 1).into()); assert_eq!(MoveType::Invalid, jump_move); } } #[test] fn test_capture_02() { use super::*; let board = Board::from_ascii( r#"........ ..p.p... ...p.... ........"#, ); { // All normal moves for pawn are valid let given = board.valid_moves((3, 1).into()); let expected: HashSet = HashSet::from([(2, 0).into(), (4, 0).into(), (2, 2).into(), (4, 2).into()]); assert_eq!(expected, given); // Pawn + Pawn on same side = Promotion let merge_move = board.move_type((3, 1).into(), (2, 2).into()); assert_eq!(MoveType::Promotion(Piece::Drone), merge_move); // Pawn + Pawn on other side = Capture let capture_move = board.move_type((3, 1).into(), (4, 2).into()); assert_eq!(MoveType::Capture, capture_move); // Pawn + Empty = Move let place_move = board.move_type((3, 1).into(), (2, 0).into()); assert_eq!(MoveType::Valid, place_move); // Pawn + Invalid Empty = Invalid let invalid_move = board.move_type((3, 1).into(), (7, 7).into()); assert_eq!(MoveType::Invalid, invalid_move); } } } 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 _ = writeln!(f, "+--+--+--+--+--+--+--+--+"); let _ = write!(f, "|"); row.iter().for_each(|piece| { let _ = match piece { Some(p) => write!(f, "{} |", p), None => write!(f, " |"), }; }); let _ = writeln!(f); }); 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; /// Event for selecting board indexes for Moves #[derive(Debug, Default, Event, Clone)] pub(crate) struct Selection(pub BoardIndex); fn setup_board(mut commands: Commands) { commands.insert_resource(Board::new()); } 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 audio_events: EventWriter, mut events: EventReader, mut pieces: Query<(Entity, &mut BoardIndex), With>, selected: Query>, mut commands: Commands, mut played: Local, curr_state: Res>, mut next_state: ResMut>, ) { // Each move event we get events.read().for_each( |Move { from, to, move_type, .. }| { // Iterate over all pieces pieces.iter_mut().for_each(|(entity, mut index)| { // If the current index is the 'from' for the move // All moves cover From -> To (captures and merges have to: None) if *index == *from { match to { // If we are moving on the board... Some(to_idx) => { debug!("Moving piece {:?} {:?} -> {:?}", entity, from, to_idx); // Update the piece's index *index = *to_idx; // Play audio sfx if !(*played) { audio_events.send(audio::AudioEvent::PutDown); audio_events.send(audio::AudioEvent::StopIdle); *played = true; } if *from != *to_idx { match move_type { MoveType::Promotion(piece) => { commands.entity(entity).insert(*piece); } _ => (), } let ns = !*curr_state.get(); debug!("Piece moved, switching sides: {:?}", ns); next_state.set(ns); } } // We are moving off the board (e.g,. capture or promotion/merge) None => match move_type { MoveType::Capture => { debug!("Capturing piece {:?}", entity); commands .entity(entity) .remove::() .insert(BeingCaptured); audio_events.send(AudioEvent::Captured); } MoveType::Promotion(..) => { commands .entity(entity) .remove::() .insert((Promoted, Visibility::Hidden)); } _ => { panic!("How did you do this!?"); } }, } } }); selected.iter().for_each(|entity| { debug!("De-selecting selected piece {:?}", entity); commands.entity(entity).remove::(); }); }, ); *played = false; } #[derive(Debug, Component)] struct Endgame; fn check_endgame(board: Res, mut next_state: ResMut>) { if board.on(Side::A).is_empty() || board.on(Side::B).is_empty() { warn!("The game is over!"); next_state.set(GameState::Endgame); } } fn set_endgame( score: Res, mut commands: Commands, tweaks_file: Res, tweaks: Res>, ui_font: Res, ) { let tweak = tweaks.get(tweaks_file.handle.clone()).expect("Load tweaks"); let button_handle = tweak.get_handle::("buttons_image_resting").unwrap(); let font_handle = tweak.get_handle::("buttons_font").unwrap(); commands .spawn(( Endgame, NodeBundle { style: Style { width: Val::Percent(100.0), height: Val::Percent(100.0), justify_content: JustifyContent::Center, align_items: AlignItems::Center, flex_direction: FlexDirection::Column, position_type: PositionType::Absolute, ..default() }, background_color: Color::NONE.into(), visibility: Visibility::Inherited, ..default() }, )) .with_children(|parent| { parent.spawn(TextBundle::from_section( "SCORE", TextStyle { font_size: 48.0, color: Color::ORANGE_RED, font: ui_font.handle.clone(), }, )); parent.spawn(TextBundle::from_section( format!("BLUE {}", score.score_b), TextStyle { font_size: 32.0, color: Color::BLUE, font: ui_font.handle.clone(), }, )); parent.spawn(TextBundle::from_section( format!("RED {}", score.score_a), TextStyle { font_size: 32.0, color: Color::RED, font: ui_font.handle.clone(), }, )); parent .spawn(( Endgame, NodeBundle { style: Style { flex_direction: FlexDirection::Row, ..default() }, ..default() }, )) .with_children(|parent| { parent .spawn(( ButtonAction(GameState::Restart), ButtonAction(MenuState::Off), ButtonBundle { style: Style { padding: UiRect::all(Val::Px(5.0)), margin: UiRect::all(Val::Px(5.0)), ..default() }, image: UiImage { texture: button_handle.clone(), ..default() }, ..default() }, )) .with_children(|parent| { parent.spawn(TextBundle { text: Text { sections: vec![TextSection { value: "N e w G a m e".into(), style: TextStyle { color: Color::WHITE, font_size: 12.0, font: font_handle.clone(), }, }], ..default() }, style: Style { margin: UiRect::all(Val::Px(10.0)), ..default() }, ..default() }); }); // Quit button parent .spawn(( ButtonAction(MenuState::Off), ButtonAction(GameState::Quit), ButtonBundle { style: Style { padding: UiRect::all(Val::Px(5.0)), margin: UiRect::all(Val::Px(5.0)), ..default() }, image: UiImage { texture: button_handle.clone(), ..default() }, ..default() }, )) .with_children(|parent| { parent.spawn(TextBundle { text: Text { sections: vec![TextSection { value: "Q u i t".into(), style: TextStyle { color: Color::WHITE, font_size: 12.0, font: font_handle.clone(), }, }], ..default() }, style: Style { margin: UiRect::all(Val::Px(10.0)), ..default() }, ..default() }); }); }); }); } fn clear_endgame(query: Query>, mut commands: Commands) { query.iter().for_each(|e| { commands.entity(e).despawn_recursive(); }) } /// We only track 3D (hack, for now) to prevent duplicates /// TODO: We can calculate this declaratively: /// * All pieces without a BoardIndex are "captured" /// * All captured pieces have their captured side preserved /// We can iterate over these pieces and calculate the score on the fly fn manage_score( query: Query< (&Side, &Piece), ( Or<(With, With)>, With, ), >, mut debug_info: ResMut, mut score: ResMut, ) { // Calculate number of captures performed by either side score.captures_a = query.iter().filter(|(s, _)| **s == Side::B).count(); score.captures_b = query.iter().filter(|(s, _)| **s == Side::A).count(); // Calculate score based on the piece values score.score_a = query .iter() .filter(|(s, _)| **s == Side::B) .fold(0, |acc, (_, piece)| acc + piece.value()); score.score_b = query .iter() .filter(|(s, _)| **s == Side::A) .fold(0, |acc, (_, piece)| acc + piece.value()); // Debug this for good measure debug_info.set( "score".into(), format!("A:{}|B:{}", score.score_a, score.score_b), ); } pub(crate) fn set_side( mut events: Query<(&mut Side, &BoardIndex), Or<(Changed, Added)>>, ) { 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), }); } // TODO: Handle 3d Pickup (Not showing hover animation, but still selected?) // TODO: Handle cancel move (currently 2d just drops it in place) fn handle_selection( mut selections: EventReader, mut move_events: EventWriter, selected: Query<(Entity, &BoardIndex), (With, With)>, pieces: Query<(Entity, &BoardIndex), (With, Without, With)>, mut board: ResMut, mut commands: Commands, mut audio_event: EventWriter, mut done: Local, // Tracks if moves/audio submitted already even if multiple pieces (2d/3d) are moved. mut latest: Local>, // Tracks the last one worked on ) { selections.read().for_each(|Selection(index)| { // Skip indexes already processed if Some(*index) != *latest { // Set the latest index to the current index *latest = Some(*index); // Reset the "done" marker *done = false; // There are no currently selected entities // Mark the piece at this index as selected if selected.is_empty() { pieces .iter() .filter(|(_, this_index)| *this_index == index) .for_each(|(piece, piece_index)| { debug!("Selecting {:?} at {:?}", piece, piece_index); commands.entity(piece).insert(Selected); if !(*done) { audio_event.send(audio::AudioEvent::PickUp); audio_event.send(AudioEvent::Idle); *done = true; } }); } // There is a currently selected entity, so submit moves else { assert!( selected.iter().len() <= 2, "There are too many pieces selected!" ); selected.iter().for_each(|(_, current_index)| { match board.move_piece(*current_index, *index) { Ok(moves) => { // De-select the piece debug!("Applying moves {:?}", moves); if !(*done) { moves.iter().for_each(|m| { move_events.send(m.clone()); }); *done = true; } } Err(GameError::NullMove) => debug!("Null move!"), Err(GameError::InvalidIndex) | Err(GameError::InvalidMove) => { debug!("Invalid index/move!"); if !(*done) { audio_event.send(AudioEvent::Invalid); *done = true; } } } }); } } }); *done = false; *latest = None; } /// Panics if more than two pieces are selected at a time fn asserts( selected_pieces: Query, With, With)>, selected_tiles: Query, With, With)>, cameras: Query, With)>, ) { if selected_pieces.iter().len() > 2 { selected_pieces.iter().for_each(|e| { debug!("Too many pieces selected, one of which is: {:?}", e); }); panic!( "More than two pieces selected {:?}", selected_pieces.iter().len() ); } if selected_tiles.iter().len() > 2 { panic!( "More than two tiles selected {:?}", selected_tiles.iter().len() ); } if cameras.iter().len() != 1 { panic!( "There should be 1 cameras in this state, but there is only {}", cameras.iter().len() ); } } /// Spawn "Valid move" indicators when a piece is selected /// Another system registers these new entities and associates the correct models and plays animations. fn show_valid_moves( events: Query<&BoardIndex, (With, Added)>, board: Res, mut indicators: Query<(&BoardIndex, &mut Visibility), With>, ) { // When a piece is selected events.iter().for_each(|idx| { // Iterate over all ValidMove entities // For each one with a ValidMove index, make it visible let valid_moves = board.valid_moves(*idx); debug!("Showing valid moves for {:?}: {:?}", idx, valid_moves); indicators.iter_mut().for_each(|(i, mut vis)| { if valid_moves.contains(i) { *vis = Visibility::Inherited; } else { *vis = Visibility::Hidden; } }); }); } /// Hide "Valid Move" indicators when a piece is de-selected fn hide_valid_moves(mut indicators: Query<&mut Visibility, With>) { indicators.iter_mut().for_each(|mut visibility| { *visibility = Visibility::Hidden; }); } // Resets the board to the "stock" setup // Somewhat conflicts with board initialization! fn reset_game( pieces: Query>, mut board: ResMut, mut score: ResMut, mut commands: Commands, mut turn_state: ResMut>, ) { // Setup the board *board = Board::new(); // Reset the score *score = Score { ..default() }; // Reset to starting side turn_state.set(TurnState(Side::B)); // Move all pieces to their correct spots pieces .iter() .zip(board.pieces().iter()) .for_each(|(e, (i, p))| { commands .entity(e) .insert((*i, *p, Visibility::Inherited)) .remove::<(BeingCaptured, Captured, Promoted)>(); }); } /// Very simple system to handle the "Quit" state (shutdown the game) fn handle_quit(mut app_exit_events: EventWriter) { app_exit_events.send(AppExit); } fn handle_restart(mut game_state: ResMut>) { game_state.set(GameState::Play); } fn assert_piece_consistency( active: Query, With)>, being_captured: Query, With)>, captured: Query, With)>, ) { let active_count = active.iter().len(); let being_captured_count = being_captured.iter().len(); let captured_count = captured.iter().len(); debug!( "Active: {} | being captured: {} | captured: {}", active_count, being_captured_count, captured_count ); let total_count = active_count + being_captured_count + captured_count; assert_eq!(total_count, 18, "Pieces does does not add up!"); } // When user presses 'u' last move is undone fn undo_move( mut board: ResMut, mut active_pieces: Query<&mut BoardIndex, With>, captured_pieces: Query<(Entity, &Captured), With>, turn: Res>, mut next_turn: ResMut>, mut commands: Commands, ) { // Keep track of the current side in case we need to go back let mut side = turn.get().0; board.undo_move().iter().for_each( |Move { epoch, from, to, .. }| { // If we have any moves to do, go back to the opposite side if side == turn.get().0 { side = !turn.get().0; } debug!("Reverting move {:?} {:?} -> {:?}", epoch, from, to); match to { Some(to_idx) => { debug!("Moving piece back from {:?}", to_idx); // Find piece currently at "to_idx" and update it's position to "from" active_pieces .iter_mut() .filter(|idx| (idx.x, idx.y) == (to_idx.x, to_idx.y)) .for_each(|mut idx| *idx = *from); } None => { captured_pieces .iter() .find_map(|(entity, captured)| (captured.epoch == *epoch).then_some(entity)) .iter() .for_each(|entity| { commands.entity(*entity).remove::().insert(*from); }); } } }, ); // Set the turn state (may be a no-op) next_turn.set(TurnState(side)); }