diff --git a/src/game.rs b/src/game.rs index cf23ef9..456e944 100644 --- a/src/game.rs +++ b/src/game.rs @@ -66,6 +66,60 @@ pub(crate) enum Piece { 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(), + } + } +} + #[derive(Debug, Component, Clone, PartialEq)] pub(crate) enum Tile { Dark, @@ -210,6 +264,35 @@ pub(crate) struct BoardIndex { 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 std::ops::Add<(isize, isize)> for BoardIndex { + type Output = BoardIndex; + + fn add(self, (a, b): (isize, isize)) -> Self { + BoardIndex { + x: self.x.saturating_add_signed(a), + y: self.y.saturating_add_signed(b), + } + } +} + +#[derive(Debug)] +pub(crate) enum MoveType { + Valid, + Invalid, + Capture, +} + #[derive(Debug, Component, PartialEq, Clone, Copy, Eq, Hash, Default)] pub(crate) enum Side { A, @@ -397,167 +480,128 @@ impl Board { } } - /// Returns the possible moves the piece at this tile can make. - /// TODO: Implement "no jumping" over pieces - pub(crate) fn valid_moves(&self, current_board_index: BoardIndex) -> HashSet { - let BoardIndex { x, y } = current_board_index; - - let f = |(a, b): (Option, Option)| { - if let (Some(this_x), Some(this_y)) = (a, b) { - // This has a valid x position - let valid_x = (0..=7).contains(&this_x); - if valid_x { - // It has a valid y position - let valid_y = (0..=3).contains(&this_y); - if valid_y { - // The checked board index - let this_board_index = BoardIndex { - x: this_x, - y: this_y, - }; - // Only propose tiles that are empty or capture a piece on the other side - let valid_capture = { - match self.at(this_board_index) { - Some(_) => { - Board::side(this_board_index) - != Board::side(current_board_index) - } - None => true, - } - }; - if valid_capture { - // You cannot move a piece from A to B and then back to A when it was just moved from SideA->SideB - // Move rejection is not allowed - let rejection = { - if let Some(Move { - from: last_from, - to: Some(last_to), - .. - }) = self.moves.last() + // 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 11 + (0..11) + .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, + piece: Piece, + from: BoardIndex, + to: BoardIndex, + ) -> Option { + // Iterate over the piece's moves + piece + .moves() + // Find if the given `to` move is one of those + .find(|(x, y)| to == from + (*x, *y)) + // 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(_) => 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(_) => { + // If there is another piece between A and B + if self + .line(from, to) + .any(|board_index| self.at(board_index).is_some()) { - // TODO: I think this is more logic than we need to express - // the sentiment... - *last_from == this_board_index - && *last_to == current_board_index - && Board::side(this_board_index) - != Board::side(current_board_index) + // Invalid move, there is a piece between A and B + Some(MoveType::Invalid) } else { - false + // Otherwise it's a capture + Some(MoveType::Capture) } - }; - // If all tests pass, this is a valid move - (!rejection).then_some(this_board_index) - } else { - None + } + None => { + // move is valid if it does not un-do the previous move + match self.moves.last() { + Some(previous) => { + // If the last `from` is the current destination + // The move is not valid + if previous.from == to { + Some(MoveType::Invalid) + // Otherwise we're good to move there + } else { + Some(MoveType::Valid) + } + } + // First move in the game, this is valid (and impossible) + None => { + // If there is another piece between A and B + if self + .line(from, to) + .any(|board_index| self.at(board_index).is_some()) + { + // Invalid move, there is a piece between A and B + Some(MoveType::Invalid) + } else { + // Otherwise it's a valid + Some(MoveType::Valid) + } + } + } + } } - } else { - None } - } 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() - // Checking moves on the X axis - .chain( - (-2..=-1) - .rev() - .map(|i| f((x.checked_add_signed(i), Some(y)))) - .take_while(|x| x.is_some()), - ) - .chain( - (1..=2) - .map(|i| f((x.checked_add_signed(i), Some(y)))) - .take_while(|x| x.is_some()), - ) - // Checking moves on the Y axis - .chain( - (1..=2) - .map(|i| f((Some(x), y.checked_add_signed(i)))) - .take_while(|x| x.is_some()), - ) - .chain( - (-2..=-1) - .rev() - .map(|i| f((Some(x), y.checked_add_signed(i)))) - .take_while(|x| x.is_some()), - ) - .filter_map(|x| x) - .collect(), - // Any distance in any straight line - Some(Piece::Queen) => std::iter::empty() - .chain( - (-7..=-1) - .rev() - .map(|i| f((x.checked_add_signed(i), Some(y)))) - .take_while(|x| x.is_some()), - ) - .chain( - (1..=7) - .map(|i| f((x.checked_add_signed(i), Some(y)))) - .take_while(|x| x.is_some()), - ) - .chain( - (-3..=-1) - .rev() - .map(|i| f((Some(x), y.checked_add_signed(i)))) - .take_while(|x| x.is_some()), - ) - .chain( - (1..=3) - .map(|i| f((Some(x), y.checked_add_signed(i)))) - .take_while(|x| x.is_some()), - ) - .chain( - (-3..=-1) - .rev() - .zip((-3..=-1).rev()) - .map(move |(a, b)| f((x.checked_add_signed(a), y.checked_add_signed(b)))) - .take_while(|x| x.is_some()), - ) - .chain( - (-3..=-1) - .rev() - .zip(1..=3) - .map(move |(a, b)| f((x.checked_add_signed(a), y.checked_add_signed(b)))) - .take_while(|x| x.is_some()), - ) - .chain( - (1..=3) - .zip((-3..=-1).rev()) - .map(move |(a, b)| f((x.checked_add_signed(a), y.checked_add_signed(b)))) - .take_while(|x| x.is_some()), - ) - .chain( - (1..=3) - .zip(1..=3) - .map(move |(a, b)| f((x.checked_add_signed(a), y.checked_add_signed(b)))) - .take_while(|x| x.is_some()), - ) - .filter_map(|x| x) - .collect(), - None => std::iter::empty().collect(), - } + /// Returns the possible moves the piece at this tile can make. + /// TODO: Implement "no jumping" over pieces + pub(crate) fn valid_moves(&self, current_board_index: BoardIndex) -> HashSet { + tiles() + .filter_map(|(board_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), + } + }) + }) + .collect() } pub(crate) fn current_epoch(&self) -> usize {