1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
//! Contains the [Fen] struct which interprets a [Forsyth–Edwards Notation (FEN)](https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation) string.

use crate::castling_rights::CastlingRights;
use crate::chess_board::{BoardPosition, PieceColor, PieceType};

/// The FEN which represents the default starting position.
const STARTING_FEN: &str = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";

/// A representation of a board state based on FEN notation.
#[derive(Debug, Clone)]
pub struct Fen {
    /// The piece placement.
    piece_placement: [[Option<(PieceColor, PieceType)>; 8]; 8],
    /// The active color.
    active_color: PieceColor,
    /// The castling rights.
    castling_rights: CastlingRights,
    /// A square over which a pawn has just passed after moving two squares, if available.
    ep_target_square: Option<BoardPosition>,
    /// The number of halfmoves since the last capture or pawn advance.
    halfmove_clock: i32,
    /// The number of the full moves. It starts at 1 and is incremented after Black's move.
    fullmove_number: i32,
}

impl Fen {
    /// Creates a new [Fen] from the given string.
    pub fn from_string(fen_string: &str) -> Self {
        // First split fen into sections separated by spaces
        let split_fen = fen_string.split_whitespace().collect::<Vec<&str>>();

        // Get piece placement data
        let piece_placement_string = split_fen[0];
        // Create an empty board state
        let mut piece_placement = [[None; 8]; 8];
        // Populate it from the given fen string
        let mut rank = 0;
        let mut file = 0;
        for rank_str in piece_placement_string.split('/') {
            for symbol in rank_str.chars().collect::<Vec<char>>() {
                if symbol.is_digit(9) {
                    file += symbol.to_digit(9).unwrap() as usize;
                } else {
                    let piece_color = if symbol.is_uppercase() {
                        PieceColor::White
                    } else {
                        PieceColor::Black
                    };
                    let piece_type = match symbol.to_uppercase().next().unwrap() {
                        'P' => PieceType::Pawn,
                        'N' => PieceType::Knight,
                        'B' => PieceType::Bishop,
                        'R' => PieceType::Rook,
                        'Q' => PieceType::Queen,
                        'K' => PieceType::King,
                        _ => panic!("Unrecognised symbol in FEN: {}", symbol),
                    };
                    piece_placement[rank][file] = Some((piece_color, piece_type));
                    file += 1;
                }
                if file >= 8 {
                    rank += 1;
                    file = 0;
                };
            }
        }

        // Get active color
        let active_color = match split_fen[1] {
            "w" => PieceColor::White,
            "b" => PieceColor::Black,
            _ => panic!("Unrecognised active color in FEN: {}", split_fen[1]),
        };

        // Get castling rights
        let castling_rights = CastlingRights::from_fen_string(split_fen[2]);

        // Get en passant target square
        let ep_target_square = match split_fen[3] {
            "-" => None,
            _ => Some(BoardPosition::new(
                Self::char_to_rank(split_fen[3].chars().nth(1).unwrap()),
                Self::char_to_file(split_fen[3].chars().next().unwrap()),
            )),
        };

        // Create Fen object
        Fen {
            piece_placement,
            active_color,
            castling_rights,
            ep_target_square,
            halfmove_clock: split_fen[4].parse::<i32>().unwrap(),
            fullmove_number: split_fen[5].parse::<i32>().unwrap(),
        }
    }

    /// Returns the piece placement.
    pub fn piece_placement(&self) -> &[[Option<(PieceColor, PieceType)>; 8]; 8] {
        &self.piece_placement
    }

    /// Returns the active color.
    pub fn active_color(&self) -> &PieceColor {
        &self.active_color
    }

    /// Returns the castling rights.
    pub fn castling_rights(&self) -> &CastlingRights {
        &self.castling_rights
    }

    /// Returns the en passant target square.
    pub fn ep_target_square(&self) -> &Option<BoardPosition> {
        &self.ep_target_square
    }

    /// Returns the number of halfmoves since the last capture or pawn advance.
    pub fn halfmove_clock(&self) -> &i32 {
        &self.halfmove_clock
    }

