Skip to main content

chess/position/
plays.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_plays(&self) -> Moves {
12        if self.is_check() {
13            return self.evasion_plays();
14        }
15
16        let shields = self.board().king_shields(self.turn());
17
18        let mut plays = self.pseudo_piece_plays();
19        plays.retain(|play| self.piece_play_is_safe(*play, shields));
20        plays.extend(self.legal_king_plays());
21        plays.extend(self.legal_en_passant_plays());
22        plays.extend(self.legal_castle_plays());
23        plays
24    }
25
26    pub fn legal_castle_plays(&self) -> Moves {
27        let mut plays = Moves::new();
28
29        for side in Side::ALL {
30            if let Some(play) = self.can_castle(side) {
31                plays.push(play);
32            }
33        }
34
35        plays
36    }
37    fn evasion_plays(&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 plays = self.legal_king_evasion_plays(king, checkers);
44        if checkers.more_than_one() {
45            return plays;
46        }
47
48        let Some(checker) = checkers.first() else {
49            return plays;
50        };
51
52        let shields = self.board().king_shields(self.turn());
53        let target = king.between(checker).with(checker);
54        let mut piece_plays = self.pseudo_piece_plays_to(target);
55        piece_plays.retain(|play| self.piece_play_is_safe(*play, shields));
56        plays.extend(piece_plays);
57        plays.extend(self.legal_en_passant_plays());
58
59        plays
60    }
61
62    pub fn pseudo_piece_plays(&self) -> Moves {
63        let target = !self.board().player(self.turn());
64        self.pseudo_piece_plays_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_plays_to(&self, target: Bitboard) -> Moves {
73        let mut plays = Moves::new();
74
75        self.pseudo_pawn_plays(target, &mut plays);
76        for role in [Knight, Bishop, Rook, Queen] {
77            self.pseudo_role_plays(role, target, &mut plays);
78        }
79
80        plays
81    }
82
83    fn legal_king_evasion_plays(&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_play_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 plays = Moves::new();
95        let target = self.board().player(self.turn()).union(attacked);
96
97        self.pseudo_role_plays(King, !target, &mut plays);
98        plays.retain(|play| self.king_play_is_safe(*play));
99
100        plays
101    }
102
103    pub fn legal_king_plays(&self) -> Moves {
104        let mut plays = Moves::new();
105        let target = !self.board().player(self.turn());
106
107        self.pseudo_role_plays(King, target, &mut plays);
108        plays.retain(|play| self.king_play_is_safe(*play));
109
110        plays
111    }
112
113    fn king_play_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_plays(&self, role: Role, target: Bitboard, plays: &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                plays.push(Move::capture(role, from, to, self.board().role_at(to)));
131            }
132        }
133    }
134
135    fn pseudo_pawn_plays(&self, target: Bitboard, plays: &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            plays.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            plays.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            plays.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            plays.extend(Move::pawn(self.turn(), from, to, self.board().role_at(to)));
172        }
173    }
174
175    fn legal_en_passant_plays(&self) -> Moves {
176        let mut plays = 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_plays(self.turn().other()));
186
187            while let Some(from) = pawns.pop_first() {
188                let play = Move::en_passant(from, to);
189                if self.en_passant_play_is_safe(play) {
190                    plays.push(play);
191                }
192            }
193        }
194
195        plays
196    }
197
198    fn piece_play_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_play_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        Game,
258        Player::*,
259        position::{Parts, Terminal},
260        square::{File::*, Square::*},
261    };
262
263    use super::{Move, Position, Side};
264
265    fn freestyle_position() -> Position {
266        let mut parts = Parts::empty();
267        let board = &mut parts.board;
268        board.insert(C1, White.king());
269        board.insert(A1, White.rook());
270        board.insert(H1, White.rook());
271        board.insert(C8, Black.king());
272        board.insert(A8, Black.rook());
273        board.insert(H8, Black.rook());
274
275        let castles = &mut parts.castles;
276        castles.set(White, Side::Queen, A);
277        castles.set(White, Side::King, H);
278        castles.set(Black, Side::Queen, A);
279        castles.set(Black, Side::King, H);
280        parts.validate().unwrap()
281    }
282
283    #[test]
284    fn generate_freestyle_castle_plays() {
285        let plays = freestyle_position().legal_castle_plays();
286
287        assert!(plays.contains(&Move::castle(White, C1, A)));
288        assert!(plays.contains(&Move::castle(White, C1, H)));
289    }
290
291    #[test]
292    fn block_freestyle_castle_play() {
293        let mut parts = freestyle_position().parts();
294        parts.board.insert(E1, White.knight());
295        let position = parts.validate().unwrap();
296
297        assert!(!position.legal_castle_plays().contains(&Move::castle(White, C1, H)));
298    }
299
300    #[test]
301    fn classify_terminal() {
302        for (fen, expected) in [
303            ("7k/6Q1/5K2/8/8/8/8/8 b - - 0 1", Some(Terminal::Checkmate)),
304            ("7k/5K2/6Q1/8/8/8/8/8 b - - 0 1", Some(Terminal::Stalemate)),
305            ("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", None),
306        ] {
307            let position = Position::from_fen(fen).unwrap();
308            assert_eq!(position.is_terminal(), expected.is_some());
309            assert_eq!(position.terminal(), expected);
310            let game = Game::new(position);
311            assert_eq!(game.start().is_terminal(), expected.is_some());
312            assert_eq!(game.start().terminal(), expected);
313        }
314    }
315}