|
|
|
|
@ -290,11 +290,12 @@ impl From<(usize, usize)> for BoardIndex {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
|
|
|
pub(crate) enum MoveType {
|
|
|
|
|
Valid,
|
|
|
|
|
Invalid,
|
|
|
|
|
Capture,
|
|
|
|
|
Merge,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Component, PartialEq, Clone, Copy, Eq, Hash, Default)]
|
|
|
|
|
@ -366,14 +367,10 @@ impl Board {
|
|
|
|
|
|
|
|
|
|
fn new() -> Board {
|
|
|
|
|
Board::from_ascii(
|
|
|
|
|
r#"...###..
|
|
|
|
|
####q###
|
|
|
|
|
...###..
|
|
|
|
|
..#.#.#."#,
|
|
|
|
|
// r#".....dqq
|
|
|
|
|
// dpp..pdq
|
|
|
|
|
// qdp..ppd
|
|
|
|
|
// qqd....."#,
|
|
|
|
|
r#".....dqq
|
|
|
|
|
dpp..pdq
|
|
|
|
|
qdp..ppd
|
|
|
|
|
qqd....."#,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -513,10 +510,17 @@ impl Board {
|
|
|
|
|
/// 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<MoveType> {
|
|
|
|
|
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)
|
|
|
|
|
@ -533,20 +537,31 @@ impl Board {
|
|
|
|
|
(Side::A, Side::A) | (Side::B, Side::B) => {
|
|
|
|
|
match dest_at {
|
|
|
|
|
// Cannot move on top of a friendly
|
|
|
|
|
Some(_) => Some(MoveType::Invalid),
|
|
|
|
|
Some(to_piece) => {
|
|
|
|
|
(!self.line(from, to)
|
|
|
|
|
.any(|board_index| self.at(board_index)
|
|
|
|
|
.is_some())
|
|
|
|
|
).then(|| {
|
|
|
|
|
match (piece, to_piece) {
|
|
|
|
|
(Piece::Pawn, Piece::Pawn) => {
|
|
|
|
|
(!side_has_drone).then_some(MoveType::Merge)
|
|
|
|
|
}
|
|
|
|
|
(Piece::Drone, Piece::Pawn) | (Piece::Pawn, Piece::Drone) => {
|
|
|
|
|
(!side_has_queen).then_some(MoveType::Merge)
|
|
|
|
|
},
|
|
|
|
|
_ => {
|
|
|
|
|
Some(MoveType::Invalid)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}).flatten()
|
|
|
|
|
},
|
|
|
|
|
// Any other spot is valid
|
|
|
|
|
None => {
|
|
|
|
|
// If there is another piece between A and B
|
|
|
|
|
if self
|
|
|
|
|
(!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)
|
|
|
|
|
}
|
|
|
|
|
.any(|board_index| self.at(board_index).is_some()))
|
|
|
|
|
.then_some(MoveType::Valid)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -554,44 +569,26 @@ impl Board {
|
|
|
|
|
(Side::A, Side::B) | (Side::B, Side::A) => {
|
|
|
|
|
match dest_at {
|
|
|
|
|
Some(_) => {
|
|
|
|
|
// If there is another piece between A and B
|
|
|
|
|
if self
|
|
|
|
|
// If there is no other piece between A and B
|
|
|
|
|
(!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 capture
|
|
|
|
|
Some(MoveType::Capture)
|
|
|
|
|
}
|
|
|
|
|
.any(|board_index| self.at(board_index).is_some()))
|
|
|
|
|
.then_some(MoveType::Capture)
|
|
|
|
|
}
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
(previous.from != to).then_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
|
|
|
|
|
// If there is no other pieces between A and B
|
|
|
|
|
// the move is valid
|
|
|
|
|
(!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)
|
|
|
|
|
}
|
|
|
|
|
.any(|board_index| self.at(board_index).is_some()))
|
|
|
|
|
.then_some(MoveType::Valid)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -599,6 +596,7 @@ impl Board {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}).flatten().or(Some(MoveType::Invalid))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Returns the possible moves the piece at this tile can make.
|
|
|
|
|
@ -606,7 +604,7 @@ impl Board {
|
|
|
|
|
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)
|
|
|
|
|
match self.move_type(*piece, current_board_index, *move_index) {
|
|
|
|
|
match self.move_type(current_board_index, *move_index) {
|
|
|
|
|
None | Some(MoveType::Invalid) => None,
|
|
|
|
|
_ => Some(*move_index),
|
|
|
|
|
}
|
|
|
|
|
@ -757,6 +755,23 @@ mod test {
|
|
|
|
|
"Mostly blocked queen, moves include captures"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Pawn can merge with pawn
|
|
|
|
|
{
|
|
|
|
|
let given = board.valid_moves((0, 0).into());
|
|
|
|
|
let expected: HashSet<BoardIndex> = 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!(Some(MoveType::Merge), move_type);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Pawn cannot merge with queen
|
|
|
|
|
{
|
|
|
|
|
let given = board.valid_moves((2, 1).into());
|
|
|
|
|
let expected: HashSet<BoardIndex> = 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
|
|
|
|
|
@ -836,6 +851,78 @@ mod test {
|
|
|
|
|
|
|
|
|
|
assert_eq!(expected, given, "Drone moves at");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_capture_01() {
|
|
|
|
|
let board = Board::from_ascii(
|
|
|
|
|
r#"...p....
|
|
|
|
|
........
|
|
|
|
|
.ppd.q..
|
|
|
|
|
........"#,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Drone can move in all expected ways
|
|
|
|
|
{
|
|
|
|
|
let expected: HashSet<BoardIndex> = 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);
|
|
|
|
|
|
|
|
|
|
// Merging drone and pawn to make a queen
|
|
|
|
|
let capture_move = board.move_type((3, 1).into(), (5, 1).into());
|
|
|
|
|
assert_eq!(Some(MoveType::Capture), capture_move);
|
|
|
|
|
|
|
|
|
|
// Merging does not cross the canal
|
|
|
|
|
let merge_move = board.move_type((3, 1).into(), (2, 1).into());
|
|
|
|
|
assert_eq!(Some(MoveType::Merge), merge_move);
|
|
|
|
|
|
|
|
|
|
let long_merge_move = board.move_type((3, 1).into(), (3, 3).into());
|
|
|
|
|
assert_eq!(Some(MoveType::Merge), long_merge_move);
|
|
|
|
|
|
|
|
|
|
// Still cannot cross over friendlies
|
|
|
|
|
let jump_move = board.move_type((3, 1).into(), (1, 1).into());
|
|
|
|
|
assert_eq!(Some(MoveType::Invalid), jump_move);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_capture_02() {
|
|
|
|
|
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<BoardIndex> = 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!(Some(MoveType::Merge), merge_move);
|
|
|
|
|
|
|
|
|
|
// Pawn + Pawn on other side = Capture
|
|
|
|
|
let capture_move = board.move_type((3, 1).into(), (4, 2).into());
|
|
|
|
|
assert_eq!(Some(MoveType::Capture), capture_move);
|
|
|
|
|
|
|
|
|
|
// Pawn + Empty = Move
|
|
|
|
|
let place_move = board.move_type((3, 1).into(), (2, 0).into());
|
|
|
|
|
assert_eq!(Some(MoveType::Valid), place_move);
|
|
|
|
|
|
|
|
|
|
// Pawn + Invalid Empty = Invalid
|
|
|
|
|
let invalid_move = board.move_type((3, 1).into(), (7, 7).into());
|
|
|
|
|
assert_eq!(Some(MoveType::Invalid), invalid_move);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl std::fmt::Display for Board {
|
|
|
|
|
|