bitstackchess 0.1.1

A bitboard‐based chess game engine with 10 × u128 move history
Documentation
//! A concrete `GameCore` struct that implements `ChessEngine` by composing:
//!   • `MovePlanes` for move‐history storage  
//!   • `PieceMapping` + `occupied: u64` + `captured_bits: u32` for board state  
//!   • The various `rules::…` modules for castling, en passant, promotion, and checkmate.

use crate::core::{Move10, MovePlanes};
use crate::board::{PieceMapping, Occupied, CapturedBits, init_chess_positions};
use crate::engine::engine_trait::{ChessEngine, MoveError};
use crate::rules::castling::CastlingLogic;
use crate::rules::en_passant::EnPassantLogic;
use crate::rules::checkmate::{CheckmateLogic, Color, GameResult};
use crate::engine::move_generator::MoveGenerator;

/// The main engine struct, holding board + move‐history + state.
pub struct GameCore {
    pub planes: MovePlanes,
    pub ply: usize,
    pub captured_bits: CapturedBits,
    pub occupied: Occupied,
    pub mapping: PieceMapping,
    pub en_passant_target: Option<u8>,
}

impl Default for GameCore {
    fn default() -> Self {
        let mut mapping = PieceMapping::new_empty();
        let mut occupied: u64 = 0;
        let mut captured_bits: u32 = 0;

        // Initialize starting position from board::init_chess_positions
        for &(pid, sq) in &init_chess_positions() {
            mapping.place_piece(pid, sq);
            occupied |= 1u64 << (sq as u64);
        }

        GameCore {
            planes: MovePlanes::new(),
            ply: 0,
            captured_bits,
            occupied,
            mapping,
            en_passant_target: None,
        }
    }
}

impl ChessEngine for GameCore {
    fn current_mapping(&self) -> &PieceMapping {
        &self.mapping
    }

    fn current_occupied(&self) -> Occupied {
        self.occupied
    }

    fn current_captured_bits(&self) -> CapturedBits {
        self.captured_bits
    }

    fn side_to_move(&self) -> Color {
        if self.ply % 2 == 0 {
            Color::White
        } else {
            Color::Black
        }
    }

    fn en_passant_target(&self) -> Option<u8> {
        self.en_passant_target
    }

    fn ply(&self) -> usize {
        self.ply
    }

    fn game_result(&self) -> GameResult {
        CheckmateLogic::game_result(
            &self.mapping,
            self.occupied,
            self.side_to_move(),
            &self.planes,
            self.ply,
            self.en_passant_target,
        )
    }

    fn read_ply(&self, i: usize) -> Move10 {
        self.planes.read_ply(i)
    }

    fn generate_pseudo_legal_moves(&self) -> Vec<Move10> {
        let side = if self.side_to_move() == Color::White { 0 } else { 1 };
        MoveGenerator::generate(
            &self.mapping,
            self.occupied,
            self.captured_bits,
            self.en_passant_target,
            side,
        )
    }

