[board, core, moves, position] Implement castling

Implement a new method on Position that evaluates whether the active color can castle
on a given wing of the board. Then, implement making a castling move in the position.

Make a new Wing enum in the core crate to specify kingside or queenside. Replace the
Castle enum from the board crate with this one. This caused a lot of churn...

Along the way fix a bunch of tests.

Note: there's still no way to actually make a castling move in explorer.
This commit is contained in:
Eryn Wells 2025-05-19 16:50:30 -07:00
parent 6816e350eb
commit 0c1863acb9
18 changed files with 499 additions and 258 deletions

View file

@ -1,8 +1,8 @@
// Eryn Wells <eryn@erynwells.me>
use crate::{defs::Kind, Move, PromotionShape};
use chessfriend_board::{castle, en_passant::EnPassant};
use chessfriend_core::{Color, File, PlacedPiece, Rank, Square};
use chessfriend_board::{en_passant::EnPassant, CastleParameters};
use chessfriend_core::{Color, File, PlacedPiece, Rank, Square, Wing};
use std::result::Result as StdResult;
use thiserror::Error;
@ -92,7 +92,7 @@ pub struct Promotion<S> {
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Castle {
color: Color,
castle: castle::Castle,
wing: Wing,
}
impl Style for Null {}
@ -119,13 +119,13 @@ impl Style for Capture {
impl Style for Castle {
fn origin_square(&self) -> Option<Square> {
let parameters = self.castle.parameters(self.color);
Some(parameters.king_origin_square())
let parameters = CastleParameters::get(self.color, self.wing);
Some(parameters.origin.king)
}
fn target_square(&self) -> Option<Square> {
let parameters = self.castle.parameters(self.color);
Some(parameters.king_target_square())
let parameters = CastleParameters::get(self.color, self.wing);
Some(parameters.target.king)
}
}
@ -255,9 +255,9 @@ impl Builder<Null> {
}
#[must_use]
pub fn castling(color: Color, castle: castle::Castle) -> Builder<Castle> {
pub fn castling(color: Color, wing: Wing) -> Builder<Castle> {
Builder {
style: Castle { color, castle },
style: Castle { color, wing },
}
}
@ -357,9 +357,9 @@ impl Builder<Push> {
impl Builder<Castle> {
fn bits(&self) -> u16 {
let bits = match self.style.castle {
castle::Castle::KingSide => Kind::KingSideCastle,
castle::Castle::QueenSide => Kind::QueenSideCastle,
let bits = match self.style.wing {
Wing::KingSide => Kind::KingSideCastle,
Wing::QueenSide => Kind::QueenSideCastle,
};
bits as u16
@ -403,6 +403,11 @@ impl Builder<EnPassantCapture> {
Move(Kind::EnPassantCapture as u16 | self.style.move_bits_unchecked())
}
/// Build an en passant move.
///
/// ## Errors
///
/// Returns an error if the target or origin squares are invalid.
pub fn build(&self) -> Result {
Ok(Move(
Kind::EnPassantCapture as u16 | self.style.move_bits()?,

View file

@ -2,8 +2,7 @@
use crate::builder::Builder;
use crate::defs::Kind;
use chessfriend_board::castle::Castle;
use chessfriend_core::{Rank, Shape, Square};
use chessfriend_core::{Rank, Shape, Square, Wing};
use std::fmt;
/// A single player's move. In game theory parlance, this is a "ply".
@ -62,10 +61,10 @@ impl Move {
}
#[must_use]
pub fn castle(&self) -> Option<Castle> {
pub fn castle(&self) -> Option<Wing> {
match self.flags() {
0b0010 => Some(Castle::KingSide),
0b0011 => Some(Castle::QueenSide),
0b0010 => Some(Wing::KingSide),
0b0011 => Some(Wing::QueenSide),
_ => None,
}
}
@ -77,7 +76,7 @@ impl Move {
#[must_use]
pub fn is_en_passant(&self) -> bool {
self.0 == Kind::EnPassantCapture as u16
self.flags() == Kind::EnPassantCapture as u16
}
#[must_use]
@ -130,8 +129,8 @@ impl fmt::Display for Move {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(castle) = self.castle() {
return match castle {
Castle::KingSide => write!(f, "{KINGSIDE_CASTLE_STR}"),
Castle::QueenSide => write!(f, "{QUEENSIDE_CASTLE_STR}"),
Wing::KingSide => write!(f, "{KINGSIDE_CASTLE_STR}"),
Wing::QueenSide => write!(f, "{QUEENSIDE_CASTLE_STR}"),
};
}

View file

@ -1,7 +1,6 @@
// Eryn Wells <eryn@erynwells.me>
use chessfriend_board::castle::Castle;
use chessfriend_core::{piece, Color, File, Shape, Square};
use chessfriend_core::{piece, Color, File, Shape, Square, Wing};
use chessfriend_moves::{testing::*, Builder, PromotionShape};
macro_rules! assert_flag {
@ -58,51 +57,50 @@ fn move_flags_capture() -> TestResult {
#[test]
fn move_flags_en_passant_capture() -> TestResult {
let mv = unsafe {
Builder::new()
.from(Square::A4)
.capturing_en_passant_on(Square::B3)
.build_unchecked()
};
let ply = Builder::new()
.from(Square::A4)
.capturing_en_passant_on(Square::B3)
.build()?;
assert_flags!(mv, false, false, true, true, false, false);
assert_eq!(mv.origin_square(), Square::A4);
assert_eq!(mv.target_square(), Square::B3);
assert_eq!(mv.capture_square(), Some(Square::B4));
assert!(ply.is_en_passant());
assert_eq!(ply.origin_square(), Square::A4);
assert_eq!(ply.target_square(), Square::B3);
assert_eq!(ply.capture_square(), Some(Square::B4));
Ok(())
}
#[test]
fn move_flags_promotion() -> TestResult {
let mv = Builder::push(&piece!(White Pawn on H7))
let ply = Builder::push(&piece!(White Pawn on H7))
.to(Square::H8)
.promoting_to(PromotionShape::Queen)
.build()?;
assert_flags!(mv, false, false, false, false, false, true);
assert_eq!(mv.promotion(), Some(Shape::Queen));
assert!(ply.is_promotion());
assert_eq!(ply.promotion(), Some(Shape::Queen));
Ok(())
}
#[test]
fn move_flags_capture_promotion() -> TestResult {
let mv = Builder::push(&piece!(White Pawn on H7))
let ply = Builder::push(&piece!(White Pawn on H7))
.to(Square::H8)
.capturing_piece(&piece!(Black Knight on G8))
.promoting_to(PromotionShape::Queen)
.build()?;
assert_flags!(mv, false, false, false, true, false, true);
assert_eq!(mv.promotion(), Some(Shape::Queen));
assert!(ply.is_capture());
assert!(ply.is_promotion());
assert_eq!(ply.promotion(), Some(Shape::Queen));
Ok(())
}
#[test]
fn move_flags_castle() -> TestResult {
let mv = Builder::castling(Color::White, Castle::KingSide).build()?;
let mv = Builder::castling(Color::White, Wing::KingSide).build()?;
assert_flags!(mv, false, false, false, false, true, false);