From 96f3d91e4341295a8b69c003eaa8f1a982e642cb Mon Sep 17 00:00:00 2001 From: jan Date: Fri, 10 Apr 2026 20:47:51 +0200 Subject: [PATCH 1/4] Refactor: Made move internals private. --- src/backend/game_state/fen_parser.rs | 22 +++++++++------------- src/backend/game_state/state.rs | 22 +++++++++++----------- src/backend/movegen/move_gen_pawn.rs | 3 +-- src/backend/types/moove.rs | 28 ++++++++++++++++++++++++---- 4 files changed, 45 insertions(+), 30 deletions(-) diff --git a/src/backend/game_state/fen_parser.rs b/src/backend/game_state/fen_parser.rs index c0f3aec..4caf434 100644 --- a/src/backend/game_state/fen_parser.rs +++ b/src/backend/game_state/fen_parser.rs @@ -267,20 +267,16 @@ pub fn moove_from_uci_notation(uci_notation: &str) -> Moove { let to = square_from_uci_notation(&uci_notation[2..4]); let promotion_char = uci_notation.chars().nth(4); - let promotion_type = match promotion_char { - None => Option::None, - Some(char) => match char { - 'r' => Some(Rook), - 'n' => Some(Knight), - 'b' => Some(Bishop), - 'q' => Some(Queen), + if let Some(char) = promotion_char { + let promotion_type = match char { + 'r' => (Rook), + 'n' => (Knight), + 'b' => (Bishop), + 'q' => (Queen), _ => panic!("Invalid promotion type {:?}", uci_notation), - }, + }; + return Moove::new_promotion(from, to, promotion_type) }; - Moove { - from, - to, - promotion_type, - } + Moove::new(from, to) } diff --git a/src/backend/game_state/state.rs b/src/backend/game_state/state.rs index b97fa22..b5bf361 100644 --- a/src/backend/game_state/state.rs +++ b/src/backend/game_state/state.rs @@ -73,10 +73,10 @@ impl State { let mut next_ir_data = IrreversibleData::new_from_previous_state(&self.irreversible_data); // Get the type of moved piece. - let moved_piece = self.bb_manager.get_piece_at_square(moove.from).unwrap(); + let moved_piece = self.bb_manager.get_piece_at_square(moove.get_from()).unwrap(); // Usually the square something was captured on (if something was captured at all) is the square we moved to... - let mut capture_square = moove.to; + let mut capture_square = moove.get_to(); if moved_piece == Pawn { // ... unless this is an en passant capture, we then need to update the capture square. next_state.make_move_ep_capture(moove, &mut capture_square); @@ -91,26 +91,26 @@ impl State { let mut moved_piece_bb = next_state.bb_manager.get_piece_bb_mut(moved_piece); // Clear the square that the piece was moved from. - moved_piece_bb.clear_square(moove.from); + moved_piece_bb.clear_square(moove.get_from()); // Update the moved piece bb if it was a pawn promotion - match moove.promotion_type { + match moove.get_promotion_type() { None => {} Some(promotion_type) => { moved_piece_bb = next_state.bb_manager.get_piece_bb_mut(promotion_type); } } // Fill the square it moved to. - moved_piece_bb.fill_square(moove.to); + moved_piece_bb.fill_square(moove.get_to()); next_state .bb_manager .get_all_pieces_bb_off_mut(self.active_color) - .fill_square(moove.to); + .fill_square(moove.get_to()); next_state .bb_manager .get_all_pieces_bb_off_mut(self.active_color) - .clear_square(moove.from); + .clear_square(moove.get_from()); // Some special king handling if moved_piece == King { @@ -120,7 +120,7 @@ impl State { next_state.make_move_castling_rights_on_rook_move_or_capture( &mut next_ir_data, moved_piece, - moove.from, + moove.get_from(), self.active_color, ); @@ -136,10 +136,10 @@ impl State { // if an en passant square exists if let Some(ep_square) = ep_square // and if we moved to the ep_square - && ep_square == moove.to + && ep_square == moove.get_to() { // update the captured square to the ep_square - offset - *capture_square = back_by_one(moove.to, self.active_color); + *capture_square = back_by_one(moove.get_to(), self.active_color); } } @@ -178,7 +178,7 @@ impl State { ) { if moove.is_double_pawn_push() { // the pawn starting square and one forward - let ep_square = back_by_one(moove.to, self.active_color); + let ep_square = back_by_one(moove.get_to(), self.active_color); irreversible_data.en_passant_square = Some(ep_square); } diff --git a/src/backend/movegen/move_gen_pawn.rs b/src/backend/movegen/move_gen_pawn.rs index 1f050bf..f30cf88 100644 --- a/src/backend/movegen/move_gen_pawn.rs +++ b/src/backend/movegen/move_gen_pawn.rs @@ -172,8 +172,7 @@ fn pawn_bb_to_moves_promotion( let rank = get_rank(square); let offset_square = square_from_rank_and_file(rank + rank_offset, file + file_offset); for piece_type in PROMOTABLE_PIECES { - let mut moove = Moove::new(offset_square, square); - moove.promotion_type = Some(piece_type); + let mut moove = Moove::new_promotion(offset_square, square, piece_type); moves.push(moove); } } diff --git a/src/backend/types/moove.rs b/src/backend/types/moove.rs index fe9f7bb..9187cc4 100644 --- a/src/backend/types/moove.rs +++ b/src/backend/types/moove.rs @@ -30,9 +30,9 @@ impl CastleType { /// To "make" this move we then would only need to xor it with the bitboard for the piece. #[derive(Copy, Clone, Debug, Ord, Eq, PartialEq, PartialOrd)] pub struct Moove { - pub from: Square, - pub to: Square, - pub promotion_type: Option, + from: Square, + to: Square, + promotion_type: Option, } impl Moove { @@ -41,10 +41,30 @@ impl Moove { Moove { from, to, - promotion_type: Option::None, + promotion_type: None, } } + pub const fn new_promotion(from: Square, to: Square, promotion_type: Piece) -> Moove { + Moove { + from, + to, + promotion_type: Some(promotion_type), + } + } + + pub fn get_from(&self) -> Square { + self.from + } + + pub fn get_to(&self) -> Square { + self.to + } + + pub fn get_promotion_type(&self) -> Option { + self.promotion_type + } + /// This assumes that the moved piece is a pawn and only checks if the rank changed by 2. pub fn is_double_pawn_push(&self) -> bool { self.from.abs_diff(self.to) == 16 From 6cce349808dbeff1a9a9ec1b5bfad48a8bb4be88 Mon Sep 17 00:00:00 2001 From: jan Date: Fri, 10 Apr 2026 22:08:23 +0200 Subject: [PATCH 2/4] Feat: Moove is not a bitfield internally. --- src/backend/types/moove.rs | 69 +++++++++++++++++++------------------- src/backend/types/piece.rs | 7 ++-- src/main.rs | 2 +- 3 files changed, 40 insertions(+), 38 deletions(-) diff --git a/src/backend/types/moove.rs b/src/backend/types/moove.rs index 9187cc4..4110f11 100644 --- a/src/backend/types/moove.rs +++ b/src/backend/types/moove.rs @@ -1,5 +1,5 @@ use std::fmt::{Display, Formatter}; -use crate::backend::types::piece::Piece; +use crate::backend::types::piece::{Piece, PROMOTABLE_PIECES}; use crate::backend::types::square::{get_file, square_to_string, Square}; #[derive(Copy, Clone)] @@ -18,66 +18,65 @@ impl CastleType { /// It knows where a piece moved from and where it moved to. /// Also stores to which piece a pawn promoted if one did at all. /// -/// PERF: This could be squeezed into a bitfield like, for example, stockfish does: -/// Around line 370: https://github.com/official-stockfish/Stockfish/blob/master/src/types.h -/// I have not done this yet for two reasons: -/// 1. I'm not sure, without any benchmarks if it gains any performance. -/// Sure, the move would be smaller, but accessing a variable would be slower, since it requires bit shifting etc. -/// In the end it comes down to a trade-off between cache locality and number of instructions per read. -/// 2. It would certainly make the code less readable. -/// -/// Alternatively, each Move could store a bitboard, with one bit set where we currently are and one where we are going to. -/// To "make" this move we then would only need to xor it with the bitboard for the piece. -#[derive(Copy, Clone, Debug, Ord, Eq, PartialEq, PartialOrd)] +/// Based on: https://github.com/official-stockfish/Stockfish/blob/master/src/types.h +/// It's stored in 16 bits +/// The first six are for the from index, the next six for the to index, leaving us with 4 bits remaining. +/// Two of those are used to encode the type of promotion piece. Either Rook, Knight, Bishop, or Queen +/// The next stores whether promotion has occurred +#[derive(Copy, Clone, Debug, Eq, PartialEq)] pub struct Moove { - from: Square, - to: Square, - promotion_type: Option, + bitfield: u16 } impl Moove { /// Creates a new `Move` instance with 'promotion_type' set to 0. pub const fn new(from: Square, to: Square) -> Moove { - Moove { - from, - to, - promotion_type: None, - } + let mut result = from as u16 | ((to as u16) << 6); + result |= 0; + Moove { bitfield: result } } - pub const fn new_promotion(from: Square, to: Square, promotion_type: Piece) -> Moove { - Moove { - from, - to, - promotion_type: Some(promotion_type), - } + pub fn new_promotion(from: Square, to: Square, promotion_type: Piece) -> Moove { + let result = Moove { bitfield: from as u16 | ((to as u16) << 6) | (promotion_type as u16) << 12 | 1 << 14 }; + result } pub fn get_from(&self) -> Square { - self.from + let mask = 0b0000_0000_0011_1111u16; + (self.bitfield & mask) as Square } pub fn get_to(&self) -> Square { - self.to + let mask = 0b0000_1111_1100_0000u16; + ((self.bitfield & mask) >> 6) as Square } + // TODO: Using Option here surely wasted like 3 clock cycles in perft run pub fn get_promotion_type(&self) -> Option { - self.promotion_type + let promo_mask = 0b0100_0000_0000_0000u16; + if (self.bitfield & promo_mask) == 0 { + return None; + } + + let type_mask = 0b0011_0000_0000_0000u16; + let piece_index = (self.bitfield & type_mask) >> 12; + + Some(PROMOTABLE_PIECES[piece_index as usize]) } /// This assumes that the moved piece is a pawn and only checks if the rank changed by 2. pub fn is_double_pawn_push(&self) -> bool { - self.from.abs_diff(self.to) == 16 + self.get_from().abs_diff(self.get_to()) == 16 } /// This assumes that the moved piece is a king and only checks if the file changed by 2. /// TODO: Not sure if this is bug free but i think so lol pub fn is_castle(&self) -> bool { - self.from.abs_diff(self.to) == 2 + self.get_from().abs_diff(self.get_to()) == 2 } pub fn get_castle_type(&self) -> CastleType { - if get_file(self.to) == 6 { + if get_file(self.get_to()) == 6 { CastleType::Short } else { CastleType::Long @@ -90,9 +89,9 @@ impl Display for Moove { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let mut result = String::new(); - result.push_str(&square_to_string(self.from)); - result.push_str(&square_to_string(self.to)); - result.push_str(match self.promotion_type { + result.push_str(&square_to_string(self.get_from())); + result.push_str(&square_to_string(self.get_to())); + result.push_str(match self.get_promotion_type() { None => "", Some(promotion_type) => match promotion_type { Piece::Rook => "r", diff --git a/src/backend/types/piece.rs b/src/backend/types/piece.rs index e3ce9fa..a271ae6 100644 --- a/src/backend/types/piece.rs +++ b/src/backend/types/piece.rs @@ -1,21 +1,24 @@ /// Represents the different pieces. +/// +/// Dear god, this is a bit hacky. +/// Do not change the order of this to differ from the PROMOTABLE_PIECES list. #[derive(Copy, Clone, Debug, Ord, Eq, PartialEq, PartialOrd)] pub enum Piece { - Pawn, Rook, Knight, Bishop, Queen, King, + Pawn, } pub const ALL_PIECES: [Piece; 6] = [ - Piece::Pawn, Piece::Rook, Piece::Knight, Piece::Bishop, Piece::Queen, Piece::King, + Piece::Pawn, ]; pub const PROMOTABLE_PIECES: [Piece; 4] = [Piece::Rook, Piece::Knight, Piece::Bishop, Piece::Queen]; diff --git a/src/main.rs b/src/main.rs index c00c749..58ba24f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -48,7 +48,7 @@ pub fn root_debug_perft(root_state: &mut State, depth: u8) -> u64 { // Generate all root moves. let mut moves = get_pseudo_legal_moves(root_state); // Sort them in the same way as perftree does - moves.sort(); + // moves.sort(); for chess_move in moves { let state = root_state.make_move(chess_move); // If we are in check after making the move -> skip. From 4ac78804ae1d7be154804546c3bb23eb2bec2e80 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 12 Apr 2026 00:23:34 +0200 Subject: [PATCH 3/4] Feat: Final bit of polish. --- src/backend/movegen/move_gen_pawn.rs | 2 +- src/backend/types/moove.rs | 34 ++++++++++++++++++++++++++-- src/backend/types/piece.rs | 1 + src/main.rs | 2 +- 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/backend/movegen/move_gen_pawn.rs b/src/backend/movegen/move_gen_pawn.rs index f30cf88..e39b160 100644 --- a/src/backend/movegen/move_gen_pawn.rs +++ b/src/backend/movegen/move_gen_pawn.rs @@ -172,7 +172,7 @@ fn pawn_bb_to_moves_promotion( let rank = get_rank(square); let offset_square = square_from_rank_and_file(rank + rank_offset, file + file_offset); for piece_type in PROMOTABLE_PIECES { - let mut moove = Moove::new_promotion(offset_square, square, piece_type); + let moove = Moove::new_promotion(offset_square, square, piece_type); moves.push(moove); } } diff --git a/src/backend/types/moove.rs b/src/backend/types/moove.rs index 4110f11..ca629f9 100644 --- a/src/backend/types/moove.rs +++ b/src/backend/types/moove.rs @@ -1,3 +1,4 @@ +use std::cmp::Ordering; use std::fmt::{Display, Formatter}; use crate::backend::types::piece::{Piece, PROMOTABLE_PIECES}; use crate::backend::types::square::{get_file, square_to_string, Square}; @@ -23,6 +24,7 @@ impl CastleType { /// The first six are for the from index, the next six for the to index, leaving us with 4 bits remaining. /// Two of those are used to encode the type of promotion piece. Either Rook, Knight, Bishop, or Queen /// The next stores whether promotion has occurred +/// One bit and thus three possible data points free! #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub struct Moove { bitfield: u16 @@ -51,7 +53,6 @@ impl Moove { ((self.bitfield & mask) >> 6) as Square } - // TODO: Using Option here surely wasted like 3 clock cycles in perft run pub fn get_promotion_type(&self) -> Option { let promo_mask = 0b0100_0000_0000_0000u16; if (self.bitfield & promo_mask) == 0 { @@ -70,7 +71,6 @@ impl Moove { } /// This assumes that the moved piece is a king and only checks if the file changed by 2. - /// TODO: Not sure if this is bug free but i think so lol pub fn is_castle(&self) -> bool { self.get_from().abs_diff(self.get_to()) == 2 } @@ -105,3 +105,33 @@ impl Display for Moove { write!(f, "{}", result) } } + +/// Implements ordering. Needed to sort them when comparing with perftree output. +/// This should only be called during debugging, not for performance-critical operations. +impl PartialOrd for Moove { + fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } +} + +impl Ord for Moove { + fn cmp(&self, other: &Self) -> Ordering { + let own_from = self.get_from(); + let other_from = other.get_from(); + + let own_to = self.get_to(); + let other_to = self.get_to(); + + if own_from > other_from { + return Ordering::Greater; + } else if own_from < other_from { + return Ordering::Less; + } + + if own_to > other_to { + return Ordering::Greater; + } else if own_to < other_to { + return Ordering::Less; + } + Ordering::Equal + } +} + diff --git a/src/backend/types/piece.rs b/src/backend/types/piece.rs index a271ae6..cd47a99 100644 --- a/src/backend/types/piece.rs +++ b/src/backend/types/piece.rs @@ -12,6 +12,7 @@ pub enum Piece { Pawn, } +/// The order of this has to match with the Piece enum. pub const ALL_PIECES: [Piece; 6] = [ Piece::Rook, Piece::Knight, diff --git a/src/main.rs b/src/main.rs index 58ba24f..c00c749 100644 --- a/src/main.rs +++ b/src/main.rs @@ -48,7 +48,7 @@ pub fn root_debug_perft(root_state: &mut State, depth: u8) -> u64 { // Generate all root moves. let mut moves = get_pseudo_legal_moves(root_state); // Sort them in the same way as perftree does - // moves.sort(); + moves.sort(); for chess_move in moves { let state = root_state.make_move(chess_move); // If we are in check after making the move -> skip. From c8b6606923cb8574ca4a9d3e86c1e6629faff129 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 12 Apr 2026 00:30:40 +0200 Subject: [PATCH 4/4] Feat: Please clippy. --- src/backend/types/moove.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/backend/types/moove.rs b/src/backend/types/moove.rs index ca629f9..2983334 100644 --- a/src/backend/types/moove.rs +++ b/src/backend/types/moove.rs @@ -39,8 +39,7 @@ impl Moove { } pub fn new_promotion(from: Square, to: Square, promotion_type: Piece) -> Moove { - let result = Moove { bitfield: from as u16 | ((to as u16) << 6) | (promotion_type as u16) << 12 | 1 << 14 }; - result + Moove { bitfield: from as u16 | ((to as u16) << 6) | (promotion_type as u16) << 12 | 1 << 14 } } pub fn get_from(&self) -> Square {