Skip to main content

atomic_movegen/
board.rs

1use crate::attacks;
2use crate::bitboard::*;
3use crate::types::*;
4use std::error::Error;
5use std::fmt;
6
7/// Errors that can occur when parsing a FEN string.
8#[derive(Debug)]
9pub enum FenError {
10    /// Fewer than 4 space-separated fields.
11    TooShort {
12        /// Number of fields actually found.
13        parts: usize,
14    },
15    /// The board section does not contain exactly 8 ranks.
16    WrongRankCount {
17        /// Expected number of ranks (always 8).
18        expected: u8,
19        /// Number of ranks found.
20        got: usize,
21    },
22    /// Invalid side-to-move field (expected `"w"` or `"b"`).
23    InvalidSideToMove(String),
24    /// Invalid castling rights field.
25    InvalidCastling(String),
26    /// Invalid en-passant target square.
27    InvalidEpSquare(String),
28    /// Integer parse failure in the move-clock or full-move fields.
29    ParseInt(String),
30}
31
32impl fmt::Display for FenError {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        match self {
35            FenError::TooShort { parts } => {
36                write!(f, "FEN too short: expected at least 4 parts, got {parts}")
37            }
38            FenError::WrongRankCount { expected, got } => {
39                write!(f, "Expected {expected} ranks in FEN, got {got}")
40            }
41            FenError::InvalidSideToMove(v) => write!(f, "Invalid side to move: {v}"),
42            FenError::InvalidCastling(v) => write!(f, "Invalid castling rights: {v}"),
43            FenError::InvalidEpSquare(v) => write!(f, "Invalid en-passant square: {v}"),
44            FenError::ParseInt(v) => write!(f, "Failed to parse integer: {v}"),
45        }
46    }
47}
48
49impl Error for FenError {}
50
51fn char_to_piece(c: char) -> Option<Piece> {
52    match c {
53        'P' => Some(W_PAWN),
54        'N' => Some(W_KNIGHT),
55        'B' => Some(W_BISHOP),
56        'R' => Some(W_ROOK),
57        'Q' => Some(W_QUEEN),
58        'C' => Some(W_COMMONER),
59        'K' => Some(W_COMMONER),
60        'p' => Some(B_PAWN),
61        'n' => Some(B_KNIGHT),
62        'b' => Some(B_BISHOP),
63        'r' => Some(B_ROOK),
64        'q' => Some(B_QUEEN),
65        'c' => Some(B_COMMONER),
66        'k' => Some(B_COMMONER),
67        _ => None,
68    }
69}
70
71/// Cached state for a position, used during move generation and legality checks.
72///
73/// Fields are populated by [`Board::populate_state`] and consumed by
74/// [`Board::legal`] and [`generate_legal`](crate::movegen::generate_legal).
75#[derive(Debug, Clone, Copy)]
76pub struct StateInfo {
77    // Hot fields (read in legal() and generate_legal())
78    pub checkers: Bitboard,
79    pub pinned: Bitboard,
80    pub commoners_count: u32,
81    pub them_commoners_count: u32,
82
83    // Cold fields (read in undo_move, write in do_move / populate_state)
84    pub castling_rights: u8,
85    pub ep_square: Option<Square>,
86    pub rule50: u8,
87    pub captured_count: u8,
88    pub captured: [(Square, Piece); 9],
89    pub cap_sq: Option<Square>,
90    pub cap_piece: Piece,
91}
92
93impl Default for StateInfo {
94    fn default() -> Self {
95        Self::new()
96    }
97}
98
99impl StateInfo {
100    /// Create a new `StateInfo` with all fields zeroed/empty.
101    pub fn new() -> Self {
102        StateInfo {
103            checkers: Bitboard::EMPTY,
104            pinned: Bitboard::EMPTY,
105            commoners_count: 0,
106            them_commoners_count: 0,
107            castling_rights: 0,
108            ep_square: None,
109            rule50: 0,
110            captured_count: 0,
111            captured: [(Square::NONE, NO_PIECE); 9],
112            cap_sq: None,
113            cap_piece: NO_PIECE,
114        }
115    }
116}
117
118/// A chessboard with atomic chess rules.
119///
120/// Maintains piece placement, side-to-move, castling rights, en-passant
121/// square, and the half-move clock. Supports FEN serialization/deserialization,
122/// making and unmaking moves, and legality checking.
123#[derive(Debug, Clone)]
124pub struct Board {
125    squares: [Piece; 64],
126    by_color: [Bitboard; 2],
127    by_type: [Bitboard; 6],
128    side_to_move: Color,
129    castling_rights: u8,
130    ep_square: Option<Square>,
131    rule50: u8,
132    game_ply: u16,
133}
134
135pub(crate) const WK_CASTLE: u8 = 1;
136pub(crate) const WQ_CASTLE: u8 = 2;
137pub(crate) const BK_CASTLE: u8 = 4;
138pub(crate) const BQ_CASTLE: u8 = 8;
139
140impl Default for Board {
141    fn default() -> Self {
142        Self::new()
143    }
144}
145
146impl Board {
147    /// Create a board in the standard starting position.
148    pub fn new() -> Self {
149        let fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
150        Board::from_fen(fen).expect("Failed to create starting position")
151    }
152
153    /// Parse a board from a FEN string.
154    ///
155    /// Accepts standard FEN with 4–6 space-separated fields. The piece
156    /// character set includes standard chess pieces plus `C`/`c` and `K`/`k`
157    /// for commoners (king-like pseudo-royal pieces).
158    pub fn from_fen(fen: &str) -> Result<Self, FenError> {
159        let parts: Vec<&str> = fen.split_whitespace().collect();
160        if parts.len() < 4 {
161            return Err(FenError::TooShort {
162                parts: parts.len(),
163            });
164        }
165
166        let mut squares = [NO_PIECE; 64];
167        let mut by_color = [Bitboard::EMPTY; 2];
168        let mut by_type = [Bitboard::EMPTY; 6];
169
170        // FEN piece placement: rank 8 to rank 1, left to right
171        // Our board indices: rank 1 = 0-7, rank 2 = 8-15, ..., rank 8 = 56-63
172        let rows: Vec<&str> = parts[0].split('/').collect();
173        if rows.len() != 8 {
174            return Err(FenError::WrongRankCount {
175                expected: 8,
176                got: rows.len(),
177            });
178        }
179        for (ri, row) in rows.iter().enumerate() {
180            let rank_idx = 7 - ri; // rank 0 (index 7) = rank 8 in FEN
181            let mut col = 0usize;
182            for c in row.chars() {
183                if c.is_ascii_digit() {
184                    col += c.to_digit(10).unwrap() as usize;
185                } else if let Some(piece) = char_to_piece(c) {
186                    let sq_idx = rank_idx * 8 + col;
187                    if sq_idx < 64 {
188                        squares[sq_idx] = piece;
189                        let sq = Square::from_u8(sq_idx as u8);
190                        let bb = Bitboard::square_bb(sq);
191                        by_color[piece.color() as usize] = by_color[piece.color() as usize] | bb;
192                        by_type[piece.type_of() as usize] = by_type[piece.type_of() as usize] | bb;
193                    }
194                    col += 1;
195                }
196            }
197        }
198
199        let side_to_move = match parts[1] {
200            "w" => Color::White,
201            "b" => Color::Black,
202            _ => {
203                return Err(FenError::InvalidSideToMove(parts[1].to_string()));
204            }
205        };
206
207        let mut castling_rights = 0u8;
208        for c in parts[2].chars() {
209            match c {
210                'K' => castling_rights |= WK_CASTLE,
211                'Q' => castling_rights |= WQ_CASTLE,
212                'k' => castling_rights |= BK_CASTLE,
213                'q' => castling_rights |= BQ_CASTLE,
214                '-' => break,
215                _ => {
216                    return Err(FenError::InvalidCastling(c.to_string()));
217                }
218            }
219        }
220
221        let ep_square = if parts[3] == "-" {
222            None
223        } else {
224            let sq = crate::types::parse_sq(parts[3]);
225            if sq == Square::A1 && parts[3] != "a1" {
226                return Err(FenError::InvalidEpSquare(parts[3].to_string()));
227            }
228            Some(sq)
229        };
230
231        let rule50 = if parts.len() > 4 {
232            parts[4].parse::<u8>().map_err(|e| FenError::ParseInt(e.to_string()))?
233        } else {
234            0
235        };
236
237        let game_ply = if parts.len() > 5 {
238            parts[5].parse::<u16>().map_err(|e| FenError::ParseInt(e.to_string()))?
239        } else {
240            1
241        };
242
243        Ok(Board {
244            squares,
245            by_color,
246            by_type,
247            side_to_move,
248            castling_rights,
249            ep_square,
250            rule50,
251            game_ply,
252        })
253    }
254
255    /// Serialize the board to a FEN string.
256    #[must_use]
257    pub fn fen(&self) -> String {
258        let mut fen = String::new();
259        for rank in (0..8).rev() {
260            let mut empty = 0;
261            for file in 0..8 {
262                let idx = rank * 8 + file;
263                if self.squares[idx] == NO_PIECE {
264                    empty += 1;
265                } else {
266                    if empty > 0 {
267                        fen.push_str(&empty.to_string());
268                        empty = 0;
269                    }
270                    fen.push(self.squares[idx].ascii_char());
271                }
272            }
273            if empty > 0 {
274                fen.push_str(&empty.to_string());
275            }
276            if rank > 0 {
277                fen.push('/');
278            }
279        }
280
281        fen.push(' ');
282        fen.push(match self.side_to_move {
283            Color::White => 'w',
284            Color::Black => 'b',
285        });
286
287        fen.push(' ');
288        let mut has_castling = false;
289        if self.castling_rights & WK_CASTLE != 0 {
290            fen.push('K');
291            has_castling = true;
292        }
293        if self.castling_rights & WQ_CASTLE != 0 {
294            fen.push('Q');
295            has_castling = true;
296        }
297        if self.castling_rights & BK_CASTLE != 0 {
298            fen.push('k');
299            has_castling = true;
300        }
301        if self.castling_rights & BQ_CASTLE != 0 {
302            fen.push('q');
303            has_castling = true;
304        }
305        if !has_castling {
306            fen.push('-');
307        }
308
309        fen.push(' ');
310        match self.ep_square {
311            Some(sq) => fen.push_str(&crate::types::sq_str(sq)),
312            None => fen.push('-'),
313        }
314
315        fen.push(' ');
316        fen.push_str(&self.rule50.to_string());
317        fen.push(' ');
318        fen.push_str(&self.game_ply.to_string());
319
320        fen
321    }
322
323    /// Return the piece on a square (or [`NO_PIECE`] if empty).
324    #[inline(always)]
325    #[must_use]
326    pub fn piece_on(&self, sq: Square) -> Piece {
327        self.squares[sq as usize]
328    }
329
330    /// Return `true` if the given square is empty.
331    #[must_use]
332    pub fn empty(&self, sq: Square) -> bool {
333        self.squares[sq as usize] == NO_PIECE
334    }
335
336    /// Return the bitboard of all occupied squares.
337    #[inline(always)]
338    #[must_use]
339    pub fn occupied(&self) -> Bitboard {
340        self.by_color[0] | self.by_color[1]
341    }
342
343    /// Return all pieces of a given color.
344    #[inline(always)]
345    pub fn pieces_color(&self, c: Color) -> Bitboard {
346        self.by_color[c as usize]
347    }
348
349    /// Return all pieces of a given type.
350    #[inline(always)]
351    pub fn pieces_pt(&self, pt: PieceType) -> Bitboard {
352        self.by_type[pt as usize]
353    }
354
355    /// Return all pieces of a given color and type.
356    #[inline(always)]
357    pub fn pieces_color_pt(&self, c: Color, pt: PieceType) -> Bitboard {
358        self.by_color[c as usize] & self.by_type[pt as usize]
359    }
360
361    /// Return the side to move.
362    #[must_use]
363    pub fn side_to_move(&self) -> Color {
364        self.side_to_move
365    }
366
367    /// Return the castling rights bitmask.
368    #[must_use]
369    pub fn castling_rights(&self) -> u8 {
370        self.castling_rights
371    }
372
373    /// Return the en-passant target square, if any.
374    #[must_use]
375    pub fn ep_square(&self) -> Option<Square> {
376        self.ep_square
377    }
378
379    /// Return the bitboard of commoners (king-like pseudo-royal pieces) for a color.
380    #[inline(always)]
381    #[must_use]
382    pub fn commoners(&self, c: Color) -> Bitboard {
383        self.pieces_color_pt(c, PieceType::Commoner)
384    }
385
386    /// Return all pieces that attack `sq` on the given occupancy.
387    ///
388    /// Includes pawns, knights, bishops, rooks, queens, and commoners of either
389    /// color that attack `sq`.
390    #[must_use]
391    pub fn attackers_to(&self, sq: Square, occupied: Bitboard) -> Bitboard {
392        let mut attackers = Bitboard::EMPTY;
393
394        // Pawn attacks: the attackers come from the opposite direction
395        let white_pawn_attacks = attacks::pawn_attacks(Color::White, sq);
396        attackers =
397            attackers | (white_pawn_attacks & self.pieces_color_pt(Color::Black, PieceType::Pawn));
398        let black_pawn_attacks = attacks::pawn_attacks(Color::Black, sq);
399        attackers =
400            attackers | (black_pawn_attacks & self.pieces_color_pt(Color::White, PieceType::Pawn));
401
402        // Knight attacks
403        let knight_atk = attacks::knight_attacks(sq);
404        attackers = attackers | (knight_atk & (self.by_type[PieceType::Knight as usize]));
405
406        // Bishop/Queen attacks
407        let bishop_atk = attacks::bishop_attacks(sq, occupied);
408        attackers = attackers
409            | (bishop_atk
410                & (self.by_type[PieceType::Bishop as usize]
411                    | self.by_type[PieceType::Queen as usize]));
412
413        // Rook/Queen attacks
414        let rook_atk = attacks::rook_attacks(sq, occupied);
415        attackers = attackers
416            | (rook_atk
417                & (self.by_type[PieceType::Rook as usize]
418                    | self.by_type[PieceType::Queen as usize]));
419
420        // Commoner (king) attacks
421        let king_atk = attacks::king_attacks(sq);
422        attackers = attackers | (king_atk & self.by_type[PieceType::Commoner as usize]);
423
424        attackers
425    }
426
427    pub(crate) fn compute_checkers(&self, us: Color) -> Bitboard {
428        let them = us.flip();
429        let commoners = self.commoners(us);
430        if commoners.is_empty() {
431            return Bitboard::EMPTY;
432        }
433
434        let mut checkers = Bitboard::EMPTY;
435        let occupied = self.occupied();
436
437        let them_bb = self.pieces_color(them);
438        let mut c = commoners;
439        while !c.is_empty() {
440            let ksq = c.pop_lsb();
441            let rook_atk = attacks::rook_attacks(ksq, occupied);
442            let bishop_atk = attacks::bishop_attacks(ksq, occupied);
443            let queen_atk = rook_atk | bishop_atk;
444            checkers = checkers
445                | (rook_atk & self.by_type[PieceType::Rook as usize] & them_bb)
446                | (bishop_atk & self.by_type[PieceType::Bishop as usize] & them_bb)
447                | (queen_atk & self.by_type[PieceType::Queen as usize] & them_bb)
448                | (attacks::knight_attacks(ksq)
449                    & self.by_type[PieceType::Knight as usize]
450                    & them_bb)
451                | (attacks::pawn_attacks(us, ksq)
452                    & self.by_type[PieceType::Pawn as usize]
453                    & them_bb);
454        }
455
456        // Adjacent commoner check (extinction pseudo-royal)
457        let them_commoners = self.commoners(them);
458        if !them_commoners.is_empty() && !commoners.is_empty() {
459            let mut tc = them_commoners;
460            while !tc.is_empty() {
461                let tksq = tc.pop_lsb();
462                if attacks::king_attacks(tksq) & commoners != Bitboard::EMPTY {
463                    checkers = checkers | Bitboard::square_bb(tksq);
464                }
465            }
466        }
467
468        checkers
469    }
470
471    /// Return the bitboard of enemy pieces checking the side to move.
472    #[must_use]
473    pub fn checkers(&self) -> Bitboard {
474        self.compute_checkers(self.side_to_move)
475    }
476
477    pub(crate) fn compute_pinned(&self, us: Color) -> Bitboard {
478        let mut pinned = Bitboard::EMPTY;
479        let commoners = self.commoners(us);
480        let them = us.flip();
481        let occupied = self.occupied();
482
483        let mut c_iter = commoners;
484        while !c_iter.is_empty() {
485            let ksq = c_iter.pop_lsb();
486
487            let mut snipers = ((self.by_type[PieceType::Rook as usize]
488                | self.by_type[PieceType::Queen as usize])
489                & attacks::rook_attacks(ksq, Bitboard::EMPTY))
490                | ((self.by_type[PieceType::Bishop as usize]
491                    | self.by_type[PieceType::Queen as usize])
492                    & attacks::bishop_attacks(ksq, Bitboard::EMPTY));
493            snipers = snipers & self.pieces_color(them);
494
495            let mut s = snipers;
496            while !s.is_empty() {
497                let sniper_sq = s.pop_lsb();
498                let between = between_bb(ksq, sniper_sq) & occupied;
499                if between.count() == 1 {
500                    pinned = pinned | between;
501                }
502            }
503        }
504
505        pinned
506    }
507
508    /// Return the bitboard of pieces of the given color that are pinned
509    /// (cannot move without exposing a commoner to capture).
510    #[must_use]
511    pub fn pinned(&self, c: Color) -> Bitboard {
512        self.compute_pinned(c)
513    }
514
515    /// Fill cached state fields (checkers, pinned, commoner counts) for the
516    /// current position so that `legal()` can read them instead of recomputing.
517    pub fn populate_state(&self, state: &mut StateInfo) {
518        state.checkers = self.compute_checkers(self.side_to_move);
519        state.pinned = self.compute_pinned(self.side_to_move);
520        state.commoners_count = self.commoners(self.side_to_move).count();
521        state.them_commoners_count = self.commoners(self.side_to_move.flip()).count();
522    }
523
524    /// Make `m` on the board, storing undo information in `state`.
525    ///
526    /// Handles all move types (normal, promotion, en-passant, castling) as well
527    /// as atomic-blast removal on captures.
528    pub fn do_move(&mut self, m: Move, state: &mut StateInfo) {
529        state.castling_rights = self.castling_rights;
530        state.ep_square = self.ep_square;
531        state.rule50 = self.rule50;
532        state.captured_count = 0;
533        state.cap_sq = None;
534        state.cap_piece = NO_PIECE;
535
536        let us = self.side_to_move;
537        let them = us.flip();
538        let from = m.from_sq();
539        let to = m.to_sq();
540        let piece = self.squares[from as usize];
541        let pt = piece.type_of();
542        let is_capture = !self.empty(to);
543
544        if m.move_type() == MoveType::Castling {
545            let (kfrom, kto, rfrom, rto) = castling_squares(us, to > from);
546            self.move_piece(kfrom, kto);
547            self.move_piece(rfrom, rto);
548            self.castling_rights &= match us {
549                Color::White => !(WK_CASTLE | WQ_CASTLE),
550                Color::Black => !(BK_CASTLE | BQ_CASTLE),
551            };
552            self.ep_square = None;
553            self.rule50 += 1;
554            self.side_to_move = them;
555            self.game_ply += 1;
556            return;
557        }
558
559        if m.move_type() == MoveType::EnPassant {
560            let ep_cap = match us {
561                Color::White => Square::from_index(to as i8 - 8),
562                Color::Black => Square::from_index(to as i8 + 8),
563            };
564            let cap_piece = self.squares[ep_cap as usize];
565            state.captured[state.captured_count as usize] = (ep_cap, cap_piece);
566            state.captured_count += 1;
567            self.remove_piece(ep_cap);
568        } else if is_capture {
569            let cap_piece = self.squares[to as usize];
570            state.cap_sq = Some(to);
571            state.cap_piece = cap_piece;
572            self.remove_piece(to);
573        }
574
575        if m.move_type() == MoveType::Promotion {
576            let prom_pt = m.promotion_type();
577            let prom_piece = make_piece(us, prom_pt);
578            self.remove_piece(from);
579            self.squares[to as usize] = prom_piece;
580            self.by_color[us as usize] = self.by_color[us as usize] | Bitboard::square_bb(to);
581            self.by_type[prom_pt as usize] =
582                self.by_type[prom_pt as usize] | Bitboard::square_bb(to);
583        } else {
584            self.move_piece(from, to);
585        }
586
587        // Blast on capture
588        if is_capture || m.move_type() == MoveType::EnPassant {
589            // Blast zone = king attacks from `to`, minus pawn squares
590            let blast_zone = attacks::king_attacks(to) & !self.by_type[PieceType::Pawn as usize];
591            let mut to_blast = blast_zone & self.occupied();
592
593            // Always blast the capturer at ground zero (pawns are NOT immune at `to`).
594            to_blast = to_blast | Bitboard::square_bb(to);
595
596            let mut b = to_blast;
597            while !b.is_empty() {
598                let bsq = b.pop_lsb();
599                let bpiece = self.squares[bsq as usize];
600                if bpiece != NO_PIECE {
601                    state.captured[state.captured_count as usize] = (bsq, bpiece);
602                    state.captured_count += 1;
603                    self.remove_piece(bsq);
604                }
605            }
606        }
607
608        self.update_castling_rights(from, to, us);
609
610        if is_capture || m.move_type() == MoveType::EnPassant {
611            // White king-side
612            if self.castling_rights & WK_CASTLE != 0
613                && self.squares[Square::H1 as usize] != make_piece(Color::White, PieceType::Rook)
614            {
615                self.castling_rights &= !WK_CASTLE;
616            }
617            // White queen-side
618            if self.castling_rights & WQ_CASTLE != 0
619                && self.squares[Square::A1 as usize] != make_piece(Color::White, PieceType::Rook)
620            {
621                self.castling_rights &= !WQ_CASTLE;
622            }
623            // Black king-side
624            if self.castling_rights & BK_CASTLE != 0
625                && self.squares[Square::H8 as usize] != make_piece(Color::Black, PieceType::Rook)
626            {
627                self.castling_rights &= !BK_CASTLE;
628            }
629            // Black queen-side
630            if self.castling_rights & BQ_CASTLE != 0
631                && self.squares[Square::A8 as usize] != make_piece(Color::Black, PieceType::Rook)
632            {
633                self.castling_rights &= !BQ_CASTLE;
634            }
635        }
636
637        if pt == PieceType::Pawn && (to as i8 - from as i8).abs() == 16 {
638            self.ep_square = Some(match us {
639                Color::White => Square::from_index(from as i8 + 8),
640                Color::Black => Square::from_index(from as i8 - 8),
641            });
642        } else {
643            self.ep_square = None;
644        }
645
646        if pt == PieceType::Pawn || is_capture {
647            self.rule50 = 0;
648        } else {
649            self.rule50 += 1;
650        }
651
652        self.side_to_move = them;
653        self.game_ply += 1;
654
655        self.populate_state(state);
656    }
657
658    /// Unmake `m`, restoring the board to its state before [`do_move`](Self::do_move).
659    ///
660    /// `state` must be the same [`StateInfo`] that was passed to `do_move`.
661    pub fn undo_move(&mut self, m: Move, state: &StateInfo) {
662        self.castling_rights = state.castling_rights;
663        self.ep_square = state.ep_square;
664        self.rule50 = state.rule50;
665
666        self.side_to_move = self.side_to_move.flip();
667        let us = self.side_to_move;
668        let from = m.from_sq();
669        let to = m.to_sq();
670
671        if m.move_type() == MoveType::Castling {
672            let (kfrom, kto, rfrom, rto) = castling_squares(us, to > from);
673            self.move_piece(kto, kfrom);
674            self.move_piece(rto, rfrom);
675            self.game_ply -= 1;
676            return;
677        }
678
679        let mut i = state.captured_count;
680        while i > 0 {
681            i -= 1;
682            let (sq, piece) = state.captured[i as usize];
683            self.place_piece(piece, sq);
684        }
685
686        if m.move_type() == MoveType::Promotion {
687            let pawn = make_piece(us, PieceType::Pawn);
688            self.remove_piece(to);
689            self.place_piece(pawn, from);
690        } else {
691            self.move_piece(to, from);
692        }
693
694        if let Some(sq) = state.cap_sq {
695            self.place_piece(state.cap_piece, sq);
696        }
697
698        self.game_ply -= 1;
699    }
700
701    fn move_piece(&mut self, from: Square, to: Square) {
702        let piece = self.squares[from as usize];
703        debug_assert!(piece != NO_PIECE);
704        self.squares[to as usize] = piece;
705        self.squares[from as usize] = NO_PIECE;
706
707        let from_bb = Bitboard::square_bb(from);
708        let to_bb = Bitboard::square_bb(to);
709
710        let c = piece.color();
711        let pt = piece.type_of();
712        self.by_color[c as usize] = (self.by_color[c as usize] ^ from_bb) | to_bb;
713        self.by_type[pt as usize] = (self.by_type[pt as usize] ^ from_bb) | to_bb;
714    }
715
716    fn remove_piece(&mut self, sq: Square) {
717        let piece = self.squares[sq as usize];
718        if piece == NO_PIECE {
719            return;
720        }
721        self.squares[sq as usize] = NO_PIECE;
722        let sq_bb = Bitboard::square_bb(sq);
723        self.by_color[piece.color() as usize] = self.by_color[piece.color() as usize] ^ sq_bb;
724        self.by_type[piece.type_of() as usize] = self.by_type[piece.type_of() as usize] ^ sq_bb;
725    }
726
727    fn place_piece(&mut self, piece: Piece, sq: Square) {
728        debug_assert!(self.squares[sq as usize] == NO_PIECE);
729        self.squares[sq as usize] = piece;
730        let sq_bb = Bitboard::square_bb(sq);
731        self.by_color[piece.color() as usize] = self.by_color[piece.color() as usize] | sq_bb;
732        self.by_type[piece.type_of() as usize] = self.by_type[piece.type_of() as usize] | sq_bb;
733    }
734
735    fn update_castling_rights(&mut self, from: Square, to: Square, _us: Color) {
736        // King side
737        if from == Square::E1 || from == Square::H1 || to == Square::H1 {
738            self.castling_rights &= !WK_CASTLE;
739        }
740        if from == Square::E1 || from == Square::A1 || to == Square::A1 {
741            self.castling_rights &= !WQ_CASTLE;
742        }
743        if from == Square::E8 || from == Square::H8 || to == Square::H8 {
744            self.castling_rights &= !BK_CASTLE;
745        }
746        if from == Square::E8 || from == Square::A8 || to == Square::A8 {
747            self.castling_rights &= !BQ_CASTLE;
748        }
749    }
750
751    /// Check whether `m` is legal under atomic chess rules.
752    ///
753    /// Considers blast-zone effects (self-explosion), castling pass-through
754    /// safety, pseudo-royal adjacency, and commoner extinction.
755    ///
756    /// `state` must contain up-to-date cached fields for the current position.
757    #[must_use]
758    pub fn legal(&self, m: Move, state: &StateInfo) -> bool {
759        let from = m.from_sq();
760        let to = m.to_sq();
761        let us = self.side_to_move;
762        let them = us.flip();
763
764        let piece = self.piece_on(from);
765        if piece == NO_PIECE {
766            return false;
767        }
768
769        let is_capture = m.move_type() != MoveType::Castling
770            && (m.move_type() == MoveType::EnPassant || self.piece_on(to) != NO_PIECE);
771
772        if m.move_type() == MoveType::Castling {
773            let ksq = from;
774            let occupied = self.occupied();
775            let pass_through = if to > ksq {
776                [ksq, Square::from_index(ksq as i8 + 1)]
777            } else {
778                [Square::from_index(ksq as i8 - 1), ksq]
779            };
780            for &sq in &pass_through {
781                let adjacent_enemy_commoners = self.commoners(them) & attacks::king_attacks(sq);
782                if adjacent_enemy_commoners.is_empty() {
783                    let atk = attackers_to(self, sq, occupied, self.by_color[them as usize])
784                        | (attacks::king_attacks(sq)
785                            & self.by_type[PieceType::Commoner as usize]
786                            & self.by_color[them as usize]);
787                    if atk != Bitboard::EMPTY {
788                        return false;
789                    }
790                }
791            }
792        }
793
794        let mut occupied = self.occupied() ^ from;
795        let mut kto = to;
796
797        if m.move_type() == MoveType::Castling {
798            let (_, kto_actual, rfrom, rto) = castling_squares(us, to > from);
799            kto = kto_actual;
800            occupied = occupied ^ Bitboard::square_bb(rfrom);
801            occupied = occupied | Bitboard::square_bb(rto);
802        } else if m.move_type() == MoveType::EnPassant {
803            let capsq = match us {
804                Color::White => Square::from_index(to as i8 - 8),
805                Color::Black => Square::from_index(to as i8 + 8),
806            };
807            occupied = occupied & !Bitboard::square_bb(capsq);
808        }
809
810        occupied = occupied | Bitboard::square_bb(kto);
811
812        if is_capture {
813            let pre_pawns = self.by_type[PieceType::Pawn as usize];
814            let pre_non_pawns = self.occupied() ^ pre_pawns;
815            let blast_adjacent = attacks::king_attacks(kto) & pre_non_pawns;
816            occupied = occupied & !(blast_adjacent | Bitboard::square_bb(kto));
817        }
818
819        let mut our_commoners = self.commoners(us) & occupied;
820
821        if m.move_type() == MoveType::Castling {
822            our_commoners = our_commoners | Bitboard::square_bb(kto);
823        } else if !is_capture {
824            let moving_piece = self.piece_on(from);
825            if moving_piece != NO_PIECE && moving_piece.type_of() == PieceType::Commoner {
826                our_commoners = our_commoners | Bitboard::square_bb(kto);
827            }
828        }
829
830        if our_commoners.is_empty() {
831            return false;
832        }
833
834        let our_pr_count = state.commoners_count as usize;
835        let them_pr_count = state.them_commoners_count as usize;
836
837        if our_pr_count <= 1 {
838            let enemy_pr_destroyed =
839                them_pr_count <= 1 && (self.commoners(them) & occupied).is_empty();
840
841            if !enemy_pr_destroyed {
842                let them_commoners = self.commoners(them);
843                let enemy_survivors = self.by_color[them as usize] & occupied;
844
845                let mut c = our_commoners;
846                while !c.is_empty() {
847                    let ksq = c.pop_lsb();
848                    let adjacent_enemy = them_commoners & attacks::king_attacks(ksq);
849                    if adjacent_enemy.is_empty()
850                        && attackers_to(self, ksq, occupied, enemy_survivors) != Bitboard::EMPTY
851                    {
852                        return false;
853                    }
854                }
855            }
856        }
857
858        true
859    }
860}
861
862/// Return the bitboard of enemy sliding/leaper pieces (rook, bishop, queen,
863/// knight, pawn) that attack `sq` on the given `occupied` board, filtered to
864/// the enemy pieces in `enemy_bb`.  Commoner (king) attacks are *not* included
865/// (they are handled separately via adjacency-immunity in the pseudo-royal
866/// rules).
867#[inline(always)]
868fn attackers_to(board: &Board, sq: Square, occupied: Bitboard, enemy_bb: Bitboard) -> Bitboard {
869    let rook_atk = attacks::rook_attacks(sq, occupied);
870    let bishop_atk = attacks::bishop_attacks(sq, occupied);
871    let queen_atk = rook_atk | bishop_atk;
872    rook_atk & board.by_type[PieceType::Rook as usize] & enemy_bb
873        | bishop_atk & board.by_type[PieceType::Bishop as usize] & enemy_bb
874        | queen_atk & board.by_type[PieceType::Queen as usize] & enemy_bb
875        | attacks::knight_attacks(sq) & board.by_type[PieceType::Knight as usize] & enemy_bb
876        | attacks::pawn_attacks(board.side_to_move, sq)
877            & board.by_type[PieceType::Pawn as usize]
878            & enemy_bb
879}
880
881/// Return the castling square data for a given color and side.
882fn castling_squares(us: Color, kingside: bool) -> (Square, Square, Square, Square) {
883    match (us, kingside) {
884        (Color::White, true) => (Square::E1, Square::G1, Square::H1, Square::F1),
885        (Color::White, false) => (Square::E1, Square::C1, Square::A1, Square::D1),
886        (Color::Black, true) => (Square::E8, Square::G8, Square::H8, Square::F8),
887        (Color::Black, false) => (Square::E8, Square::C8, Square::A8, Square::D8),
888    }
889}
890
891/// Returns `true` when `m` is trivially legal — i.e. not a capture, not a
892/// commoner move, not en-passant, the moving piece is unpinned, there are no
893/// checkers, and at least one own commoner still exists.
894///
895/// When this returns `true` the move is guaranteed legal without needing the
896/// full `legal()` check (blast, pseudo-royal, castling pass-through).
897#[inline(always)]
898pub(crate) fn is_move_trivially_legal(board: &Board, m: Move, state: &StateInfo) -> bool {
899    if !state.checkers.is_empty() {
900        return false;
901    }
902    if state.commoners_count == 0 {
903        return false;
904    }
905
906    let from = m.from_sq();
907    let pt = board.piece_on(from).type_of();
908    if pt == PieceType::Commoner {
909        return false;
910    }
911
912    let mt = m.move_type();
913    if mt == MoveType::EnPassant {
914        return false;
915    }
916
917    // A non-Castling move that captures a piece (EnPassant already handled above).
918    let is_capture = mt != MoveType::Castling && board.piece_on(m.to_sq()) != NO_PIECE;
919    if is_capture {
920        return false;
921    }
922
923    // Castling reaches here (not a capture, not en-passant, not a commoner move).
924    // Castling still needs the full pass-through check, so reject fast-path.
925    if mt == MoveType::Castling {
926        return false;
927    }
928
929    // Check pin: a pinned piece might expose a commoner.
930    if (state.pinned & Bitboard::square_bb(from)) != Bitboard::EMPTY {
931        return false;
932    }
933
934    true
935}
936
937impl fmt::Display for Board {
938    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
939        writeln!(f)?;
940        for rank in (0..8).rev() {
941            write!(f, "{} ", rank + 1)?;
942            for file in 0..8 {
943                let wrapped_sq = make_square(
944                    match file {
945                        0 => File::A,
946                        1 => File::B,
947                        2 => File::C,
948                        3 => File::D,
949                        4 => File::E,
950                        5 => File::F,
951                        6 => File::G,
952                        7 => File::H,
953                        _ => unreachable!(),
954                    },
955                    match rank {
956                        0 => Rank::R1,
957                        1 => Rank::R2,
958                        2 => Rank::R3,
959                        3 => Rank::R4,
960                        4 => Rank::R5,
961                        5 => Rank::R6,
962                        6 => Rank::R7,
963                        7 => Rank::R8,
964                        _ => unreachable!(),
965                    },
966                );
967                let wrapped_idx = wrapped_sq as usize;
968                if self.squares[wrapped_idx] == NO_PIECE {
969                    write!(f, " .")?;
970                } else {
971                    write!(f, " {}", self.squares[wrapped_idx].ascii_char())?;
972                }
973            }
974            writeln!(f)?;
975        }
976        writeln!(f, "   a b c d e f g h")?;
977        writeln!(f, "Side to move: {:?}", self.side_to_move)?;
978        writeln!(f, "Castling: {:b}", self.castling_rights)?;
979        writeln!(f, "EP: {:?}", self.ep_square)?;
980        writeln!(f, "FEN: {}", self.fen())?;
981        Ok(())
982    }
983}
984
985impl Square {
986    /// Construct a [`Square`] from its 0–63 index. Returns [`Square::NONE`]
987    /// for out-of-range values.
988    pub fn from_index(idx: i8) -> Square {
989        if (0..64).contains(&idx) {
990            crate::types::SQUARES[idx as usize]
991        } else {
992            Square::NONE
993        }
994    }
995
996    /// Construct a [`Square`] from its 0–63 index as a `u8`.
997    pub fn from_u8(idx: u8) -> Square {
998        Square::from_index(idx as i8)
999    }
1000}
1001
1002#[cfg(test)]
1003mod tests {
1004    use super::*;
1005
1006    #[test]
1007    fn test_starting_position() {
1008        let board = Board::new();
1009        assert_eq!(board.piece_on(Square::E1), W_COMMONER);
1010        assert_eq!(board.piece_on(Square::D1), W_QUEEN);
1011        assert_eq!(board.piece_on(Square::E8), B_COMMONER);
1012        assert_eq!(board.side_to_move(), Color::White);
1013        assert_eq!(
1014            board.castling_rights,
1015            WK_CASTLE | WQ_CASTLE | BK_CASTLE | BQ_CASTLE
1016        );
1017    }
1018
1019    #[test]
1020    fn test_fen_roundtrip() {
1021        let board = Board::new();
1022        let fen = board.fen();
1023        let board2 = Board::from_fen(&fen).unwrap();
1024        assert_eq!(board.fen(), board2.fen());
1025    }
1026
1027    #[test]
1028    fn test_custom_fen() {
1029        let fen = "8/8/8/8/8/8/8/4K3 w - - 0 1";
1030        let board = Board::from_fen(fen).unwrap();
1031        assert_eq!(board.piece_on(Square::E1), W_COMMONER);
1032    }
1033
1034    #[test]
1035    fn test_checkers() {
1036        // Position where white queen gives check to black commoner
1037        let fen = "4k3/8/8/8/8/8/8/4Q2K b - - 0 1";
1038        let board = Board::from_fen(fen).unwrap();
1039        let checkers = board.checkers();
1040        assert!(!checkers.is_empty(), "Expected checkers, got empty");
1041    }
1042
1043    #[test]
1044    fn test_pinned() {
1045        // Black rook on e4, white pawn on e3, white commoner on e2 - pawn is pinned
1046        let fen = "4k3/8/8/8/4r3/4P3/4K3/8 w - - 0 1";
1047        let board = Board::from_fen(fen).unwrap();
1048        let pinned = board.pinned(Color::White);
1049        assert!(!pinned.is_empty(), "Expected pinned pieces, got empty");
1050    }
1051
1052    #[test]
1053    fn test_do_undo_restores_state() {
1054        let fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
1055        let mut board = Board::from_fen(fen).unwrap();
1056        let orig_fen = board.fen();
1057        let mut state = StateInfo::new();
1058
1059        // e4
1060        let m = Move::make_move(Square::E2, Square::E4);
1061        board.do_move(m, &mut state);
1062        board.undo_move(m, &state);
1063
1064        assert_eq!(board.fen(), orig_fen);
1065    }
1066
1067    #[test]
1068    fn test_do_undo_capture_restores() {
1069        let fen2 = "rnbqkbnr/ppp1pppp/8/3p4/4P3/8/PPPP1PPP/RNBQKBNR w KQkq d6 0 2";
1070        let mut board2 = Board::from_fen(fen2).unwrap();
1071        let orig_fen = board2.fen();
1072        let mut state2 = StateInfo::new();
1073
1074        let m = Move::make_move(Square::E4, Square::D5);
1075        board2.do_move(m, &mut state2);
1076        board2.undo_move(m, &state2);
1077
1078        assert_eq!(board2.fen(), orig_fen);
1079    }
1080
1081    #[test]
1082    fn test_self_explosion_legal_with_surviving_commoner() {
1083        // White commoners on d3 AND e1; white rook on d5, black pawn on d4.
1084        // Rook takes pawn on d4 — blast zone (c3-e5) includes d3, destroying
1085        // the commoner on d3, but the commoner on e1 survives.
1086        // This is NOT self-explosion — only the LAST commoner being destroyed
1087        // makes a move illegal.
1088        let fen = "4k3/8/8/3R4/3p4/3C4/8/4K3 w - - 0 1";
1089        let board = Board::from_fen(fen).unwrap();
1090        let mut moves = MoveList::new();
1091        crate::movegen::generate_legal(&board, &mut moves);
1092        // The capture should be LEGAL because e1 survives and is not under attack.
1093        let has_rook_d4 = moves
1094            .as_slice()
1095            .iter()
1096            .any(|&m| m.from_sq() == Square::D5 && m.to_sq() == Square::D4);
1097        assert!(
1098            has_rook_d4,
1099            "rook capture on d4 should be legal (e1 commoner survives)"
1100        );
1101    }
1102
1103    #[test]
1104    fn test_self_explosion_illegal_last_commoner() {
1105        // White commoner ONLY on d3; white rook on d5, black pawn on d4.
1106        // Rook takes pawn on d4 — blast zone includes d3, destroying the
1107        // LAST (only) commoner → self-explosion, illegal.
1108        let fen = "4k3/8/8/3R4/3p4/3C4/8/8 w - - 0 1";
1109        let board = Board::from_fen(fen).unwrap();
1110        let mut moves = MoveList::new();
1111        crate::movegen::generate_legal(&board, &mut moves);
1112        for &m in moves.as_slice() {
1113            assert!(
1114                m.from_sq() != Square::D5 || m.to_sq() != Square::D4,
1115                "rook capture on d4 should be illegal (last commoner destroyed)"
1116            );
1117        }
1118    }
1119
1120    #[test]
1121    fn test_blast_zone_removes_pieces() {
1122        // White rook on e4, black knight on e5, black pawn on f5
1123        // Rook captures knight — blast zone around e5 (d4-f4, d5-f5, d6-f6)
1124        // removes: rook (non-pawn capturer), knight, but NOT the pawn on f5
1125        let fen = "4k3/8/8/4np2/4R3/8/8/4K3 w - - 0 1";
1126        let mut board = Board::from_fen(fen).unwrap();
1127        let mut state = StateInfo::new();
1128        let m = Move::make_move(Square::E4, Square::E5);
1129        board.do_move(m, &mut state);
1130        // The rook and knight should be gone; the black pawn on f5 should remain
1131        assert!(
1132            board.piece_on(Square::E4) == NO_PIECE,
1133            "rook at e4 should be gone"
1134        );
1135        assert!(
1136            board.piece_on(Square::E5) == NO_PIECE,
1137            "knight at e5 should be gone"
1138        );
1139        assert!(
1140            board.piece_on(Square::F5) == B_PAWN,
1141            "pawn at f5 should survive"
1142        );
1143    }
1144
1145    #[test]
1146    fn test_pinned_piece_capture_explodes_pinner() {
1147        // Black rook on e5 (pinning), white rook on e3, black pawn on e4,
1148        // white commoner on e1. The rook on e3 is pinned by the rook on e5
1149        // (both on e-file, commoner on e1 behind).
1150        // But rook captures pawn on e4 — blast zone (d3-f5) destroys the
1151        // rook on e5, so the pin is removed and the move is legal.
1152        let fen = "4k3/8/8/4r3/4p3/4R3/8/4K3 w - - 0 1";
1153        let board = Board::from_fen(fen).unwrap();
1154        let mut moves = MoveList::new();
1155        crate::movegen::generate_legal(&board, &mut moves);
1156        let has_rook_e4 = moves
1157            .as_slice()
1158            .iter()
1159            .any(|&m| m.from_sq() == Square::E3 && m.to_sq() == Square::E4);
1160        assert!(
1161            has_rook_e4,
1162            "rook capture on e4 should be legal (blast removes pinning rook)"
1163        );
1164    }
1165
1166    #[test]
1167    fn test_en_passant_blast() {
1168        // White pawn on d5, black pawn on c5 (just double-pushed), black knight on d4
1169        // White plays dxc6 en passant — blast at c6
1170        let fen2 = "4k3/8/8/2Pp4/8/8/8/4K3 w KQkq d6 0 2";
1171        let mut board2 = Board::from_fen(fen2).unwrap();
1172        let mut state2 = StateInfo::new();
1173        let m = Move::make_enpassant(Square::C5, Square::D6);
1174        board2.do_move(m, &mut state2);
1175        // After EP capture + blast: pawns on c5 and d5 are gone,
1176        // commoners should remain (out of blast zone)
1177        assert!(board2.piece_on(Square::C5) == NO_PIECE);
1178        assert!(board2.piece_on(Square::D5) == NO_PIECE);
1179    }
1180}