    fn push_move(&mut self, mv: Move10) -> Result<(), MoveError> {
        let side = if self.side_to_move() == Color::White { 0 } else { 1 };
        let pid_within = mv.piece_id();
        let global_pid = 16 * side + pid_within;

        // 1) Copy current board state for a legality check:
        let saved_mapping = self.mapping.clone();
        let saved_occupied = self.occupied;
        let saved_captured = self.captured_bits;
        let saved_ep = self.en_passant_target;

        // 2) Attempt to apply the move on a **clone** for check testing:
        {
            let mut temp_mapping = saved_mapping.clone();
            let mut temp_occupied = saved_occupied;
            let mut temp_captured = saved_captured;
            let mut temp_ep = saved_ep;
            let src_sq = temp_mapping.piece_square[global_pid as usize].ok_or(MoveError::IllegalMove)? as usize;
            let dst_sq = mv.dest() as usize;

            // --- Castling on temp:
            let is_castle = pid_within == 15
                && ((side == 0 && ((src_sq == 4 && dst_sq == 6) || (src_sq == 4 && dst_sq == 2)))
                    || (side == 1 && ((src_sq == 60 && dst_sq == 62) || (src_sq == 60 && dst_sq == 58))));
            if is_castle {
                if side == 0 && dst_sq == 6 {
                    CastlingLogic::do_white_kingside(&mut temp_mapping, &mut temp_occupied);
                    temp_ep = None;
                } else if side == 0 && dst_sq == 2 {
                    CastlingLogic::do_white_queenside(&mut temp_mapping, &mut temp_occupied);
                    temp_ep = None;
                } else if side == 1 && dst_sq == 62 {
                    CastlingLogic::do_black_kingside(&mut temp_mapping, &mut temp_occupied);
                    temp_ep = None;
                } else if side == 1 && dst_sq == 58 {
                    CastlingLogic::do_black_queenside(&mut temp_mapping, &mut temp_occupied);
                    temp_ep = None;
                }
            } else {
                // --- En passant capture on temp:
                let is_pawn = pid_within < 8;
                if is_pawn {
                    let sr = (src_sq >> 3) as i8;
                    let sf = (src_sq & 7) as i8;
                    let dr = (dst_sq >> 3) as i8;
                    let df = (dst_sq & 7) as i8;
                    if (df - sf).abs() == 1 && (dr - sr).abs() == 1 {
                        if let Some(ep) = temp_ep {
                            if ep as usize == dst_sq {
                                let cap_sq = if side == 0 { dst_sq - 8 } else { dst_sq + 8 };
                                if let Some(opp) = temp_mapping.who_on_square(cap_sq as u8) {
                                    temp_mapping.remove_piece(opp);
                                    temp_occupied &= !(1u64 << cap_sq);
                                    temp_captured |= 1u32 << (opp as u32);
                                }
                            }
                        }
                    }
                }
                // --- Normal capture on temp:
                if let Some(opp) = temp_mapping.who_on_square(dst_sq as u8) {
                    temp_mapping.remove_piece(opp);
                    temp_occupied &= !(1u64 << dst_sq);
                    temp_captured |= 1u32 << (opp as u32);
                }
                // --- Move piece on temp:
                temp_mapping.move_piece(global_pid as u8, dst_sq as u8);
                temp_occupied &= !(1u64 << (src_sq as u64));
                temp_occupied |= 1u64 << dst_sq;
                // --- Recompute temp_ep (pawn double-step?):
                if pid_within < 8 {
                    if let Some(new_ep) =
                        EnPassantLogic::compute_ep_target(src_sq as u8, dst_sq as u8, side)
                    {
                        temp_ep = Some(new_ep);
                    } else {
                        temp_ep = None;
                    }
                } else {
                    temp_ep = None;
                }
            }

            // --- Check king safety on temp:
            let king_pid = if side == 0 { 15 } else { 31 };
            if CheckmateLogic::is_in_check(&temp_mapping, temp_occupied, king_pid) {
                return Err(MoveError::KingInCheckAfterMove);
            }
        }

        // 3) Now that legality is confirmed, **apply** the move to `self`:
        let src_sq = self.mapping.piece_square[global_pid as usize].ok_or(MoveError::IllegalMove)? as usize;
        let dst_sq = mv.dest() as usize;

        let is_castle = pid_within == 15
            && ((side == 0 && ((src_sq == 4 && dst_sq == 6) || (src_sq == 4 && dst_sq == 2)))
                || (side == 1 && ((src_sq == 60 && dst_sq == 62) || (src_sq == 60 && dst_sq == 58))));
        if is_castle {
            if side == 0 && dst_sq == 6 {
                CastlingLogic::do_white_kingside(&mut self.mapping, &mut self.occupied);
                self.en_passant_target = None;
            } else if side == 0 && dst_sq == 2 {
                CastlingLogic::do_white_queenside(&mut self.mapping, &mut self.occupied);
                self.en_passant_target = None;
            } else if side == 1 && dst_sq == 62 {
                CastlingLogic::do_black_kingside(&mut self.mapping, &mut self.occupied);
                self.en_passant_target = None;
            } else if side == 1 && dst_sq == 58 {
                CastlingLogic::do_black_queenside(&mut self.mapping, &mut self.occupied);
                self.en_passant_target = None;
            }
        } else {
            // En-passant capture on real:
            let is_pawn = pid_within < 8;
            if is_pawn {
                let sr = (src_sq >> 3) as i8;
                let sf = (src_sq & 7) as i8;
                let dr = (dst_sq >> 3) as i8;
                let df = (dst_sq & 7) as i8;
                if (df - sf).abs() == 1 && (dr - sr).abs() == 1 {
                    if let Some(ep) = self.en_passant_target {
                        if ep as usize == dst_sq {
                            let cap_sq = if side == 0 { dst_sq - 8 } else { dst_sq + 8 };
                            if let Some(opp) = self.mapping.who_on_square(cap_sq as u8) {
                                self.mapping.remove_piece(opp);
                                self.occupied &= !(1u64 << cap_sq);
                                self.captured_bits |= 1u32 << (opp as u32);
                            }
                        }
                    }
                }
            }
            // Normal capture on real:
            if let Some(opp) = self.mapping.who_on_square(dst_sq as u8) {
                self.captured_bits |= 1u32 << (opp as u32);
                self.mapping.remove_piece(opp);
                self.occupied &= !(1u64 << dst_sq);
            }
            // Move piece on real:
            self.mapping.move_piece(global_pid as u8, dst_sq as u8);
            self.occupied &= !(1u64 << (src_sq as u64));
            self.occupied |= 1u64 << dst_sq;
            // Recompute ep for real:
            if is_pawn {
                if let Some(new_ep) =
                    EnPassantLogic::compute_ep_target(src_sq as u8, dst_sq as u8, side)
                {
                    self.en_passant_target = Some(new_ep);
                } else {
                    self.en_passant_target = None;
                }
            } else {
                self.en_passant_target = None;
            }
        }

        // 4) Record Move10 in planes & increment ply:
        self.planes.write_ply(self.ply, Some(mv));
        self.ply += 1;
        Ok(())
    }

