chess/position/
validate.rs1use 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 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 if white.king_moves().contains(black) {
25 return Err(Error::AdjacentKings);
26 }
27
28 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 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 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 if board.get(player.castle_rook_from(rook_file)) != Some(player.rook()) {
57 return Err(Error::CastleRook { player, side, file: rook_file });
58 }
59 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}