Skip to main content

chess/position/
moves.rs

1use crate::{Role, Square, board::Bitboard};
2
3use super::{Move, Position, Side};
4
5use Role::*;
6
7type Moves = Vec<Move>;
8
9/// Position Move API.
10impl Position {
11    pub fn legal_moves(&self) -> Moves {
12        if self.is_check() {
13            return self.evasion_moves();
14        }
15
16        let shields = self.board().king_shields(self.turn());
17
18        let mut moves = self.pseudo_piece_moves();
19        moves.retain(|m| self.piece_move_is_safe(*m, shields));
20        moves.extend(self.legal_king_moves());
21        moves.extend(self.legal_en_passant_moves());
22        moves.extend(self.legal_castle_moves());
23        moves
24    }
25
26    pub fn legal_castle_moves(&self) -> Moves {
27        let mut moves = Moves::new();
28
29        for side in Side::ALL {
30            if let Some(play) = self.can_castle(side) {
31                moves.push(play);
32            }
33        }
34
35        moves
36    }
37    fn evasion_moves(&self) -> Moves {
38        let checkers = self.checkers();
39        let Some(king) = self.board().king_of(self.turn()) else {
40            return Moves::new();
41        };
42
43        let mut moves = self.legal_king_evasion_moves(king, checkers);
44        if checkers.more_than_one() {
45            return moves;
46        }
47
48        let Some(checker) = checkers.first() else {
49            return moves;
50        };
51
52        let shields = self.board().king_shields(self.turn());
53        let target = king.between(checker).with(checker);
54        let mut piece_moves = self.pseudo_piece_moves_to(target);
55        piece_moves.retain(|m| self.piece_move_is_safe(*m, shields));
56        moves.extend(piece_moves);
57        moves.extend(self.legal_en_passant_moves());
58
59        moves
60    }
61
62    pub fn pseudo_piece_moves(&self) -> Moves {
63        let target = !self.board().player(self.turn());
64        self.pseudo_piece_moves_to(target)
65    }
66
67    /// Ordinary non-king, non-en-passant, non-castle pseudo moves landing in
68    /// `target`.
69    ///
70    /// In non-check positions `target` is every square not occupied by us. In
71    /// single-check evasions it is the checker square plus blocking squares.
72    fn pseudo_piece_moves_to(&self, target: Bitboard) -> Moves {
73        let mut moves = Moves::new();
74
75        self.pseudo_pawn_moves(target, &mut moves);
76        for role in [Knight, Bishop, Rook, Queen] {
77            self.pseudo_role_moves(role, target, &mut moves);
78        }
79
80        moves
81    }
82
83    fn legal_king_evasion_moves(&self, king: Square, checkers: Bitboard) -> Moves {
84        let sliders = checkers.intersection(self.board().sliders());
85        let mut attacked = Bitboard::EMPTY;
86        let mut sliders = sliders;
87        while let Some(checker) = sliders.pop_first() {
88            // `king_move_is_safe` checks attacks to the destination, but a
89            // slider checking the king still controls the ray through the old
90            // king square after the king moves away.
91            attacked.append(checker.full_ray(king).difference(Bitboard::from_square(checker)));
92        }
93
94        let mut moves = Moves::new();
95        let target = self.board().player(self.turn()).union(attacked);
96
97        self.pseudo_role_moves(King, !target, &mut moves);
98        moves.retain(|m| self.king_move_is_safe(*m));
99
100        moves
101    }
102
103    pub fn legal_king_moves(&self) -> Moves {
104        let mut moves = Moves::new();
105        let target = !self.board().player(self.turn());
106
107        self.pseudo_role_moves(King, target, &mut moves);
108        moves.retain(|m| self.king_move_is_safe(*m));
109
110        moves
111    }
112
113    fn king_move_is_safe(&self, play: Move) -> bool {
114        let occupied = self.board().occupied().difference(Bitboard::from_square(play.from));
115        self.board().attacks_on(play.to, self.turn().other(), occupied).is_empty()
116    }
117
118    fn king_square_is_safe(&self, square: Square) -> bool {
119        self.board().attacks_on(square, self.turn().other(), self.board().occupied()).is_empty()
120    }
121
122    fn pseudo_role_moves(&self, role: Role, target: Bitboard, moves: &mut Moves) {
123        let occupied = self.board().occupied();
124        let mut pieces = self.board().role(role).intersection(self.board().player(self.turn()));
125
126        while let Some(from) = pieces.pop_first() {
127            let piece = role.of(self.turn());
128            let mut targets = from.attacks(piece, occupied).intersection(target);
129            while let Some(to) = targets.pop_first() {
130                moves.push(Move::capture(role, from, to, self.board().role_at(to)));
131            }
132        }
133    }
134
135    fn pseudo_pawn_moves(&self, target: Bitboard, moves: &mut Moves) {
136        let occupied = self.board().occupied();
137        let them = self.board().player(self.turn().other());
138        let pawns = self.board().pawns().intersection(self.board().player(self.turn()));
139        let empty = !occupied;
140        let push = self.turn().pawn_push();
141        let double_push = self.turn().pawn_double_push();
142        let [left, right] = self.turn().pawn_capture_directions();
143        let double_rank = Bitboard::from_rank(self.turn().pawn_double_push_rank());
144
145        let single = pawns.checked_shift(push).intersection(empty);
146        let double = single.intersection(double_rank).checked_shift(push).intersection(empty);
147        let captures_left = pawns.checked_shift(left).intersection(them);
148        let captures_right = pawns.checked_shift(right).intersection(them);
149
150        let mut targets = single.intersection(target);
151        while let Some(to) = targets.pop_first() {
152            let from = to.checked_add(push.reverse()).expect("valid pawn source");
153            moves.extend(Move::pawn(self.turn(), from, to, None));
154        }
155
156        let mut targets = double.intersection(target);
157        while let Some(to) = targets.pop_first() {
158            let from = to.checked_add(double_push.reverse()).expect("valid pawn source");
159            moves.push(Move::normal(Pawn, from, to));
160        }
161
162        let mut targets = captures_left.intersection(target);
163        while let Some(to) = targets.pop_first() {
164            let from = to.checked_add(left.reverse()).expect("valid pawn source");
165            moves.extend(Move::pawn(self.turn(), from, to, self.board().role_at(to)));
166        }
167
168        let mut targets = captures_right.intersection(target);
169        while let Some(to) = targets.pop_first() {
170            let from = to.checked_add(right.reverse()).expect("valid pawn source");
171            moves.extend(Move::pawn(self.turn(), from, to, self.board().role_at(to)));
172        }
173    }
174
175    fn legal_en_passant_moves(&self) -> Moves {
176        let mut moves = Moves::new();
177
178        if let Some(to) = self.en_passant() {
179            let to = to.square();
180
181            let mut pawns = self
182                .board()
183                .pawns()
184                .intersection(self.board().player(self.turn()))
185                .intersection(to.pawn_attack_moves(self.turn().other()));
186
187            while let Some(from) = pawns.pop_first() {
188                let m = Move::en_passant(from, to);
189                if self.en_passant_move_is_safe(m) {
190                    moves.push(m);
191                }
192            }
193        }
194
195        moves
196    }
197
198    fn piece_move_is_safe(&self, play: Move, shields: Bitboard) -> bool {
199        // In a legal, not-in-check position, an ordinary piece move can only
200        // expose our king by moving a shielding piece off a slider ray.
201        if !shields.contains(play.from) {
202            return true;
203        }
204
205        match self.board().king_of(self.turn()) {
206            // A shielding piece remains safe if it stays on the full ray
207            // through the king and its original square. This does not need to
208            // be only the segment between king and attacker: ordinary move
209            // generation cannot move through either piece, and capturing the
210            // attacker is safe.
211            Some(king) => king.full_ray(play.from).contains(play.to),
212            // Invalid positions without a king have no safe legal moves.
213            None => false,
214        }
215    }
216
217    fn en_passant_move_is_safe(&self, play: Move) -> bool {
218        let Some(king) = self.board().king_of(self.turn()) else {
219            return false;
220        };
221
222        let captured = Square::new(play.to.file(), play.from.rank());
223        let occupied = self
224            .board()
225            .occupied()
226            .difference(Bitboard::from_square(play.from))
227            .difference(Bitboard::from_square(captured))
228            .union(Bitboard::from_square(play.to));
229
230        self.board().attacks_on(king, self.turn().other(), occupied).is_empty()
231    }
232
233    pub fn can_castle(&self, side: Side) -> Option<Move> {
234        let rook_file = self.castles().get(self.turn(), side)?;
235        let king_from = self.board().king_of(self.turn())?;
236        let empty_path = self.turn().castle_empty_path(king_from, rook_file);
237        if !self.board().occupied().intersection(empty_path).is_empty() {
238            return None;
239        }
240
241        if self
242            .turn()
243            .castle_king_path(king_from, side)
244            .iter()
245            .any(|square| !self.king_square_is_safe(square))
246        {
247            return None;
248        }
249
250        Some(Move::castle(self.turn(), king_from, rook_file))
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use crate::{
257        Player::*,
258        position::Parts,
259        square::{File::*, Square::*},
260    };
261
262    use super::{Move, Position, Side};
263
264    fn freestyle_position() -> Position {
265        let mut parts = Parts::empty();
266        let board = &mut parts.board;
267        board.insert(C1, White.king());
268        board.insert(A1, White.rook());
269        board.insert(H1, White.rook());
270        board.insert(C8, Black.king());
271        board.insert(A8, Black.rook());
272        board.insert(H8, Black.rook());
273
274        let castles = &mut parts.castles;
275        castles.set(White, Side::Queen, A);
276        castles.set(White, Side::King, H);
277        castles.set(Black, Side::Queen, A);
278        castles.set(Black, Side::King, H);
279        parts.validate().unwrap()
280    }
281
282    #[test]
283    fn generates_freestyle_castle_moves() {
284        let moves = freestyle_position().legal_castle_moves();
285
286        assert!(moves.contains(&Move::castle(White, C1, A)));
287        assert!(moves.contains(&Move::castle(White, C1, H)));
288    }
289
290    #[test]
291    fn blocks_freestyle_castle_move() {
292        let mut parts = freestyle_position().parts();
293        parts.board.insert(E1, White.knight());
294        let position = parts.validate().unwrap();
295
296        assert!(!position.legal_castle_moves().contains(&Move::castle(White, C1, H)));
297    }
298}