    fn pop_move(&mut self) {
        if self.ply == 0 {
            return;
        }
        self.ply -= 1;
        let new_k = self.ply;

        self.planes.write_ply(new_k, None);

        // Reset state to starting position:
        self.captured_bits = 0;
        self.occupied = 0;
        self.mapping = PieceMapping::new_empty();
        self.en_passant_target = None;
        for &(pid, sq) in &init_chess_positions() {
            self.mapping.place_piece(pid, sq);
            self.occupied |= 1u64 << (sq as u64);
        }

        // Replay plies 0..new_k
        for i in 0..new_k {
            let mv = self.planes.read_ply(i);
            let side = (i % 2) as u8;
            let pid_within = mv.piece_id();
            let global_pid = 16 * side + pid_within;
            let src = self.mapping.piece_square[global_pid as usize].unwrap() as usize;
            let dest = mv.dest() as usize;

            // --- Castling on replay:
            let is_castle = pid_within == 15
                && ((side == 0 && ((src == 4 && dest == 6) || (src == 4 && dest == 2)))
                    || (side == 1 && ((src == 60 && dest == 62) || (src == 60 && dest == 58))));
            if is_castle {
                if side == 0 && dest == 6 {
                    CastlingLogic::do_white_kingside(&mut self.mapping, &mut self.occupied);
                    self.en_passant_target = None;
                    continue;
                } else if side == 0 && dest == 2 {
                    CastlingLogic::do_white_queenside(&mut self.mapping, &mut self.occupied);
                    self.en_passant_target = None;
                    continue;
                } else if side == 1 && dest == 62 {
                    CastlingLogic::do_black_kingside(&mut self.mapping, &mut self.occupied);
                    self.en_passant_target = None;
                    continue;
                } else if side == 1 && dest == 58 {
                    CastlingLogic::do_black_queenside(&mut self.mapping, &mut self.occupied);
                    self.en_passant_target = None;
                    continue;
                }
            }

            // --- En passant on replay:
            let is_pawn = pid_within < 8;
            if is_pawn {
                let src_rank = (src >> 3) as i8;
                let src_file = (src & 7) as i8;
                let dst_rank = (dest >> 3) as i8;
                let dst_file = (dest & 7) as i8;
                if (dst_file - src_file).abs() == 1 && (dst_rank - src_rank).abs() == 1 {
                    if let Some(ep_sq) = self.en_passant_target {
                        if ep_sq as usize == dest {
                            let captured_sq =
                                if side == 0 { (dest as i8 - 8) as u8 } else { (dest as i8 + 8) as u8 };
                            if let Some(opp_pid) = self.mapping.who_on_square(captured_sq) {
                                self.captured_bits |= 1u32 << (opp_pid as u32);
                                self.mapping.remove_piece(opp_pid);
                                self.occupied &= !(1u64 << (captured_sq as u64));
                            }
                        }
                    }
                }
            }

            // --- Normal capture on replay:
            if let Some(opp_pid) = self.mapping.who_on_square(dest as u8) {
                self.captured_bits |= 1u32 << (opp_pid as u32);
                self.mapping.remove_piece(opp_pid);
                self.occupied &= !(1u64 << (dest as u64));
            }

            // --- Move piece on replay:
            self.mapping.move_piece(global_pid as u8, dest as u8);
            self.occupied &= !(1u64 << (src as u64));
            self.occupied |= 1u64 << (dest as u64);

            // --- Recompute en passant on replay:
            if is_pawn {
                if let Some(ep_sq) = EnPassantLogic::compute_ep_target(src as u8, dest as u8, side) {
                    self.en_passant_target = Some(ep_sq);
                } else {
                    self.en_passant_target = None;
                }
            } else {
                self.en_passant_target = None;
            }
        }
    }

    fn reset(&mut self) {
        *self = GameCore::default();
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::board::{init_chess_positions, encode_piece, encode_square};

    #[test]
    fn starting_position_correct() {
        let engine = GameCore::default();
        for &(pid, sq) in &init_chess_positions() {
            assert_eq!(engine.mapping.piece_square[pid as usize], Some(sq));
            assert_eq!((engine.occupied >> (sq as u64)) & 1u64, 1u64);
        }
        assert_eq!(engine.ply(), 0);
        assert_eq!(engine.en_passant_target, None);
        assert_eq!(engine.game_result(), GameResult::Ongoing);
    }
}