Skip to main content

chess/position/
validate.rs

1use crate::{
2    Player,
3    board::{Bitboard, Players},
4};
5
6use super::{Error, Parts, Position, Result, Side};
7
8use Player::*;
9
10impl Position {
11    pub fn new(parts: Parts) -> Result<Self> {
12        let Parts { board, turn, castles, en_passant, reversible, round } = parts;
13
14        // 1. Exactly one king for each player
15        let Some(white) = board.unique_king_of(White) else {
16            return Err(Error::KingCount(White));
17        };
18        let Some(black) = board.unique_king_of(Black) else {
19            return Err(Error::KingCount(Black));
20        };
21        let kings = Players { black, white };
22
23        // 2. Kings not adjacent
24        if white.king_moves().contains(black) {
25            return Err(Error::AdjacentKings);
26        }
27
28        // 3. The player who just moved is not in check
29        let other = turn.other();
30        let king = kings.get(other);
31        if !board.attacks_on(king, turn, board.occupied()).is_empty() {
32            return Err(Error::KingAttacked(other));
33        }
34
35        // 4. No pawns are on either back rank
36        if !board.pawns().intersection(Bitboard::BACKRANKS).is_empty() {
37            return Err(Error::PawnOnBackrank);
38        }
39
40        for player in Player::ALL {
41            if !castles.has(player, Side::Queen) && !castles.has(player, Side::King) {
42                continue;
43            }
44
45            // 5. A king with castling rights is on its back rank
46            let king = kings.get(player);
47            if king.rank() != player.backrank() {
48                return Err(Error::CastleKing(player));
49            }
50
51            for side in Side::ALL {
52                let Some(rook_file) = castles.get(player, side) else {
53                    continue;
54                };
55                // 6. Each castle right has player's rook on the indicated square
56                if board.get(player.castle_rook_from(rook_file)) != Some(player.rook()) {
57                    return Err(Error::CastleRook { player, side, file: rook_file });
58                }
59                // 7. This castling rook is on the indicated side of the king
60                if !Side::of_rook(king, rook_file).eq(side) {
61                    return Err(Error::CastleSide { player, side, file: rook_file });
62                }
63            }
64        }
65
66        let en_passant = board.effective_en_passant(en_passant, turn);
67        Ok(Position { board, turn, castles, en_passant, reversible, round })
68    }
69}
70
71impl Parts {
72    pub fn validate(self) -> Result<Position> {
73        Position::new(self)
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use crate::{
80        Player, Position, Side, Square,
81        formats::{Parser as _, fen::parse_position},
82        position::{Error, Parts},
83        square::{File, Square::*},
84    };
85
86    use File::*;
87    use Player::*;
88
89    fn validate(fen: &str) -> Result<Position, Error> {
90        Position::new(parse_position.parse(fen).unwrap())
91    }
92
93    #[test]
94    fn validates_standard_position() {
95        validate("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1").unwrap();
96    }
97
98    #[test]
99    fn rejects_missing_king() {
100        assert_eq!(validate("8/8/8/8/8/8/4P3/4K3 w - - 0 1").unwrap_err(), Error::KingCount(Black));
101    }
102
103    #[test]
104    fn rejects_pawn_on_backrank() {
105        assert_eq!(validate("4k3/8/8/8/8/8/8/4K2P w - - 0 1").unwrap_err(), Error::PawnOnBackrank);
106    }
107
108    #[test]
109    fn drops_ineffective_en_passant() {
110        let position = validate("4k3/8/8/8/8/8/8/4K3 w - e3 0 1").unwrap();
111
112        assert_eq!(position.en_passant, None);
113    }
114
115    #[test]
116    fn rejects_side_not_to_move_in_check() {
117        assert_eq!(
118            validate("4k3/8/8/8/8/8/4R3/4K3 w - - 0 1").unwrap_err(),
119            Error::KingAttacked(Black)
120        );
121    }
122
123    fn freestyle_position(king_file: File, queen_rook: File, king_rook: File) -> Parts {
124        let mut parts = Parts::empty();
125        let board = &mut parts.board;
126        board.insert(Square::new(king_file, White.backrank()), White.king());
127        board.insert(Square::new(queen_rook, White.backrank()), White.rook());
128        board.insert(Square::new(king_rook, White.backrank()), White.rook());
129        board.insert(Square::new(king_file, Black.backrank()), Black.king());
130        board.insert(Square::new(queen_rook, Black.backrank()), Black.rook());
131        board.insert(Square::new(king_rook, Black.backrank()), Black.rook());
132
133        let castles = &mut parts.castles;
134        castles.set(White, Side::Queen, queen_rook);
135        castles.set(White, Side::King, king_rook);
136        castles.set(Black, Side::Queen, queen_rook);
137        castles.set(Black, Side::King, king_rook);
138        parts
139    }
140
141    #[test]
142    fn validates_freestyle_castling() {
143        Position::new(freestyle_position(C, A, H)).unwrap();
144    }
145
146    #[test]
147    fn validates_freestyle_position_without_castling_after_king_moved() {
148        parse_position
149            .parse("2r5/1pb2rkp/6p1/3p1p2/3P1P2/n1PB1R2/P5PP/3RB1K1 w - - 4 26")
150            .map(Position::new)
151            .unwrap()
152            .unwrap();
153    }
154
155    #[test]
156    fn rejects_freestyle_castling_without_backrank_king() {
157        let mut position = freestyle_position(C, A, H);
158        let king = position.board.remove(C1).unwrap();
159        position.board.insert(C2, king);
160
161        assert_eq!(Position::new(position).unwrap_err(), Error::CastleKing(White));
162    }
163
164    #[test]
165    fn rejects_freestyle_castling_with_rook_on_wrong_side() {
166        assert_eq!(
167            Position::new(freestyle_position(C, B, A)).unwrap_err(),
168            Error::CastleSide { player: Black, side: Side::King, file: A }
169        );
170    }
171}