685 lines
24 KiB
Rust
685 lines
24 KiB
Rust
use crate::api_types::{
|
|
self,
|
|
Color::{self, White},
|
|
Coordinates, PieceKind, PollResponse,
|
|
};
|
|
use std::{
|
|
cell::RefCell,
|
|
collections::{HashMap, HashSet},
|
|
ops,
|
|
};
|
|
|
|
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
|
|
pub(crate) struct Position {
|
|
file: u8,
|
|
rank: u8,
|
|
}
|
|
|
|
impl Position {
|
|
fn from_coordinates(coord: Coordinates) -> Self {
|
|
let file = coord.file.to_number();
|
|
let rank = coord.rank.to_number();
|
|
Position { file, rank }
|
|
}
|
|
}
|
|
|
|
impl ops::Add<(i8, i8)> for &Position {
|
|
type Output = Option<Position>;
|
|
|
|
fn add(self, rhs: (i8, i8)) -> Self::Output {
|
|
let file = self.file as i8 + rhs.0;
|
|
let rank = self.rank as i8 + rhs.1;
|
|
if !(0..=7).contains(&file) || !(0..=7).contains(&rank) {
|
|
None
|
|
} else {
|
|
Some(Position {
|
|
file: file as u8,
|
|
rank: rank as u8,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
|
pub(crate) struct Move {
|
|
from: Position,
|
|
to: Position,
|
|
promote_to: Option<PieceKind>,
|
|
}
|
|
|
|
impl Move {
|
|
fn from_api_move(api_move: api_types::Move) -> Self {
|
|
let from = Position::from_coordinates(api_move.from);
|
|
let to = Position::from_coordinates(api_move.to);
|
|
Move {
|
|
from,
|
|
to,
|
|
promote_to: api_move.kind,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub(crate) struct Piece {
|
|
kind: PieceKind,
|
|
color: Color,
|
|
visible: RefCell<HashSet<Position>>,
|
|
}
|
|
|
|
impl Piece {
|
|
fn get_visible_in_direction(
|
|
direction: (i8, i8),
|
|
pos: &Position,
|
|
state: &GameState,
|
|
) -> HashSet<Position> {
|
|
let mut visible = HashSet::new();
|
|
let mut current_square = pos.clone();
|
|
|
|
loop {
|
|
// keep moving in the specified direction, adding empty squares, until we reach edge of board
|
|
// if we encounter a piece, that is the last visible square
|
|
current_square = match ¤t_square + direction {
|
|
None => break,
|
|
Some(pos) => pos,
|
|
};
|
|
|
|
// add current square to visible
|
|
visible.insert(current_square.clone());
|
|
|
|
// path is blocked by piece, so cannot see/move past it
|
|
if state.board.get(¤t_square).is_some() {
|
|
break;
|
|
}
|
|
}
|
|
visible
|
|
}
|
|
|
|
fn generate_visible(&self, pos: &Position, state: &GameState) {
|
|
let mut visible = HashSet::new();
|
|
let mut move_directions: Vec<(i8, i8)> = Vec::new();
|
|
|
|
match self.kind {
|
|
PieceKind::Pawn => {
|
|
match self.color {
|
|
Color::Black => {
|
|
// can capture diagonally one rank ahead
|
|
visible.extend(pos + (-1, -1));
|
|
visible.extend(pos + (1, -1));
|
|
}
|
|
Color::White => {
|
|
visible.extend(pos + (-1, 1));
|
|
visible.extend(pos + (1, 1));
|
|
}
|
|
}
|
|
}
|
|
PieceKind::Rook => {
|
|
move_directions.extend([(0, 1), (1, 0), (-1, 0), (1, 0)]);
|
|
}
|
|
PieceKind::Knight => {
|
|
let knight_moves: Vec<(i8, i8)> = vec![
|
|
(-1, -2),
|
|
(-1, 2),
|
|
(1, -2),
|
|
(1, 2),
|
|
(-2, -1),
|
|
(-2, 1),
|
|
(2, -1),
|
|
(2, 1),
|
|
];
|
|
visible.extend(knight_moves.iter().filter_map(|&mv| pos + mv));
|
|
}
|
|
PieceKind::Bishop => {
|
|
move_directions.extend([(1, 1), (-1, -1), (-1, 1), (1, -1)]);
|
|
}
|
|
PieceKind::Queen => {
|
|
move_directions.extend([
|
|
(0, 1),
|
|
(1, 0),
|
|
(-1, 0),
|
|
(1, 0),
|
|
(1, 1),
|
|
(-1, -1),
|
|
(-1, 1),
|
|
(1, -1),
|
|
]);
|
|
}
|
|
PieceKind::King => {
|
|
let king_moves: Vec<(i8, i8)> = vec![
|
|
(1, 0),
|
|
(-1, 0),
|
|
(1, 1),
|
|
(-1, 1),
|
|
(1, -1),
|
|
(-1, -1),
|
|
(0, -1),
|
|
(0, 1),
|
|
];
|
|
visible.extend(king_moves.iter().filter_map(|&mv| pos + mv));
|
|
}
|
|
}
|
|
|
|
for direction in move_directions.drain(..) {
|
|
visible.extend(Piece::get_visible_in_direction(direction, pos, state))
|
|
}
|
|
|
|
*self.visible.borrow_mut() = visible;
|
|
}
|
|
|
|
fn get_possible_moves(&self, piece_pos: &Position, state: &GameState) -> HashSet<Move> {
|
|
let mut possible_moves: HashSet<Move> = HashSet::new();
|
|
|
|
// for all pieces except pawn, visible squares are squares you can move to
|
|
// for pawns, visible squares are diagonally ahead, possible moves are either 1 or 2 squares
|
|
// directly ahead
|
|
if self.kind == PieceKind::Pawn {
|
|
let (opposite_color, starting_rank, one_square, two_square) = match self.color {
|
|
Color::Black => (Color::White, 6, (0, -1), (0, -2)),
|
|
Color::White => (Color::Black, 1, (0, 1), (0, 2)),
|
|
};
|
|
|
|
// can move to visible squares diagonally ahead of a piece of opposite color is there
|
|
// OR if it is the en passant target square
|
|
for pos in self.visible.borrow().iter() {
|
|
if let Some(piece) = state.board.get(pos) {
|
|
if piece.color == opposite_color {
|
|
// if moving to back rank, add a move for each promotion option, otherwise don't promote
|
|
if pos.rank == 0 || pos.rank == 7 {
|
|
possible_moves.insert(Move {
|
|
from: piece_pos.clone(),
|
|
to: pos.clone(),
|
|
promote_to: Some(PieceKind::Bishop),
|
|
});
|
|
possible_moves.insert(Move {
|
|
from: piece_pos.clone(),
|
|
to: pos.clone(),
|
|
promote_to: Some(PieceKind::Knight),
|
|
});
|
|
possible_moves.insert(Move {
|
|
from: piece_pos.clone(),
|
|
to: pos.clone(),
|
|
promote_to: Some(PieceKind::Rook),
|
|
});
|
|
possible_moves.insert(Move {
|
|
from: piece_pos.clone(),
|
|
to: pos.clone(),
|
|
promote_to: Some(PieceKind::Queen),
|
|
});
|
|
} else {
|
|
possible_moves.insert(Move {
|
|
from: piece_pos.clone(),
|
|
to: pos.clone(),
|
|
promote_to: None,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
if state.en_passant_target.as_ref() == Some(pos) {
|
|
possible_moves.insert(Move {
|
|
from: piece_pos.clone(),
|
|
to: pos.clone(),
|
|
promote_to: None,
|
|
});
|
|
}
|
|
}
|
|
|
|
// can move to square directly ahead if no piece is there
|
|
let one_square = (piece_pos + one_square).unwrap();
|
|
if state.board.get(&one_square).is_none() {
|
|
// if moving to back rank, add a move for each promotion option, otherwise don't promote
|
|
if one_square.rank == 0 || one_square.rank == 7 {
|
|
possible_moves.insert(Move {
|
|
from: piece_pos.clone(),
|
|
to: one_square.clone(),
|
|
promote_to: Some(PieceKind::Bishop),
|
|
});
|
|
possible_moves.insert(Move {
|
|
from: piece_pos.clone(),
|
|
to: one_square.clone(),
|
|
promote_to: Some(PieceKind::Knight),
|
|
});
|
|
possible_moves.insert(Move {
|
|
from: piece_pos.clone(),
|
|
to: one_square.clone(),
|
|
promote_to: Some(PieceKind::Rook),
|
|
});
|
|
possible_moves.insert(Move {
|
|
from: piece_pos.clone(),
|
|
to: one_square,
|
|
promote_to: Some(PieceKind::Queen),
|
|
});
|
|
} else {
|
|
possible_moves.insert(Move {
|
|
from: piece_pos.clone(),
|
|
to: one_square,
|
|
promote_to: None,
|
|
});
|
|
}
|
|
// can move two squares ahead if we are on starting rank
|
|
if piece_pos.rank == starting_rank {
|
|
let two_square = (piece_pos + two_square).unwrap();
|
|
if state.board.get(&two_square).is_none() {
|
|
possible_moves.insert(Move {
|
|
from: piece_pos.clone(),
|
|
to: two_square,
|
|
promote_to: None,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// for all other pieces, can move to any visible square unless it contains
|
|
// a piece of the same color (can capture opponent piece, but not own)
|
|
possible_moves = self
|
|
.visible
|
|
.borrow()
|
|
.iter()
|
|
.filter(|&pos| {
|
|
if let Some(piece) = state.board.get(pos) {
|
|
// space is occupied, check if it is same color or not
|
|
piece.color != self.color
|
|
} else {
|
|
// space is empty, so able to move there
|
|
true
|
|
}
|
|
})
|
|
.map(|pos| Move {
|
|
from: piece_pos.clone(),
|
|
to: pos.clone(),
|
|
promote_to: None,
|
|
})
|
|
.collect();
|
|
|
|
// check if king can castle
|
|
if self.kind == PieceKind::King {
|
|
// king side castle - must still be allowed and have two empty squares between
|
|
// king and rook, not visible to opponent
|
|
if state.castle.get(&self.color).unwrap().can_castle_king_side
|
|
&& state.board.get(&(piece_pos + (1, 0)).unwrap()).is_none()
|
|
&& state.board.get(&(piece_pos + (2, 0)).unwrap()).is_none()
|
|
&& !state
|
|
.visible
|
|
.get(&self.color.opposite())
|
|
.unwrap()
|
|
.contains(&(piece_pos + (1, 0)).unwrap())
|
|
&& !state.player_in_check(&self.color)
|
|
{
|
|
// move is represented as king moving two squares
|
|
let castle_pos = (piece_pos + (2, 0)).unwrap();
|
|
possible_moves.insert(Move {
|
|
from: piece_pos.clone(),
|
|
to: castle_pos,
|
|
promote_to: None,
|
|
});
|
|
}
|
|
// queen side castle - must still be allowed and have three empty squares between
|
|
// king and rook
|
|
if state.castle.get(&self.color).unwrap().can_castle_queen_side
|
|
&& state.board.get(&(piece_pos + (-1, 0)).unwrap()).is_none()
|
|
&& state.board.get(&(piece_pos + (-2, 0)).unwrap()).is_none()
|
|
&& state.board.get(&(piece_pos + (-3, 0)).unwrap()).is_none()
|
|
&& !state
|
|
.visible
|
|
.get(&self.color.opposite())
|
|
.unwrap()
|
|
.contains(&(piece_pos + (-1, 0)).unwrap())
|
|
&& !state.player_in_check(&self.color)
|
|
{
|
|
// move is represented as king moving two squares
|
|
let castle_pos = (piece_pos + (-2, 0)).unwrap();
|
|
possible_moves.insert(Move {
|
|
from: piece_pos.clone(),
|
|
to: castle_pos,
|
|
promote_to: None,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// final validation - possible moves must not lead to check
|
|
let possible_moves = possible_moves
|
|
.iter()
|
|
.filter(|&mv| {
|
|
let mut new_state = state.clone();
|
|
// apply move
|
|
new_state.apply_move(mv);
|
|
//new_state.generate_visible();
|
|
!new_state.player_in_check(&self.color)
|
|
})
|
|
.cloned()
|
|
.collect();
|
|
possible_moves
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
struct CastleState {
|
|
can_castle_king_side: bool,
|
|
can_castle_queen_side: bool,
|
|
}
|
|
|
|
pub(crate) enum GameOutcome {
|
|
WhiteWin,
|
|
BlackWin,
|
|
Draw,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
pub(crate) struct GameState {
|
|
board: HashMap<Position, Piece>,
|
|
visible: HashMap<Color, HashSet<Position>>,
|
|
en_passant_target: Option<Position>,
|
|
castle: HashMap<Color, CastleState>,
|
|
allowed_turn: Color,
|
|
}
|
|
|
|
impl GameState {
|
|
pub(crate) fn starting_positions() -> Self {
|
|
let mut this = Self::default();
|
|
|
|
for (file, rank, kind, color) in INITIAL_PIECES {
|
|
this.board.insert(
|
|
Position { file, rank },
|
|
Piece {
|
|
kind,
|
|
color,
|
|
visible: RefCell::new(HashSet::new()),
|
|
},
|
|
);
|
|
}
|
|
|
|
this.generate_visible();
|
|
|
|
this.castle.insert(
|
|
Color::Black,
|
|
CastleState {
|
|
can_castle_king_side: true,
|
|
can_castle_queen_side: true,
|
|
},
|
|
);
|
|
this.castle.insert(
|
|
Color::White,
|
|
CastleState {
|
|
can_castle_king_side: true,
|
|
can_castle_queen_side: true,
|
|
},
|
|
);
|
|
|
|
this
|
|
}
|
|
|
|
pub(crate) fn to_serializable(&self) -> PollResponse {
|
|
let board_state = self
|
|
.board
|
|
.iter()
|
|
.map(|(pos, piece)| {
|
|
(
|
|
api_types::File::from_number(pos.file).unwrap(),
|
|
api_types::Rank::from_number(pos.rank).unwrap(),
|
|
piece.kind.clone(),
|
|
piece.color.clone(),
|
|
)
|
|
})
|
|
.collect();
|
|
|
|
let game_state = api_types::GameState::from_color(self.allowed_turn.clone());
|
|
|
|
PollResponse {
|
|
board_state,
|
|
game_state,
|
|
}
|
|
}
|
|
|
|
fn generate_visible(&mut self) {
|
|
let mut black_visible = HashSet::new();
|
|
let mut white_visible = HashSet::new();
|
|
|
|
for (pos, piece) in self.board.iter() {
|
|
piece.generate_visible(pos, self);
|
|
match piece.color {
|
|
Color::Black => {
|
|
black_visible.extend(piece.visible.borrow().clone());
|
|
}
|
|
White => {
|
|
white_visible.extend(piece.visible.borrow().clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut visible = HashMap::new();
|
|
visible.insert(Color::Black, black_visible);
|
|
visible.insert(Color::White, white_visible);
|
|
|
|
self.visible = visible;
|
|
}
|
|
|
|
// determines if given color king is in check
|
|
fn player_in_check(&self, color: &Color) -> bool {
|
|
let opponent_visible = self.visible.get(&color.opposite()).unwrap();
|
|
let king_color_collection: Vec<(&Position, &Piece)> = self
|
|
.board
|
|
.iter()
|
|
.filter(|(_, piece)| piece.kind == PieceKind::King && &piece.color == color)
|
|
.collect();
|
|
let (king_pos, _king) = &king_color_collection[0];
|
|
opponent_visible.contains(king_pos)
|
|
}
|
|
|
|
fn get_possible_moves(&self, color: &Color) -> HashSet<Move> {
|
|
let mut possible_moves = HashSet::new();
|
|
for (pos, piece) in &self.board {
|
|
if &piece.color == color {
|
|
possible_moves.extend(piece.get_possible_moves(&pos, &self));
|
|
}
|
|
}
|
|
possible_moves
|
|
}
|
|
|
|
// returns outcome of game if it is over, otherwise returns None
|
|
fn get_outcome(&self) -> Option<GameOutcome> {
|
|
// checkmate
|
|
if self.player_in_check(&Color::White)
|
|
&& self.get_possible_moves(&Color::White).is_empty()
|
|
&& self.allowed_turn == Color::White
|
|
{
|
|
return Some(GameOutcome::BlackWin);
|
|
} else if self.player_in_check(&Color::Black)
|
|
&& self.get_possible_moves(&Color::Black).is_empty()
|
|
&& self.allowed_turn == Color::Black
|
|
{
|
|
return Some(GameOutcome::WhiteWin);
|
|
}
|
|
|
|
if !self.player_in_check(&Color::White)
|
|
&& self.get_possible_moves(&Color::White).is_empty()
|
|
&& self.allowed_turn == Color::White
|
|
{
|
|
return Some(GameOutcome::Draw);
|
|
} else if !self.player_in_check(&Color::Black)
|
|
&& self.get_possible_moves(&Color::Black).is_empty()
|
|
&& self.allowed_turn == Color::Black
|
|
{
|
|
return Some(GameOutcome::Draw);
|
|
}
|
|
|
|
// draw by insufficient material - occurs if both sides only have king, or king and minor piece
|
|
let mut is_insufficient = true;
|
|
if self.board.len() <= 4 {
|
|
let mut white_pieces = 0;
|
|
let mut black_pieces = 0;
|
|
for (_, piece) in &self.board {
|
|
if piece.kind == PieceKind::Rook
|
|
|| piece.kind == PieceKind::Queen
|
|
|| piece.kind == PieceKind::Pawn
|
|
{
|
|
is_insufficient = false;
|
|
} else {
|
|
if piece.color == Color::White {
|
|
white_pieces += 1;
|
|
} else {
|
|
black_pieces += 1;
|
|
}
|
|
}
|
|
}
|
|
if white_pieces > 2 || black_pieces > 2 {
|
|
is_insufficient = false;
|
|
}
|
|
}
|
|
if is_insufficient {
|
|
return Some(GameOutcome::Draw);
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn apply_move(&mut self, board_move: &Move) {
|
|
let from = board_move.from.clone();
|
|
let to = board_move.to.clone();
|
|
let piece = self.board.remove(&from).unwrap();
|
|
|
|
// if pawn is moving to en passant square, remove captured pawn
|
|
if piece.kind == PieceKind::Pawn && Some(&to) == self.en_passant_target.as_ref() {
|
|
let pawn_pos = &to
|
|
+ match piece.color {
|
|
Color::Black => (0, 1),
|
|
White => (0, -1),
|
|
};
|
|
self.board.remove(&pawn_pos.unwrap());
|
|
}
|
|
// update en passant square if a pawn moves 2 squares ahead
|
|
if piece.kind == PieceKind::Pawn
|
|
&& ((piece.color == Color::White && from.rank == 1 && to.rank == 3)
|
|
|| (piece.color == Color::Black && from.rank == 6 && to.rank == 4))
|
|
{
|
|
// en passant square is square behind where pawn moves to
|
|
self.en_passant_target = &to
|
|
+ match piece.color {
|
|
Color::Black => (0, 1),
|
|
White => (0, -1),
|
|
};
|
|
} else {
|
|
self.en_passant_target = None;
|
|
}
|
|
|
|
// check if we are castling
|
|
if piece.kind == PieceKind::King {
|
|
if self.castle.get(&piece.color).unwrap().can_castle_king_side
|
|
&& Some(&to) == (&from + (2, 0)).as_ref()
|
|
{
|
|
// perform king side castle by moving rook (king gets moved to to_pos)
|
|
let rook_pos = Position {
|
|
file: 7,
|
|
rank: to.rank,
|
|
};
|
|
if let Some(rook) = self.board.remove(&rook_pos) {
|
|
self.board.insert((&rook_pos + (-2, 0)).unwrap(), rook);
|
|
}
|
|
} else if self.castle.get(&piece.color).unwrap().can_castle_queen_side
|
|
&& Some(&to) == (&from + (-2, 0)).as_ref()
|
|
{
|
|
// perform queen side castle
|
|
let rook_pos = Position {
|
|
file: 0,
|
|
rank: to.rank,
|
|
};
|
|
if let Some(rook) = self.board.remove(&rook_pos) {
|
|
self.board.insert((&rook_pos + (3, 0)).unwrap(), rook);
|
|
}
|
|
}
|
|
// even if not castling, if you move king you can no longer castle
|
|
self.castle
|
|
.get_mut(&piece.color)
|
|
.unwrap()
|
|
.can_castle_queen_side = false;
|
|
self.castle
|
|
.get_mut(&piece.color)
|
|
.unwrap()
|
|
.can_castle_king_side = false;
|
|
}
|
|
|
|
// if moving rook from starting position, can no longer castle on that side
|
|
if piece.kind == PieceKind::Rook {
|
|
if from.file == 7 {
|
|
self.castle
|
|
.get_mut(&piece.color)
|
|
.unwrap()
|
|
.can_castle_king_side = false;
|
|
} else if from.file == 0 {
|
|
self.castle
|
|
.get_mut(&piece.color)
|
|
.unwrap()
|
|
.can_castle_queen_side = false;
|
|
}
|
|
}
|
|
|
|
// in the case of promotion, change the piecekind
|
|
let piece = match &board_move.promote_to {
|
|
None => piece,
|
|
Some(new_kind) => {
|
|
let mut piece = piece.clone();
|
|
piece.kind = new_kind.clone();
|
|
piece
|
|
}
|
|
};
|
|
|
|
// move the piece
|
|
self.board.insert(to, piece);
|
|
|
|
self.generate_visible()
|
|
}
|
|
|
|
pub(crate) fn handle(&mut self, action: api_types::Move) {
|
|
let board_move = Move::from_api_move(action);
|
|
|
|
let piece = match self.board.get(&board_move.from) {
|
|
None => {
|
|
panic!("No piece found at position specified in move");
|
|
}
|
|
Some(piece) => piece,
|
|
}
|
|
.clone();
|
|
|
|
if piece
|
|
.get_possible_moves(&board_move.from, self)
|
|
.contains(&board_move)
|
|
{
|
|
self.apply_move(&board_move);
|
|
self.allowed_turn = self.allowed_turn.opposite();
|
|
}
|
|
}
|
|
}
|
|
|
|
const INITIAL_PIECES: [(u8, u8, PieceKind, Color); 32] = [
|
|
(0, 0, PieceKind::Rook, Color::White),
|
|
(1, 0, PieceKind::Knight, Color::White),
|
|
(2, 0, PieceKind::Bishop, Color::White),
|
|
(3, 0, PieceKind::Queen, Color::White),
|
|
(4, 0, PieceKind::King, Color::White),
|
|
(5, 0, PieceKind::Bishop, Color::White),
|
|
(6, 0, PieceKind::Knight, Color::White),
|
|
(7, 0, PieceKind::Rook, Color::White),
|
|
(0, 1, PieceKind::Pawn, Color::White),
|
|
(1, 1, PieceKind::Pawn, Color::White),
|
|
(2, 1, PieceKind::Pawn, Color::White),
|
|
(3, 1, PieceKind::Pawn, Color::White),
|
|
(4, 1, PieceKind::Pawn, Color::White),
|
|
(5, 1, PieceKind::Pawn, Color::White),
|
|
(6, 1, PieceKind::Pawn, Color::White),
|
|
(7, 1, PieceKind::Pawn, Color::White),
|
|
(0, 6, PieceKind::Pawn, Color::Black),
|
|
(1, 6, PieceKind::Pawn, Color::Black),
|
|
(2, 6, PieceKind::Pawn, Color::Black),
|
|
(3, 6, PieceKind::Pawn, Color::Black),
|
|
(4, 6, PieceKind::Pawn, Color::Black),
|
|
(5, 6, PieceKind::Pawn, Color::Black),
|
|
(6, 6, PieceKind::Pawn, Color::Black),
|
|
(7, 6, PieceKind::Pawn, Color::Black),
|
|
(0, 7, PieceKind::Rook, Color::Black),
|
|
(1, 7, PieceKind::Knight, Color::Black),
|
|
(2, 7, PieceKind::Bishop, Color::Black),
|
|
(3, 7, PieceKind::Queen, Color::Black),
|
|
(4, 7, PieceKind::King, Color::Black),
|
|
(5, 7, PieceKind::Bishop, Color::Black),
|
|
(6, 7, PieceKind::Knight, Color::Black),
|
|
(7, 7, PieceKind::Rook, Color::Black),
|
|
];
|