Skip to main content

atomic_movegen/
movegen.rs

1use crate::attacks;
2use crate::board::{
3    BK_CASTLE, BQ_CASTLE, Board, StateInfo, WK_CASTLE, WQ_CASTLE, is_move_trivially_legal,
4};
5use crate::types::*;
6
7const PROMOTION_PIECES: [PieceType; 4] = [
8    PieceType::Queen,
9    PieceType::Rook,
10    PieceType::Bishop,
11    PieceType::Knight,
12];
13
14/// Generate all pseudo-legal moves for the side to move.
15///
16/// Pseudo-legal means every move that is legal *except* moves that would
17/// result in self-explosion (losing the last commoner), castling through
18/// check, or leaving a commoner under attack. Use [`generate_legal`] for
19/// fully legal moves.
20pub fn generate_pseudo_legal(board: &Board, moves: &mut MoveList) {
21    let us = board.side_to_move();
22    let them = us.flip();
23    let occupied = board.occupied();
24    let target = !occupied | board.pieces_color(them);
25
26    let mut p = board.pieces_color_pt(us, PieceType::Pawn);
27    while !p.is_empty() {
28        let from = p.pop_lsb();
29        generate_pawn_moves_for(board, us, them, from, moves);
30    }
31
32    let mut knights = board.pieces_color_pt(us, PieceType::Knight);
33    while !knights.is_empty() {
34        let from = knights.pop_lsb();
35        let attacks = attacks::knight_attacks(from) & target;
36        let mut a = attacks;
37        while !a.is_empty() {
38            let to = a.pop_lsb();
39            moves.push(Move::make_move(from, to));
40        }
41    }
42
43    let mut bishops = board.pieces_color_pt(us, PieceType::Bishop);
44    while !bishops.is_empty() {
45        let from = bishops.pop_lsb();
46        let attacks = attacks::bishop_attacks(from, occupied) & target;
47        let mut a = attacks;
48        while !a.is_empty() {
49            let to = a.pop_lsb();
50            moves.push(Move::make_move(from, to));
51        }
52    }
53
54    let mut rooks = board.pieces_color_pt(us, PieceType::Rook);
55    while !rooks.is_empty() {
56        let from = rooks.pop_lsb();
57        let attacks = attacks::rook_attacks(from, occupied) & target;
58        let mut a = attacks;
59        while !a.is_empty() {
60            let to = a.pop_lsb();
61            moves.push(Move::make_move(from, to));
62        }
63    }
64
65    let mut queens = board.pieces_color_pt(us, PieceType::Queen);
66    while !queens.is_empty() {
67        let from = queens.pop_lsb();
68        let attacks = attacks::queen_attacks(from, occupied) & target;
69        let mut a = attacks;
70        while !a.is_empty() {
71            let to = a.pop_lsb();
72            moves.push(Move::make_move(from, to));
73        }
74    }
75
76    let mut commoners = board.pieces_color_pt(us, PieceType::Commoner);
77    while !commoners.is_empty() {
78        let from = commoners.pop_lsb();
79        let attacks = attacks::king_attacks(from) & target;
80        let mut a = attacks;
81        while !a.is_empty() {
82            let to = a.pop_lsb();
83            moves.push(Move::make_move(from, to));
84        }
85    }
86
87    // Castling moves
88    generate_castling(board, us, moves);
89}
90
91fn generate_pawn_moves_for(
92    board: &Board,
93    us: Color,
94    them: Color,
95    from: Square,
96    moves: &mut MoveList,
97) {
98    let from_rank = rank_of(from);
99    let from_f = file_of(from) as i8;
100
101    let (push_dir, push_double, start_rank, promo_rank) = match us {
102        Color::White => (8i8, 16i8, Rank::R2, Rank::R8),
103        Color::Black => (-8i8, -16i8, Rank::R7, Rank::R1),
104    };
105
106    let from_idx = from as i8;
107
108    // Single push
109    let to_idx = from_idx + push_dir;
110    let to_sq = Square::from_index(to_idx);
111    if to_sq != Square::NONE && board.empty(to_sq) {
112        if rank_of(to_sq) == promo_rank {
113            for &pt in &PROMOTION_PIECES {
114                moves.push(Move::make_promotion(from, to_sq, pt));
115            }
116        } else {
117            moves.push(Move::make_move(from, to_sq));
118        }
119
120        // Double push (only from starting rank)
121        if from_rank == start_rank {
122            let to_idx2 = from_idx + push_double;
123            let to_sq2 = Square::from_index(to_idx2);
124            if to_sq2 != Square::NONE && board.empty(to_sq2) {
125                moves.push(Move::make_move(from, to_sq2));
126            }
127        }
128    }
129
130    // Captures - adjacent files only
131    for df in &[-1i8, 1i8] {
132        let target_f = from_f + df;
133        if !(0..=7).contains(&target_f) {
134            continue;
135        }
136        let to_idx = from_idx + push_dir + df;
137        let to_sq = Square::from_index(to_idx);
138        if to_sq == Square::NONE {
139            continue;
140        }
141        // Verify correct file (guard against wrapping)
142        if file_of(to_sq) as i8 != target_f {
143            continue;
144        }
145        if board.pieces_color(them) & Bitboard::square_bb(to_sq) != Bitboard::EMPTY {
146            if rank_of(to_sq) == promo_rank {
147                for &pt in &PROMOTION_PIECES {
148                    moves.push(Move::make_promotion(from, to_sq, pt));
149                }
150            } else {
151                moves.push(Move::make_move(from, to_sq));
152            }
153        }
154    }
155
156    // En passant
157    if let Some(ep_sq) = board.ep_square() {
158        let ep_f = file_of(ep_sq) as i8;
159        if ep_f == from_f - 1 || ep_f == from_f + 1 {
160            let df = ep_f - from_f;
161            let to_idx = from_idx + push_dir + df;
162            let to_sq = Square::from_index(to_idx);
163            if to_sq == ep_sq {
164                moves.push(Move::make_enpassant(from, ep_sq));
165            }
166        }
167    }
168}
169
170fn generate_castling(board: &Board, us: Color, moves: &mut MoveList) {
171    let (
172        king_side_right,
173        queen_side_right,
174        king_sq,
175        king_side_rook_sq,
176        queen_side_rook_sq,
177        king_side_squares,
178        queen_side_squares,
179    ) = match us {
180        Color::White => (
181            WK_CASTLE,
182            WQ_CASTLE,
183            Square::E1,
184            Square::H1,
185            Square::A1,
186            [Square::F1, Square::G1],
187            [Square::B1, Square::C1, Square::D1],
188        ),
189        Color::Black => (
190            BK_CASTLE,
191            BQ_CASTLE,
192            Square::E8,
193            Square::H8,
194            Square::A8,
195            [Square::F8, Square::G8],
196            [Square::B8, Square::C8, Square::D8],
197        ),
198    };
199
200    // King-side castling
201    if board.castling_rights() & king_side_right != 0 {
202        let mut clear = true;
203        for &sq in &king_side_squares {
204            if !board.empty(sq) {
205                clear = false;
206                break;
207            }
208        }
209        if clear {
210            moves.push(Move::make_castling(king_sq, king_side_rook_sq));
211        }
212    }
213
214    // Queen-side castling
215    if board.castling_rights() & queen_side_right != 0 {
216        let mut clear = true;
217        for &sq in &queen_side_squares {
218            if !board.empty(sq) {
219                clear = false;
220                break;
221            }
222        }
223        if clear {
224            moves.push(Move::make_castling(king_sq, queen_side_rook_sq));
225        }
226    }
227}
228
229/// Generate all fully legal moves for the side to move.
230///
231/// Wraps [`generate_pseudo_legal`] and filters out illegal moves using
232/// [`Board::legal`] and a fast-path trivial-legality check.
233pub fn generate_legal(board: &Board, moves: &mut MoveList) {
234    let mut state = StateInfo::new();
235    board.populate_state(&mut state);
236    generate_pseudo_legal(board, moves);
237
238    // In-place compaction: fast-path accept without legal() call.
239    let orig_len = moves.len();
240    if orig_len == 0 {
241        return;
242    }
243
244    let new_len = {
245        let ms = moves.as_mut_slice();
246        let mut write_idx = 0;
247        for read_idx in 0..orig_len {
248            let m = ms[read_idx];
249            if is_move_trivially_legal(board, m, &state) || board.legal(m, &state) {
250                ms[write_idx] = m;
251                write_idx += 1;
252            }
253        }
254        write_idx
255    };
256    moves.set_len(new_len);
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262    use crate::board::Board;
263
264    #[test]
265    fn test_starting_position_move_count() {
266        let board = Board::new();
267        let mut moves = MoveList::new();
268        generate_pseudo_legal(&board, &mut moves);
269        let mut legal_moves = MoveList::new();
270        generate_legal(&board, &mut legal_moves);
271        // Standard starting position: 20 legal moves
272        assert_eq!(legal_moves.len(), 20);
273    }
274
275    #[test]
276    fn test_knight_moves_start() {
277        let board = Board::new();
278        let mut moves = MoveList::new();
279        generate_pseudo_legal(&board, &mut moves);
280        let knight_moves: Vec<Move> = moves
281            .as_slice()
282            .iter()
283            .filter(|&&m| {
284                let from = m.from_sq();
285                from == Square::B1 || from == Square::G1
286            })
287            .copied()
288            .collect();
289        // Each knight has 2 moves from starting position
290        assert_eq!(knight_moves.len(), 4);
291    }
292}