1use core::num::NonZeroU32;
2
3use crate::{
4 board::{self, Bitboard, Board, Piece, Player, PlayerTable},
5 finite::Empty as _,
6 position::{Castles, EnPassant, Parts, Position, Side},
7 square::{File, Rank, Square},
8};
9
10use super::{StrInput as Input, prelude::*};
11
12#[derive(Debug, thiserror::Error)]
19pub enum Error {
20 #[error("invalid FEN: {0}")]
21 Invalid(String),
22}
23
24pub type Result<T, E = Error> = core::result::Result<T, E>;
25
26fn backtrack() -> ErrMode<ContextError> {
27 ErrMode::Backtrack(ContextError::new())
28}
29
30pub fn parse_position(input: &mut Input<'_>) -> ModalResult<Parts> {
33 backtrack_err(preceded(multispace0, position)).parse_next(input)
34}
35
36fn position(input: &mut Input<'_>) -> ModalResult<Parts> {
37 let board = board.parse_next(input)?;
38 let fields = fields.parse_next(input)?;
39 let castles = resolve_castles(board, fields.castle_rights).ok_or_else(backtrack)?;
40 Ok(Parts {
41 board,
42 turn: fields.turn,
43 castles,
44 en_passant: fields.en_passant,
45 reversible: fields.reversible,
46 round: fields.round,
47 })
48}
49
50impl Position {
51 pub fn from_fen(fen: &str) -> Result<Self> {
52 Parts::from_fen(fen)?.validate().map_err(|_| Error::Invalid(fen.to_string()))
53 }
54}
55
56impl Parts {
57 pub fn from_fen(fen: &str) -> Result<Parts> {
58 parse_position.parse(fen).map_err(|_| Error::Invalid(fen.to_string()))
59 }
60}
61
62impl Parts {
63 pub fn apparent_fen(&self) -> String {
64 format!("{} {}", self.board.fen(), self.turn.fen())
65 }
66
67 pub fn fen(&self) -> String {
68 format!(
69 "{} {} {} {} {}",
70 self.apparent_fen(),
71 self.castles.fen(),
72 en_passant_square(self.en_passant),
73 self.reversible,
74 self.round
75 )
76 }
77
78 pub fn chess_fen(&self) -> Option<String> {
79 self.castles.chess_compatible().then(|| {
80 format!(
81 "{} {} {} {} {}",
82 self.apparent_fen(),
83 self.castles.chess_fen(),
84 en_passant_square(self.en_passant),
85 self.reversible,
86 self.round
87 )
88 })
89 }
90
91 pub fn shredder_fen(&self) -> String {
92 format!(
93 "{} {} {} {} {}",
94 self.apparent_fen(),
95 self.castles.shredder_fen(),
96 en_passant_square(self.en_passant),
97 self.reversible,
98 self.round
99 )
100 }
101}
102
103impl Position {
104 pub fn apparent_fen(&self) -> String {
105 format!("{} {}", self.board().fen(), self.turn().fen(),)
106 }
107
108 pub fn fen(&self) -> String {
109 self.parts().fen()
110 }
111
112 pub fn chess_fen(&self) -> Option<String> {
113 self.parts().chess_fen()
114 }
115
116 pub fn shredder_fen(&self) -> String {
117 self.parts().shredder_fen()
118 }
119
120 pub fn transposition_fen(&self) -> String {
121 format!(
122 "{} {} {}",
123 self.apparent_fen(),
124 self.castles().fen(),
125 en_passant_square(self.en_passant()),
126 )
127 }
128}
129
130impl Board {
131 pub fn fen(self) -> String {
132 let mut fen = String::new();
133
134 for rank in Rank::iter_rev() {
135 if rank != Rank::Eight {
136 fen.push('/');
137 }
138
139 let mut empty = 0;
140 for file in File::iter() {
141 let square = Square::new(file, rank);
142 if let Some(piece) = self.get(square) {
143 if empty > 0 {
144 fen.push(char::from_digit(empty, 10).unwrap());
145 empty = 0;
146 }
147 fen.push(piece.char());
148 } else {
149 empty += 1;
150 }
151 }
152 if empty > 0 {
153 fen.push(char::from_digit(empty, 10).unwrap());
154 }
155 }
156
157 fen
158 }
159}
160
161impl Player {
162 pub fn fen(self) -> char {
163 match self {
164 Player::Black => 'b',
165 Player::White => 'w',
166 }
167 }
168}
169
170impl Castles {
171 pub fn fen(self) -> String {
172 if self.chess_compatible() { self.chess_fen() } else { self.shredder_fen() }
173 }
174
175 pub fn chess_fen(self) -> String {
176 use Player::*;
177 use Side::*;
178
179 let mut fen = String::new();
180 if self.has(White, King) {
181 fen.push('K');
182 }
183 if self.has(White, Queen) {
184 fen.push('Q');
185 }
186 if self.has(Black, King) {
187 fen.push('k');
188 }
189 if self.has(Black, Queen) {
190 fen.push('q');
191 }
192 if fen.is_empty() {
193 fen.push('-');
194 }
195 fen
196 }
197
198 pub fn shredder_fen(self) -> String {
199 use Player::*;
200 use Side::*;
201
202 let mut fen = String::new();
203 for (player, side) in [(White, King), (White, Queen), (Black, King), (Black, Queen)] {
204 if let Some(file) = self.get(player, side) {
205 let letter = if player.is_white() { file.upper() } else { file.lower() };
206 fen.push(letter);
207 }
208 }
209 if fen.is_empty() {
210 fen.push('-');
211 }
212 fen
213 }
214}
215
216pub fn board(input: &mut Input<'_>) -> ModalResult<Board> {
217 let mut parts = board::Parts::default();
218 for rank in Rank::iter_rev() {
219 if rank != Rank::Eight {
220 '/'.parse_next(input)?;
221 }
222
223 parts |= board_row(rank).parse_next(input)?;
224 }
225 Board::new(parts).map_err(|_| backtrack())
226}
227
228fn board_row(rank: Rank) -> impl FnMut(&mut Input<'_>) -> ModalResult<board::Parts> {
229 move |input| {
230 let mut row = board::Parts::default();
231 let mut files = File::cursor();
232 loop {
233 if files.done() {
234 return Ok(row);
235 }
236
237 let char = board_fen_char.parse_next(input)?;
238 match char {
239 i @ '1'..='8' => {
240 if !files.skip(i as u8 - b'0') {
241 return Err(backtrack());
242 }
243 }
244 piece => {
245 let Some(file) = files.next() else {
246 return Err(backtrack());
247 };
248 let square = Bitboard::from(Square::new(file, rank));
249 let piece = Piece::panicky_from_char(piece);
250 row.players[piece.player] |= square;
251 row.roles[piece.role] |= square;
252 }
253 }
254 }
255 }
256}
257
258struct Fields {
259 turn: Player,
260 castle_rights: CastleRights,
261 en_passant: Option<EnPassant>,
262 reversible: u32,
263 round: NonZeroU32,
264}
265
266fn fields(input: &mut Input<'_>) -> ModalResult<Fields> {
267 let counters = opt_field((reversible, opt_field(round)));
270 let suffix = opt_field((turn, opt_field((castle_rights, opt_field((en_passant, counters))))))
271 .parse_next(input)?;
272
273 let mut fields = Fields {
274 turn: Player::White,
275 castle_rights: CastleRights::empty(),
276 en_passant: None,
277 reversible: 0,
278 round: NonZeroU32::MIN,
279 };
280
281 let Some((turn, suffix)) = suffix else {
282 return Ok(fields);
283 };
284 fields.turn = turn;
285
286 let Some((castle_rights, suffix)) = suffix else {
287 return Ok(fields);
288 };
289 fields.castle_rights = castle_rights;
290
291 let Some((en_passant, suffix)) = suffix else {
292 return Ok(fields);
293 };
294 fields.en_passant = en_passant;
295
296 let Some((reversible, round)) = suffix else {
297 return Ok(fields);
298 };
299 fields.reversible = reversible;
300
301 if let Some(round) = round {
302 fields.round = round;
303 }
304
305 Ok(fields)
306}
307
308fn en_passant_square(en_passant: Option<EnPassant>) -> String {
309 en_passant.map_or_else(|| "-".to_string(), |square| Square::from(square).to_string())
310}
311
312fn board_fen_char(input: &mut Input<'_>) -> ModalResult<char> {
313 one_of(|c| "12345678pnbrkqPNBRKQ".contains(c)).parse_next(input)
314}
315
316fn turn(input: &mut Input<'_>) -> ModalResult<Player> {
317 one_of(|c| "bw".contains(c))
318 .map(|c| match c {
319 'b' => Player::Black,
320 'w' => Player::White,
321 _ => unreachable!(),
322 })
323 .parse_next(input)
324}
325
326type CastleRights = PlayerTable<Vec<CastleRight>>;
327
328#[derive(Clone, Copy, Eq, PartialEq)]
329enum CastleRight {
330 File(File),
331 Side(Side),
332}
333
334impl From<File> for CastleRight {
335 fn from(file: File) -> Self {
336 Self::File(file)
337 }
338}
339
340impl From<Side> for CastleRight {
341 fn from(side: Side) -> Self {
342 Self::Side(side)
343 }
344}
345
346fn resolve_castles(board: Board, rights: CastleRights) -> Option<Castles> {
347 let mut castles = Castles::empty();
348
349 for player in Player::iter() {
350 let rights = rights.get_ref(player);
351 if rights.is_empty() {
352 continue;
353 }
354
355 let king = board.king_of(player)?;
356 for &right in rights.iter() {
357 let (side, file) = match right {
365 CastleRight::File(file) => (Side::of_rook(king, file), file),
366 CastleRight::Side(side) => (side, x_fen_rook(board, player, king, side)?),
367 };
368 if castles.has(player, side) {
370 return None;
371 }
372 castles.set(player, side, file);
373 }
374 }
375
376 Some(castles)
377}
378
379fn x_fen_rook(board: Board, player: Player, king: Square, side: Side) -> Option<File> {
382 let backrank_rooks = board
383 .rooks()
384 .intersection(board.player(player))
385 .intersection(Bitboard::from_rank(player.backrank()));
386 match side {
387 Side::King => backrank_rooks.intersection(king.east()).last(),
388 Side::Queen => backrank_rooks.intersection(king.west()).first(),
389 }
390 .map(Square::file)
391}
392
393fn castle_rights(input: &mut Input<'_>) -> ModalResult<CastleRights> {
394 alt(('-'.value(CastleRights::empty()), some_castles)).parse_next(input)
395}
396
397fn some_castles(input: &mut Input<'_>) -> ModalResult<CastleRights> {
398 use Player::*;
399
400 let mut rights = PlayerTable::default();
401 rights[White] = opt(player_castles(White)).parse_next(input)?.unwrap_or_default();
402 rights[Black] = opt(player_castles(Black)).parse_next(input)?.unwrap_or_default();
403
404 if rights.is_empty() {
405 return Err(backtrack());
406 }
407
408 Ok(rights)
409}
410
411fn player_castles<'i>(
412 player: Player,
413) -> impl FnMut(&mut Input<'i>) -> ModalResult<Vec<CastleRight>> {
414 move |input: &mut Input<'i>| {
415 let mut castle_letter = castle_letter(player);
416 let first = castle_letter.parse_next(input)?;
417 let mut rights = vec![first];
418
419 if let Some(second) = opt(&mut castle_letter).parse_next(input)? {
420 if first == second {
421 return Err(backtrack());
422 }
423 rights.push(second);
424 }
425
426 Ok(rights)
427 }
428}
429
430fn castle_letter<'i>(player: Player) -> impl FnMut(&mut Input<'i>) -> ModalResult<CastleRight> {
431 move |input: &mut Input<'i>| {
432 let letters = if player.is_black() { "abcdefghkq" } else { "ABCDEFGHKQ" };
433 one_of(|c| letters.contains(c))
434 .map(|letter: char| match letter.to_ascii_lowercase() {
435 'k' => Side::King.into(),
436 'q' => Side::Queen.into(),
437 'a'..='h' => File::panicky_from_char(letter).into(),
438 _ => unreachable!(),
439 })
440 .parse_next(input)
441 }
442}
443
444fn file(input: &mut Input<'_>) -> ModalResult<File> {
445 one_of(|c| "abcdefgh".contains(c)).map(File::panicky_from_char).parse_next(input)
446}
447
448fn en_passant(input: &mut Input<'_>) -> ModalResult<Option<EnPassant>> {
449 alt((
450 '-'.value(None),
451 terminated(file, '3').map(|file| Some(Square::new(file, Rank::Three).try_into().unwrap())),
452 terminated(file, '6').map(|file| Some(Square::new(file, Rank::Six).try_into().unwrap())),
453 ))
454 .parse_next(input)
455}
456
457fn reversible(input: &mut Input<'_>) -> ModalResult<u32> {
458 dec_uint.parse_next(input)
459}
460
461fn round(input: &mut Input<'_>) -> ModalResult<NonZeroU32> {
462 dec_uint.verify_map(NonZeroU32::new).parse_next(input)
463}
464
465fn opt_field<'i, O>(
466 parser: impl Parser<Input<'i>, O, ErrMode<ContextError>>,
467) -> impl Parser<Input<'i>, Option<O>, ErrMode<ContextError>> {
468 opt(preceded(space1, cut_err(parser)))
469}
470
471#[test]
472fn board_fen_example() {
473 use File::*;
474 use Player::*;
475 use Rank::*;
476 use Side::*;
477
478 let fen = "rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKBNR b KQkq e3 1 3";
485 let position = parse_position.parse(fen).unwrap();
486 assert_eq!(position.turn, Black);
487 assert!(position.castles.has(Black, King));
488 assert!(position.castles.has(Black, Queen));
489 assert!(position.castles.has(White, King));
490 assert!(position.castles.has(White, Queen));
491 assert_eq!(position.en_passant.map(Into::into), Some(Square::new(E, Three)));
492 assert_eq!(position.reversible, 1);
493 assert_eq!(u32::from(position.round), 3);
494 assert_eq!(
495 position.validate().unwrap().fen(),
496 "rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKBNR b KQkq - 1 3"
497 );
498 assert_eq!(
499 Position::from_fen(fen).unwrap().fen(),
500 "rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKBNR b KQkq - 1 3"
501 );
502
503 let partial_fen = "rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKBNR b KQkq e3";
504 let position = parse_position.parse(partial_fen).unwrap();
505 assert_eq!(position.turn, Black);
506 assert!(position.castles.has(Black, King));
507 assert!(position.castles.has(Black, Queen));
508 assert!(position.castles.has(White, King));
509 assert!(position.castles.has(White, Queen));
510 assert_eq!(position.en_passant.map(Into::into), Some(Square::new(E, Three)));
511 assert_eq!(position.reversible, 0);
512 assert_eq!(u32::from(position.round), 1);
513 assert_eq!(
514 position.validate().unwrap().fen(),
515 "rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKBNR b KQkq - 0 1"
516 );
517 assert_eq!(
518 Position::from_fen(partial_fen).unwrap().fen(),
519 "rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKBNR b KQkq - 0 1"
520 );
521
522 let board_fen = "rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKBNR";
523 let position = parse_position.parse(board_fen).unwrap();
524 assert_eq!(position.turn, White);
525 assert!(!position.castles.has(Black, King));
526 assert!(!position.castles.has(Black, Queen));
527 assert!(!position.castles.has(White, King));
528 assert!(!position.castles.has(White, Queen));
529 assert_eq!(position.en_passant, None);
530 assert_eq!(position.reversible, 0);
531 assert_eq!(u32::from(position.round), 1);
532 assert_eq!(
533 position.validate().unwrap().fen(),
534 "rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKBNR w - - 0 1"
535 );
536 assert_eq!(
537 Position::from_fen(board_fen).unwrap().fen(),
538 "rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKBNR w - - 0 1"
539 );
540}
541
542#[test]
543fn parses_shredder_castling() {
544 use File::*;
545 use Player::*;
546 use Side::*;
547
548 let fen = "bqnb1rkr/pp3ppp/3ppn2/2p5/5P2/P2P4/NPP1P1PP/BQ1BNRKR w HFhf - 2 9";
549 let position = Position::from_fen(fen).unwrap();
550 assert_eq!(position.castles().get(White, King), Some(H));
551 assert_eq!(position.castles().get(White, Queen), Some(F));
552 assert_eq!(position.castles().get(Black, King), Some(H));
553 assert_eq!(position.castles().get(Black, Queen), Some(F));
554 assert_eq!(position.fen(), fen);
555 assert_eq!(
556 position.transposition_fen(),
557 "bqnb1rkr/pp3ppp/3ppn2/2p5/5P2/P2P4/NPP1P1PP/BQ1BNRKR w HFhf -"
558 );
559}
560
561#[test]
562fn parses_x_fen_castling() {
563 use File::*;
564 use Player::*;
565 use Side::*;
566
567 let fen = "bbqnnrkr/pppppppp/8/8/8/8/PPPPPPPP/BBQNNRKR w KQkq - 0 1";
568 let position = Position::from_fen(fen).unwrap();
569
570 assert_eq!(position.castles().get(White, King), Some(H));
571 assert_eq!(position.castles().get(White, Queen), Some(F));
572 assert_eq!(position.castles().get(Black, King), Some(H));
573 assert_eq!(position.castles().get(Black, Queen), Some(F));
574 assert_eq!(position.fen(), "bbqnnrkr/pppppppp/8/8/8/8/PPPPPPPP/BBQNNRKR w HFhf - 0 1");
575}
576
577#[test]
578fn writes_chess_and_shredder_castling() {
579 use File::*;
580 use Player::*;
581 use Side::*;
582
583 let mut castles = Castles::empty();
584 castles.set(White, King, H);
585 castles.set(White, Queen, F);
586 castles.set(Black, King, H);
587 castles.set(Black, Queen, F);
588
589 assert_eq!(castles.chess_fen(), "KQkq");
590 assert_eq!(castles.shredder_fen(), "HFhf");
591 assert_eq!(castles.fen(), "HFhf");
592 assert_eq!(Castles::chess().fen(), "KQkq");
593 assert_eq!(Castles::empty().fen(), "-");
594 assert_eq!(Castles::empty().chess_fen(), "-");
595 assert_eq!(Castles::empty().shredder_fen(), "-");
596}
597
598#[test]
599fn castle_resolves_x_fen_castling() {
600 use File::*;
601 use Player::*;
602 use Side::*;
603
604 let board = Board::freestyle(crate::Scharnagl::new(0).unwrap());
605 let rights = castle_rights.parse("KQkq").unwrap();
606 let castles = resolve_castles(board, rights).unwrap();
607
608 assert_eq!(castles.get(White, King), Some(H));
609 assert_eq!(castles.get(White, Queen), Some(F));
610 assert_eq!(castles.get(Black, King), Some(H));
611 assert_eq!(castles.get(Black, Queen), Some(F));
612}
613
614#[test]
615fn castle_resolves_shredder_castling() {
616 use File::*;
617 use Player::*;
618 use Side::*;
619
620 let board = Board::freestyle(crate::Scharnagl::new(0).unwrap());
621 let rights = castle_rights.parse("HFhf").unwrap();
622 let castles = resolve_castles(board, rights).unwrap();
623
624 assert_eq!(castles.get(White, King), Some(H));
625 assert_eq!(castles.get(White, Queen), Some(F));
626 assert_eq!(castles.get(Black, King), Some(H));
627 assert_eq!(castles.get(Black, Queen), Some(F));
628}
629
630#[test]
631fn rejects_duplicate_castling_files() {
632 let fen = "bqnb1rkr/pp3ppp/3ppn2/2p5/5P2/P2P4/NPP1P1PP/BQ1BNRKR w HH - 2 9";
633 assert!(Parts::from_fen(fen).is_err());
634}
635
636#[test]
637fn rejects_more_than_two_castling_files_per_player() {
638 let fen = "bqnb1rkr/pp3ppp/3ppn2/2p5/5P2/P2P4/NPP1P1PP/BQ1BNRKR w HFAh - 2 9";
639 assert!(Parts::from_fen(fen).is_err());
640}
641
642#[test]
643fn board_row_parses_exactly_one_rank() {
644 assert!(board_row(Rank::Eight).parse("rnbqkbnr").is_ok());
645 assert!(board_row(Rank::Eight).parse("8").is_ok());
646}
647
648#[test]
649fn board_row_rejects_invalid_rank_width() {
650 assert!(board_row(Rank::Eight).parse("7").is_err());
651 assert!(board_row(Rank::Eight).parse("9").is_err());
652 assert!(board_row(Rank::Eight).parse("rnbqkbnrr").is_err());
653 assert!(board_row(Rank::Eight).parse("8r").is_err());
654}
655
656#[test]
657fn rejects_invalid_board_rank_width() {
658 assert!(Parts::from_fen("8/8/8/8/8/8/8/8 w - - 0 1").is_ok());
659 assert!(Parts::from_fen("8/8/8/8/8/8/8/7 w - - 0 1").is_err());
660 assert!(Parts::from_fen("8/8/8/8/8/8/8/9 w - - 0 1").is_err());
661 assert!(Parts::from_fen("8/8/8/8/8/8/8/8r w - - 0 1").is_err());
662}