Skip to main content

simbelmyne_chess/
fen.rs

1//! Logic for parsing FEN strings
2//!
3//! A FEN string (short for Forsyth-Edwards Notation) captures an entire board
4//! state at a given point in time. This includes more than just the actual
5//! pieces: it also includes whose turn it is, what castling rights remain,
6//! whether it's possible to capture en-passant on this turn, etc...
7//!
8//! An example of a FEN-serialized board is:
9//!
10//!   rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2
11//!
12//! A FEN string always consists of 6 space-separated parts:
13//!
14//! 1. rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR
15//!  The piece list, read as follows: Starting at the top rank, each character
16//!  either represents a piece (in standard algebraic notation), or a number
17//!  that represents a number of open squares until the next piece (or the end
18//!  of the rank).
19//!    
20//! 2. w
21//!  The player to go next
22//!
23//! 3. KQkq
24//!  The remaining castling rights, read as "White Kingside",
25//!  "White Queenside", etc... If no castling rights remain, it's simply
26//!  written as a "-". (Note that these castling rights do not include
27//!  temporary states like "this square is currently under attack". It only
28//!  tracks whether or not the king/rooks have moved, and thus can never
29//!  castle.
30//!
31//! 4. c6
32//!  The square that is currently viable for an en-passant capture. This gets
33//!  unset on the next move (or updated, if a new square becomes available).
34//!  Some as with castling rights, it's simply written as a "-" when unset.
35//!
36//! 5. 0
37//!  The half-move clock. This counts the number of half-turns (i.e, ply)
38//!  since the last capture or pawn move. We need this to uphold the 50 move
39//!  rule
40//!
41//! 6. 2
42//!  The turn counter. Monotonically increasing counter that keeps track of
43//!  how many full turns have gone. Gets incremented at the end of Black's turn.
44//!
45//! Not doing the best job at having clear errors when passed invalid FEN
46//! strings, it'll just scream "Invalid!" and blow up. 💥
47
48use std::str::FromStr;
49
50use crate::bitboard::Bitboard;
51use crate::board::Board;
52use crate::movegen::castling::CastlingRights;
53use crate::piece::Color;
54use crate::piece::Piece;
55use crate::piece::PieceType;
56use crate::square::Square;
57use anyhow::anyhow;
58use itertools::Itertools;
59
60impl Board {
61  // Serialize a board into a FEN string
62  pub fn to_fen(&self) -> String {
63    let ranks = self.piece_list.into_iter().chunks(8);
64    let ranks = ranks.into_iter().collect_vec();
65    let mut rank_strs: Vec<String> = Vec::new();
66
67    for rank in ranks.into_iter().rev() {
68      let mut elements: Vec<String> = Vec::new();
69      let piece_runs = rank.into_iter().group_by(|p| p.is_some());
70
71      for run in &piece_runs {
72        match run {
73          (true, pieces) => {
74            for piece in pieces {
75              elements.push(piece.unwrap().to_string())
76            }
77          }
78          (false, gaps) => elements.push(gaps.count().to_string()),
79        }
80      }
81
82      rank_strs.push(elements.join(""));
83    }
84
85    let pieces = rank_strs.into_iter().join("/");
86    let next_player = self.current.to_string();
87    let castling = self.castling_rights.to_string();
88    let en_passant = self
89      .en_passant
90      .map(|sq| sq.to_string())
91      .unwrap_or(String::from("-"));
92    let half_moves = self.half_moves;
93    let full_moves = self.full_moves;
94
95    format!("{pieces} {next_player} {castling} {en_passant} {half_moves} {full_moves}")
96  }
97
98  // Parse a board from a FEN string
99  pub fn from_fen(fen: &str) -> anyhow::Result<Board> {
100    let mut parts = fen.split(' ');
101
102    let piece_string = parts.next().ok_or(anyhow!("Invalid FEN string"))?;
103
104    // Parse the pieces
105
106    let mut piece_bbs = [Bitboard::EMPTY; PieceType::COUNT];
107    let mut occupied_squares = [Bitboard::EMPTY; Color::COUNT];
108    let mut piece_list = [None; Square::COUNT];
109    let mut square_idx: usize = 0;
110
111    // FEN starts with the 8th rank down, so we need to reverse the ranks
112    // to go in ascending order
113    for rank in piece_string.split('/').rev() {
114      for c in rank.chars() {
115        let c = c.to_string();
116
117        if let Ok(gap) = usize::from_str(&c) {
118          square_idx += gap;
119        } else if let Ok(piece) = Piece::from_str(&c) {
120          let square = Square::from(square_idx);
121          let bb = Bitboard::from(square);
122
123          piece_list[square_idx] = Some(piece);
124          piece_bbs[piece.piece_type()] |= bb;
125          occupied_squares[piece.color()] |= bb;
126
127          square_idx += 1;
128        }
129      }
130    }
131
132    // Parse the game state
133
134    let current: Color =
135      parts.next().ok_or(anyhow!("Invalid FEN string"))?.parse()?;
136
137    let castling_rights: CastlingRights =
138      parts.next().ok_or(anyhow!("Invalid FEN string"))?.parse()?;
139
140    let en_passant: Option<Square> = parts
141      .next()
142      .ok_or(anyhow!("Invalid FEN string"))?
143      .parse()
144      .ok();
145
146    let half_moves =
147      parts.next().ok_or(anyhow!("Invalid FEN string"))?.parse()?;
148
149    let full_moves =
150      parts.next().ok_or(anyhow!("Invalid FEN string"))?.parse()?;
151
152    let board = Board::new(
153      piece_list,
154      piece_bbs,
155      occupied_squares,
156      current,
157      castling_rights,
158      en_passant,
159      half_moves,
160      full_moves,
161    );
162
163    Ok(board)
164  }
165}
166
167////////////////////////////////////////////////////////////////////////////////
168//
169// Tests
170//
171////////////////////////////////////////////////////////////////////////////////
172
173#[test]
174fn test_to_fen() {
175  let initial_fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
176  let board = Board::from_str(initial_fen).unwrap();
177  let fen = board.to_fen();
178  assert_eq!(initial_fen, fen);
179}