blunders_engine/
fen.rs

1//! Forsyth-Edwards Notation, a standard notation for describing a chess position.
2//!
3//! [Wikipedia FEN](https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation)\
4//! [Chess Programming FEN](https://www.chessprogramming.org/Forsyth-Edwards_Notation)\
5//!
6//! Example:\
7//! Starting Chess FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
8
9use std::convert::TryFrom;
10use std::error;
11use std::fmt::{self, Display};
12use std::ops::RangeInclusive;
13use std::str::FromStr;
14
15use crate::boardrepr::{Mailbox, PieceSets};
16use crate::coretypes::{Castling, Color, File, MoveCount, Piece, Rank, Square};
17use crate::position::Position;
18
19#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
20pub enum ParseFenError {
21    IllFormed,
22    Placement,
23    SideToMove,
24    Castling,
25    EnPassant,
26    HalfMoveClock,
27    FullMoveNumber,
28}
29
30impl ParseFenError {
31    pub fn as_str(&self) -> &'static str {
32        use ParseFenError::*;
33        match self {
34            IllFormed => "ill formed",
35            Placement => "placement",
36            SideToMove => "side to move",
37            Castling => "castling",
38            EnPassant => "en passant",
39            HalfMoveClock => "half move clock",
40            FullMoveNumber => "full move number",
41        }
42    }
43}
44
45impl Display for ParseFenError {
46    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
47        write!(f, "{}", self.as_str())
48    }
49}
50
51impl error::Error for ParseFenError {}
52
53/// Implement Fen for any types which can be fully parsed from a FEN string.
54pub trait Fen: Sized {
55    /// Attempt to parse a Fen string into implementing type.
56    fn parse_fen(s: &str) -> Result<Self, ParseFenError>;
57
58    /// Returns string representation of implementing type in Fen format.
59    fn to_fen(&self) -> String;
60
61    /// HalfMove Clock is any non-negative number.
62    fn parse_halfmove_clock(s: &str) -> Result<MoveCount, ParseFenError> {
63        s.parse::<MoveCount>()
64            .map_err(|_| ParseFenError::HalfMoveClock)
65    }
66
67    /// FullMove Number starts at 1, and can increment infinitely.
68    fn parse_fullmove_number(s: &str) -> Result<MoveCount, ParseFenError> {
69        let fullmove: MoveCount = s.parse().unwrap_or(0);
70        if fullmove != 0 {
71            Ok(fullmove)
72        } else {
73            Err(ParseFenError::FullMoveNumber)
74        }
75    }
76}
77
78impl Fen for Position {
79    /// Attempt to parse a Fen string into implementing type.
80    fn parse_fen(s: &str) -> Result<Self, ParseFenError> {
81        // Ensure 6 whitespace separated components.
82        if s.split_whitespace().count() != 6 {
83            return Err(ParseFenError::IllFormed);
84        }
85        let fen_parts: Vec<&str> = s.split_whitespace().collect();
86
87        // Fen Order: Placement/Side-To-Move/Castling/En-Passant/Halfmove/Fullmove
88        let pieces: PieceSets = FenComponent::try_from_fen_str(fen_parts[0])?;
89        let player: Color = FenComponent::try_from_fen_str(fen_parts[1])?;
90        let castling: Castling = FenComponent::try_from_fen_str(fen_parts[2])?;
91        let en_passant: Option<Square> = FenComponent::try_from_fen_str(fen_parts[3])?;
92        let halfmoves: MoveCount = Self::parse_halfmove_clock(fen_parts[4])?;
93        let fullmoves: MoveCount = Self::parse_fullmove_number(fen_parts[5])?;
94
95        Ok(Self {
96            pieces,
97            player,
98            castling,
99            en_passant,
100            halfmoves,
101            fullmoves,
102        })
103    }
104
105    /// Returns string representation of implementing type in Fen format.
106    fn to_fen(&self) -> String {
107        format!(
108            "{} {} {} {} {} {}",
109            self.pieces().to_fen_str(),
110            self.player().to_fen_str(),
111            self.castling().to_fen_str(),
112            self.en_passant().to_fen_str(),
113            self.halfmoves(),
114            self.fullmoves()
115        )
116    }
117}
118
119/// Allows converting data that can be represented as a FEN sub-string
120/// to and from &str.
121pub trait FenComponent: Sized {
122    type Error;
123    fn try_from_fen_str(s: &str) -> Result<Self, Self::Error>;
124    fn to_fen_str(&self) -> String;
125}
126
127/// Placement FenComponent.
128impl FenComponent for Mailbox {
129    type Error = ParseFenError;
130    fn try_from_fen_str(s: &str) -> Result<Self, Self::Error> {
131        // Placement is 8 ranks separated by '/'.
132        // Each rank need to sum up to 8 pieces.
133        const NUMS: RangeInclusive<char> = '1'..='8';
134        const PIECES: [char; 12] = ['R', 'N', 'B', 'Q', 'K', 'P', 'r', 'n', 'b', 'q', 'k', 'p'];
135        const ERR: ParseFenError = ParseFenError::Placement;
136
137        let mut num_ranks = 0u32;
138        let mut squares = Square::iter();
139        let mut board = Mailbox::new();
140
141        // Iterate FEN string in normal Rank-File order.
142        for rank_str in s.split('/').rev() {
143            let mut sum_rank = 0;
144            num_ranks += 1;
145
146            for ch in rank_str.chars() {
147                if NUMS.contains(&ch) {
148                    let num = ch.to_digit(10).ok_or(ERR)?;
149                    squares.nth(num as usize - 1);
150                    sum_rank += num;
151                } else if PIECES.contains(&ch) {
152                    let piece = Piece::try_from(ch).map_err(|_| ERR)?;
153                    let square = squares.next().ok_or(ERR)?;
154                    board[square] = Some(piece);
155                    sum_rank += 1;
156                } else {
157                    return Err(ERR);
158                }
159            }
160            if sum_rank != 8 {
161                return Err(ERR);
162            }
163        }
164
165        (num_ranks == 8)
166            .then(|| board)
167            .ok_or(ParseFenError::Placement)
168    }
169
170    fn to_fen_str(&self) -> String {
171        // For each Rank, count consecutive empty squares.
172        // Before pushing some char, add empty count if not 0 then set to 0.
173        use File::*;
174        use Rank::*;
175        let mut fen_str = String::new();
176
177        for rank in [R8, R7, R6, R5, R4, R3, R2, R1] {
178            let mut empty_counter = 0u8;
179
180            for file in [A, B, C, D, E, F, G, H] {
181                match self[(file, rank)] {
182                    Some(piece) => {
183                        if empty_counter != 0 {
184                            fen_str.push_str(&empty_counter.to_string());
185                            empty_counter = 0;
186                        }
187                        fen_str.push(piece.into())
188                    }
189                    None => empty_counter += 1,
190                };
191            }
192
193            if empty_counter != 0 {
194                fen_str.push_str(&empty_counter.to_string());
195            }
196            fen_str.push('/');
197        }
198        fen_str.pop(); // Extra '/'.
199        fen_str
200    }
201}
202
203/// Placement FenComponent.
204impl FenComponent for PieceSets {
205    type Error = ParseFenError;
206    fn try_from_fen_str(s: &str) -> Result<Self, Self::Error> {
207        Mailbox::try_from_fen_str(s).map(|mailbox| Self::from(&mailbox))
208    }
209    fn to_fen_str(&self) -> String {
210        Mailbox::from(self).to_fen_str()
211    }
212}
213
214/// Side-To-Move FenComponent.
215impl FenComponent for Color {
216    type Error = ParseFenError;
217    /// Side to move is either character 'w' | 'b'
218    fn try_from_fen_str(s: &str) -> Result<Self, Self::Error> {
219        let ch = s.chars().next().ok_or(ParseFenError::SideToMove)?;
220        Color::try_from(ch).map_err(|_| ParseFenError::SideToMove)
221    }
222    fn to_fen_str(&self) -> String {
223        self.to_string()
224    }
225}
226
227/// Castling FenComponent.
228impl FenComponent for Castling {
229    type Error = ParseFenError;
230    /// Castling is either '-' or `[K][Q][k][q]`
231    fn try_from_fen_str(s: &str) -> Result<Self, Self::Error> {
232        Castling::from_str(s).map_err(|_| ParseFenError::Castling)
233    }
234    fn to_fen_str(&self) -> String {
235        self.to_string()
236    }
237}
238
239/// En-Passant FenComponent.
240impl FenComponent for Option<Square> {
241    type Error = ParseFenError;
242    /// En Passant is either - or a square coordinate ex: "a4".
243    fn try_from_fen_str(s: &str) -> Result<Self, Self::Error> {
244        const RANKS: [char; 2] = ['3', '6'];
245        let mut chars = s.chars();
246        let first = chars.next().ok_or(ParseFenError::EnPassant)?;
247
248        if first == '-' {
249            Ok(None)
250        } else {
251            let second = chars.next().ok_or(ParseFenError::EnPassant)?;
252            Ok(Some(
253                RANKS
254                    .contains(&second)
255                    .then(|| Square::from_str(s))
256                    .ok_or(ParseFenError::EnPassant)?
257                    .map_err(|_| ParseFenError::EnPassant)?,
258            ))
259        }
260    }
261    fn to_fen_str(&self) -> String {
262        match self {
263            Some(square) => square.to_string(),
264            None => "-".to_string(),
265        }
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    #[test]
274    fn parse_default_fen_string() {
275        //! Assert that the starting position FEN string parses into Fen object.
276        //! Assert that starting FEN string, parsed Fen object, and default Fen object
277        //! are equivalent.
278        const FEN_STR: &str = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
279        let pos = Position::parse_fen(FEN_STR).unwrap();
280        let start_pos = Position::start_position();
281
282        assert_eq!(pos, start_pos);
283        assert_eq!(pos.to_fen(), FEN_STR);
284        assert_eq!(start_pos.to_fen(), FEN_STR);
285        println!("{}", start_pos.to_fen());
286    }
287
288    #[test]
289    fn parse_placement_fen_substrings() {
290        //! Assert Fen::parse_placement(&str) works properly.
291        const VALID1: &str = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR";
292        const VALID2: &str = "rn1qkb1r/ppp2ppp/4pn2/3p4/3P2bP/2N1PN2/PPP2PP1/R1BQKB1R";
293        const VALID3: &str = "r1Q2rk1/p3qppp/np1bpn2/3p4/1PpP2bP/2N1PN2/PBP2PPR/R3KB2";
294        const VALID4: &str = "2r2rk1/p4p2/nR4Pp/3p4/3P2P1/P1p5/2P1KP1R/4b3";
295
296        const INVALID1: &str = "";
297        const INVALID2: &str = "hello world";
298        const INVALID3: &str = "nbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR";
299        const INVALID4: &str = "nbqkbnr/ pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR";
300        const INVALID5: &str = " rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR";
301        const INVALID6: &str = "rnbqkbnr/pppppppp/27/8/8/8/PPPPPPPP/RNBQKBNR";
302
303        assert_eq!(
304            Mailbox::try_from_fen_str(VALID1).unwrap().to_fen_str(),
305            VALID1
306        );
307        assert_eq!(
308            Mailbox::try_from_fen_str(VALID2).unwrap().to_fen_str(),
309            VALID2
310        );
311        assert_eq!(
312            Mailbox::try_from_fen_str(VALID3).unwrap().to_fen_str(),
313            VALID3
314        );
315        assert_eq!(
316            Mailbox::try_from_fen_str(VALID4).unwrap().to_fen_str(),
317            VALID4
318        );
319        assert!(Mailbox::try_from_fen_str(INVALID1).is_err());
320        assert!(Mailbox::try_from_fen_str(INVALID2).is_err());
321        assert!(Mailbox::try_from_fen_str(INVALID3).is_err());
322        assert!(Mailbox::try_from_fen_str(INVALID4).is_err());
323        assert!(Mailbox::try_from_fen_str(INVALID5).is_err());
324        assert!(Mailbox::try_from_fen_str(INVALID6).is_err());
325    }
326
327    #[test]
328    fn parse_castling_fen_substring() {
329        const VALID1: &str = "-";
330        const VALID2: &str = "Q";
331        const VALID3: &str = "K";
332        const VALID4: &str = "q";
333        const VALID5: &str = "k";
334        const VALID6: &str = "KQkq";
335
336        const INVALID1: &str = "";
337        const INVALID2: &str = "a";
338        const INVALID3: &str = " KQkq";
339
340        assert_eq!(
341            Castling::try_from_fen_str(VALID1).unwrap().to_fen_str(),
342            VALID1
343        );
344        assert_eq!(
345            Castling::try_from_fen_str(VALID2).unwrap().to_fen_str(),
346            VALID2
347        );
348        assert_eq!(
349            Castling::try_from_fen_str(VALID3).unwrap().to_fen_str(),
350            VALID3
351        );
352        assert_eq!(
353            Castling::try_from_fen_str(VALID4).unwrap().to_fen_str(),
354            VALID4
355        );
356        assert_eq!(
357            Castling::try_from_fen_str(VALID5).unwrap().to_fen_str(),
358            VALID5
359        );
360        assert_eq!(
361            Castling::try_from_fen_str(VALID6).unwrap().to_fen_str(),
362            VALID6
363        );
364        assert!(Castling::try_from_fen_str(INVALID1).is_err());
365        assert!(Castling::try_from_fen_str(INVALID2).is_err());
366        assert!(Castling::try_from_fen_str(INVALID3).is_err());
367    }
368}