    /// Returns the number of the full moves.
    pub fn fullmove_number(&self) -> &i32 {
        &self.fullmove_number
    }

    /// Converts the given rank char to the corresponding board index.
    fn char_to_rank(char: char) -> usize {
        match char {
            '0' => 0,
            '1' => 1,
            '2' => 2,
            '3' => 3,
            '4' => 4,
            '5' => 5,
            '6' => 6,
            '7' => 7,
            _ => panic!("Unexpected rank char: {}.", char),
        }
    }

    /// Converts the given file char to the corresponding board index.
    fn char_to_file(char: char) -> usize {
        match char {
            'a' => 0,
            'b' => 1,
            'c' => 2,
            'd' => 3,
            'e' => 4,
            'f' => 5,
            'g' => 6,
            'h' => 7,
            _ => panic!("Unexpected file char: {}.", char),
        }
    }
}

impl Default for Fen {
    fn default() -> Self {
        Fen::from_string(STARTING_FEN)
    }
}

#[cfg(test)]
mod tests {
    //! Unit tests for the [Fen] module.
    use crate::chess_board::BOARD_SIZE;

    use super::*;

    #[test]
    fn test_fen_from_string() {
        // Randomly generated fen
        let fen_string = "5R2/2p4n/1Q6/6Pp/1R2P3/2P2b1K/P2krq2/2N5 w - - 0 1";

        // Create a new fen from the above string
        let fen = Fen::from_string(fen_string);

        // Confirm that the fen has the correct properties
        let expected_placement = [
            [
                None,
                None,
                None,
                None,
                None,
                Some((PieceColor::White, PieceType::Rook)),
                None,
                None,
            ],
            [
                None,
                None,
                Some((PieceColor::Black, PieceType::Pawn)),
                None,
                None,
                None,
                None,
                Some((PieceColor::Black, PieceType::Knight)),
            ],
            [
                None,
                Some((PieceColor::White, PieceType::Queen)),
                None,
                None,
                None,
                None,
                None,
                None,
            ],
            [
                None,
                None,
                None,
                None,
                None,
                None,
                Some((PieceColor::White, PieceType::Pawn)),
                Some((PieceColor::Black, PieceType::Pawn)),
            ],
            [
                None,
                Some((PieceColor::White, PieceType::Rook)),
                None,
                None,
                Some((PieceColor::White, PieceType::Pawn)),
                None,
                None,
                None,
            ],
            [
                None,
                None,
                Some((PieceColor::White, PieceType::Pawn)),
                None,
                None,
                Some((PieceColor::Black, PieceType::Bishop)),
                None,
                Some((PieceColor::White, PieceType::King)),
            ],
            [
                Some((PieceColor::White, PieceType::Pawn)),
                None,
                None,
                Some((PieceColor::Black, PieceType::King)),
                Some((PieceColor::Black, PieceType::Rook)),
                Some((PieceColor::Black, PieceType::Queen)),
                None,
                None,
            ],
            [
                None,
                None,
                Some((PieceColor::White, PieceType::Knight)),
                None,
                None,
                None,
                None,
                None,
            ],
        ];
        for rank in 0..BOARD_SIZE {
            for file in 0..BOARD_SIZE {
                if fen.piece_placement[rank][file].is_some() {
                    assert_eq!(
                        fen.piece_placement[rank][file].unwrap().0,
                        expected_placement[rank][file].unwrap().0
                    );
                    assert_eq!(
                        fen.piece_placement[rank][file].unwrap().1,
                        expected_placement[rank][file].unwrap().1
                    );
                } else {
                    assert_eq!(expected_placement[rank][file], None);
                }
            }
        }

        assert_eq!(fen.active_color, PieceColor::White);
        assert_eq!(
            fen.castling_rights,
            CastlingRights {
                white: [false, false],
                black: [false, false]
            }
        );
        assert_eq!(fen.ep_target_square, None);
        assert_eq!(fen.halfmove_clock, 0);
        assert_eq!(fen.fullmove_number, 1);
    }
}