feat: basic make move
This commit is contained in:
parent
8db3a236c0
commit
c29a7ff789
@ -1,5 +1,7 @@
|
|||||||
use crate::{BoardState, CastleRights, ColPiece, Color, Square, BOARD_HEIGHT, BOARD_WIDTH};
|
use crate::{BoardState, CastleRights, ColPiece, Color, Square, BOARD_HEIGHT, BOARD_WIDTH};
|
||||||
|
|
||||||
|
pub const START_POSITION: &str = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
|
||||||
|
|
||||||
pub trait FromFen {
|
pub trait FromFen {
|
||||||
type Error;
|
type Error;
|
||||||
fn from_fen(_: String) -> Result<Self, Self::Error>
|
fn from_fen(_: String) -> Result<Self, Self::Error>
|
||||||
|
38
src/lib.rs
38
src/lib.rs
@ -111,18 +111,19 @@ impl ColPiece {
|
|||||||
struct Square(usize);
|
struct Square(usize);
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum IndexError {
|
enum SquareError {
|
||||||
OutOfBounds,
|
OutOfBounds,
|
||||||
|
InvalidCharacter(char),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<usize> for Square {
|
impl TryFrom<usize> for Square {
|
||||||
type Error = IndexError;
|
type Error = SquareError;
|
||||||
|
|
||||||
fn try_from(value: usize) -> Result<Self, Self::Error> {
|
fn try_from(value: usize) -> Result<Self, Self::Error> {
|
||||||
if (0..N_SQUARES).contains(&value) {
|
if (0..N_SQUARES).contains(&value) {
|
||||||
Ok(Square(value))
|
Ok(Square(value))
|
||||||
} else {
|
} else {
|
||||||
Err(IndexError::OutOfBounds)
|
Err(SquareError::OutOfBounds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -132,7 +133,7 @@ impl From<Square> for usize {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl Square {
|
impl Square {
|
||||||
fn from_row_col(r: usize, c: usize) -> Result<Self, IndexError> {
|
fn from_row_col(r: usize, c: usize) -> Result<Self, SquareError> {
|
||||||
//! Get index of square based on row and column.
|
//! Get index of square based on row and column.
|
||||||
let ret = BOARD_WIDTH * r + c;
|
let ret = BOARD_WIDTH * r + c;
|
||||||
ret.try_into()
|
ret.try_into()
|
||||||
@ -154,6 +155,27 @@ impl Square {
|
|||||||
let file = letters[col];
|
let file = letters[col];
|
||||||
format!("{file}{rank}")
|
format!("{file}{rank}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert typical human-readable form (e.g. `e4`) to square index.
|
||||||
|
fn from_algebraic(value: String) -> Result<Self, SquareError> {
|
||||||
|
let bytes = value.as_bytes();
|
||||||
|
let col = match bytes[0] as char {
|
||||||
|
'a' => 0,
|
||||||
|
'b' => 1,
|
||||||
|
'c' => 2,
|
||||||
|
'd' => 3,
|
||||||
|
'e' => 4,
|
||||||
|
'f' => 5,
|
||||||
|
'g' => 6,
|
||||||
|
'h' => 7,
|
||||||
|
_ => {return Err(SquareError::InvalidCharacter(bytes[0] as char))}
|
||||||
|
};
|
||||||
|
if let Some(row) = (bytes[1] as char).to_digit(10) {
|
||||||
|
Square::from_row_col(row as usize - 1, col as usize)
|
||||||
|
} else {
|
||||||
|
Err(SquareError::InvalidCharacter(bytes[1] as char))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -161,9 +183,11 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_to_algebraic() {
|
fn test_to_from_algebraic() {
|
||||||
for (sqr, idx) in [("a1", 0), ("a8", 56), ("h1", 7), ("h8", 63)] {
|
let test_cases = [("a1", 0), ("a8", 56), ("h1", 7), ("h8", 63)];
|
||||||
assert_eq!(Square::try_from(idx).unwrap().to_algebraic(), sqr)
|
for (sqr, idx) in test_cases {
|
||||||
|
assert_eq!(Square::try_from(idx).unwrap().to_algebraic(), sqr);
|
||||||
|
assert_eq!(Square::from_algebraic(sqr.to_string()).unwrap(), Square::try_from(idx).unwrap());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
146
src/movegen.rs
146
src/movegen.rs
@ -1,6 +1,7 @@
|
|||||||
//! Move generation.
|
//! Move generation.
|
||||||
|
|
||||||
use crate::{Color, Square, BoardState};
|
use crate::fen::{FromFen, ToFen, START_POSITION};
|
||||||
|
use crate::{BoardState, Color, Square, BOARD_WIDTH};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
/// Game tree node.
|
/// Game tree node.
|
||||||
@ -11,6 +12,16 @@ struct Node {
|
|||||||
prev: Option<Rc<Node>>,
|
prev: Option<Rc<Node>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for Node {
|
||||||
|
fn default() -> Self {
|
||||||
|
Node {
|
||||||
|
pos: BoardState::from_fen(START_POSITION.to_string())
|
||||||
|
.expect("Starting FEN should be valid"),
|
||||||
|
prev: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Piece enum specifically for promotions.
|
/// Piece enum specifically for promotions.
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
enum PromotePiece {
|
enum PromotePiece {
|
||||||
@ -26,15 +37,17 @@ struct MoveData {
|
|||||||
dest: Square,
|
dest: Square,
|
||||||
}
|
}
|
||||||
/// Pseudo-legal move.
|
/// Pseudo-legal move.
|
||||||
|
///
|
||||||
|
/// No checking is made to see if the move is actually pseudo-legal.
|
||||||
enum Move {
|
enum Move {
|
||||||
/// Pawn promotes to another piece.
|
/// Pawn promotes to another piece.
|
||||||
Promotion { data: MoveData, piece: PromotePiece },
|
Promotion(MoveData, PromotePiece),
|
||||||
/// King castles with rook.
|
/// King castles with rook.
|
||||||
Castle { data: MoveData },
|
Castle(MoveData),
|
||||||
/// Capture, or push move.
|
/// Capture, or push move.
|
||||||
Normal { data: MoveData },
|
Normal(MoveData),
|
||||||
/// This move is an en-passant capture.
|
/// This move is an en-passant capture.
|
||||||
EnPassant { data: MoveData },
|
EnPassant(MoveData),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Move {
|
impl Move {
|
||||||
@ -53,16 +66,127 @@ impl Move {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
Move::Promotion { data, piece } => todo!(),
|
Move::Promotion(data, piece) => todo!(),
|
||||||
Move::Castle { data } => todo!(),
|
Move::Castle(data) => todo!(),
|
||||||
Move::Normal { data } => {
|
Move::Normal(data) => {
|
||||||
let pc = node.pos.get_piece(data.src).unwrap();
|
let pc_src = node.pos.get_piece(data.src).unwrap();
|
||||||
|
if matches!(pc_src.pc, crate::Piece::Pawn) {
|
||||||
|
// pawn moves are irreversible
|
||||||
|
node.pos.half_moves = 0;
|
||||||
|
|
||||||
|
// en-passant
|
||||||
|
if data.src.0 + (BOARD_WIDTH) * 2 == data.dest.0 {
|
||||||
|
node.pos.ep_square = Some(
|
||||||
|
Square::try_from(data.src.0 + BOARD_WIDTH)
|
||||||
|
.expect("En-passant target should be valid."),
|
||||||
|
)
|
||||||
|
} else if data.dest.0 + (BOARD_WIDTH) * 2 == data.src.0 {
|
||||||
|
node.pos.ep_square = Some(
|
||||||
|
Square::try_from(data.src.0 - BOARD_WIDTH)
|
||||||
|
.expect("En-passant target should be valid."),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
node.pos.ep_square = None;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
node.pos.half_moves += 1;
|
||||||
|
node.pos.ep_square = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(_pc_dest) = node.pos.get_piece(data.dest) {
|
||||||
|
// captures are irreversible
|
||||||
|
node.pos.half_moves = 0;
|
||||||
|
}
|
||||||
|
|
||||||
node.pos.del_piece(data.src);
|
node.pos.del_piece(data.src);
|
||||||
node.pos.set_piece(data.dest, pc);
|
node.pos.set_piece(data.dest, pc_src);
|
||||||
}
|
}
|
||||||
Move::EnPassant { data } => todo!(),
|
Move::EnPassant(data) => todo!(),
|
||||||
}
|
}
|
||||||
|
|
||||||
node
|
node
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::fen::START_POSITION;
|
||||||
|
|
||||||
|
/// Test that make move works for regular piece pushes and captures.
|
||||||
|
///
|
||||||
|
/// Also tests en passant target square.
|
||||||
|
#[test]
|
||||||
|
fn test_normal_move() {
|
||||||
|
let start_pos = START_POSITION;
|
||||||
|
// (src, dest, expected fen)
|
||||||
|
// FENs made with https://lichess.org/analysis
|
||||||
|
// En-passant target square is manually added, since Lichess doesn't have it when
|
||||||
|
// en-passant is not legal.
|
||||||
|
let moves = [
|
||||||
|
(
|
||||||
|
"e2",
|
||||||
|
"e4",
|
||||||
|
"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"e7",
|
||||||
|
"e5",
|
||||||
|
"rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq e6 0 2",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"g1",
|
||||||
|
"f3",
|
||||||
|
"rnbqkbnr/pppp1ppp/8/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"g8",
|
||||||
|
"f6",
|
||||||
|
"rnbqkb1r/pppp1ppp/5n2/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 2 3",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"f1",
|
||||||
|
"c4",
|
||||||
|
"rnbqkb1r/pppp1ppp/5n2/4p3/2B1P3/5N2/PPPP1PPP/RNBQK2R b KQkq - 3 3",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"f8",
|
||||||
|
"c5",
|
||||||
|
"rnbqk2r/pppp1ppp/5n2/2b1p3/2B1P3/5N2/PPPP1PPP/RNBQK2R w KQkq - 4 4",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"d1",
|
||||||
|
"e2",
|
||||||
|
"rnbqk2r/pppp1ppp/5n2/2b1p3/2B1P3/5N2/PPPPQPPP/RNB1K2R b KQkq - 5 4",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"d8",
|
||||||
|
"e7",
|
||||||
|
"rnb1k2r/ppppqppp/5n2/2b1p3/2B1P3/5N2/PPPPQPPP/RNB1K2R w KQkq - 6 5",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"f3",
|
||||||
|
"e5",
|
||||||
|
"rnb1k2r/ppppqppp/5n2/2b1N3/2B1P3/8/PPPPQPPP/RNB1K2R b KQkq - 0 5",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"e7",
|
||||||
|
"e5",
|
||||||
|
"rnb1k2r/pppp1ppp/5n2/2b1q3/2B1P3/8/PPPPQPPP/RNB1K2R w KQkq - 0 6",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut node = Node::default();
|
||||||
|
|
||||||
|
for (src, dest, expect_fen) in moves {
|
||||||
|
let idx_src = Square::from_algebraic(src.to_string()).unwrap();
|
||||||
|
let idx_dest = Square::from_algebraic(dest.to_string()).unwrap();
|
||||||
|
let mv = Move::Normal(MoveData {
|
||||||
|
src: idx_src,
|
||||||
|
dest: idx_dest,
|
||||||
|
});
|
||||||
|
node = mv.make(node);
|
||||||
|
assert_eq!(node.pos.to_fen(), expect_fen.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user