Re-architecture for moves

Now that moves are a bit more complicated I did a pass at calculating
which moves are valid for a given piece.

The speed/memory usage on this version is likely higher, but we're
dealing with really small numbers of moves so I'm not worried about
it. Optimizations should be simple if I look for them.

As for the re-architect, we generate a list of possible moves for a
piece, check that against some candidates spot, and if it _could_ be a
valid move, we do additional checks like if this is a capture, if this
is an invalidating move, or if this would "Jump" (which was my main goal
with this refactor).
main
Elijah C. Voigt 2 years ago
parent f0befe0928
commit 3157666d84

@ -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<BoardIndex> {
let BoardIndex { x, y } = current_board_index;
let f = |(a, b): (Option<usize>, Option<usize>)| {
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<Item = BoardIndex> {
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<MoveType> {
// 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<BoardIndex> {
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 {

Loading…
Cancel
Save