Skip to main content

eschac/
setup.rs

1//! Building chess positions.
2//!
3//! [`Setup`] is a builder for the [`Position`] type.
4
5use crate::bitboard::*;
6use crate::board::*;
7use crate::lookup;
8use crate::position::*;
9
10use alloc::string::String;
11
12/// **A builder type for chess positions.**
13///
14/// This type is useful to edit a position without having to ensure it stays legal at every step.
15/// It must be validated and converted to a [`Position`] using [`Setup::into_position`] before
16/// generating moves.
17///
18/// ## Text description
19///
20/// Forsyth-Edwards Notation (FEN) is typically used to describe chess positions as text. eschac
21/// uses a slightly different notation, which simply removes the last two fields of the FEN record
22/// (i.e. the halfmove clock and the fullmove number) as the [`Position`] type does not keep track
23/// of those. For example, the starting position is recorded as
24/// `rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq -`.
25///
26/// [`Display`](core::fmt::Display) and [`FromStr`](alloc::str::FromStr) are purposely not
27/// implemented on [`Setup`] as there does not exist a truely canonical format for writing chess
28/// positions. The format described above is implemented in
29/// [`from_text_record`](Setup::from_text_record) and [`to_text_record`](AsSetup::to_text_record).
30#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
31pub struct Setup {
32    bitboards: [Bitboard; 5],
33}
34
35impl Setup {
36    /// Creates an empty board, i.e. `8/8/8/8/8/8/8/8 w - -`.
37    #[inline]
38    pub fn new() -> Self {
39        Self::from_bitboards_inner([Bitboard(0); 5])
40    }
41
42    /// Reads a position from a text record
43    /// (see [`from_ascii_record`](Setup::from_ascii_record)).
44    #[inline]
45    pub fn from_text_record(record: &str) -> Result<Self, ParseRecordError> {
46        Self::from_ascii_record(record.as_bytes())
47    }
48
49    /// Reads a position from a text record.
50    pub fn from_ascii_record(record: &[u8]) -> Result<Self, ParseRecordError> {
51        let mut s = record.iter().copied().peekable();
52        (|| {
53            let mut setup = Setup::new();
54            let mut accept_empty_square = true;
55            let mut rank: u8 = 7;
56            let mut file: u8 = 0;
57            for c in s.by_ref() {
58                if c == b'/' {
59                    (file == 8).then_some(())?;
60                    rank = rank.checked_sub(1)?;
61                    file = 0;
62                    accept_empty_square = true;
63                } else if (b'1'..=b'8').contains(&c) && accept_empty_square {
64                    file = file + c - b'0';
65                    (file <= 8).then_some(())?;
66                    accept_empty_square = false;
67                } else if c == b' ' {
68                    break;
69                } else {
70                    let role = Role::from_ascii(c)?;
71                    let color = match c.is_ascii_uppercase() {
72                        true => Color::White,
73                        false => Color::Black,
74                    };
75                    setup = setup.set(
76                        Square::from_coords(File::new(file)?, Rank::new(rank)?),
77                        Some(Piece { role, color }),
78                    );
79                    file += 1;
80                    accept_empty_square = true;
81                }
82            }
83            (rank == 0).then_some(())?;
84            (file == 8).then_some(())?;
85
86            setup = match s.next()? {
87                b'w' => setup.set_turn(Color::White),
88                b'b' => setup.set_turn(Color::Black),
89                _ => return None,
90            };
91            (s.next()? == b' ').then_some(())?;
92
93            if s.next_if_eq(&b'-').is_none() {
94                if s.next_if_eq(&b'K').is_some() {
95                    setup = setup.set_castling_rights(Color::White, Castle::Short, true);
96                }
97                if s.next_if_eq(&b'Q').is_some() {
98                    setup = setup.set_castling_rights(Color::White, Castle::Long, true);
99                }
100                if s.next_if_eq(&b'k').is_some() {
101                    setup = setup.set_castling_rights(Color::Black, Castle::Short, true);
102                }
103                if s.next_if_eq(&b'q').is_some() {
104                    setup = setup.set_castling_rights(Color::Black, Castle::Long, true);
105                }
106            }
107            (s.next()? == b' ').then_some(())?;
108
109            match s.next()? {
110                b'-' => (),
111                file => {
112                    setup = setup.set_en_passant_target_square(Some(Square::from_coords(
113                        File::from_ascii(file)?,
114                        Rank::from_ascii(s.next()?)?,
115                    )));
116                }
117            }
118            s.next().is_none().then_some(())?;
119
120            Some(setup)
121        })()
122        .ok_or_else(|| ParseRecordError {
123            byte: record.len() - s.len(),
124        })
125    }
126
127    /// Sets the occupancy of a square.
128    #[inline]
129    pub fn set(&self, square: Square, piece: Option<Piece>) -> Self {
130        let mask = square.bitboard();
131        let [mut a, mut b, mut c, mut d, e] = self.bitboards();
132        if let Some(piece) = piece {
133            match piece.role {
134                Role::Pawn => {
135                    a |= mask;
136                    b &= !mask;
137                    c &= !mask;
138                }
139                Role::Knight => {
140                    a &= !mask;
141                    b |= mask;
142                    c &= !mask;
143                }
144                Role::Bishop => {
145                    a |= mask;
146                    b |= mask;
147                    c &= !mask;
148                }
149                Role::Rook => {
150                    a &= !mask;
151                    b &= !mask;
152                    c |= mask;
153                }
154                Role::Queen => {
155                    a |= mask;
156                    b &= !mask;
157                    c |= mask;
158                }
159                Role::King => {
160                    a &= !mask;
161                    b |= mask;
162                    c |= mask;
163                }
164            }
165            match piece.color {
166                Color::White => d &= !mask,
167                Color::Black => d |= mask,
168            }
169        }
170        Self::from_bitboards_inner([a, b, c, d, e])
171    }
172
173    /// Sets the color to play.
174    #[inline]
175    pub fn set_turn(&self, color: Color) -> Self {
176        let [a, b, c, d, mut e] = self.bitboards();
177        e.0 &= !2;
178        e.0 |= (color as u8 as u64) << 1;
179        Self::from_bitboards_inner([a, b, c, d, e])
180    }
181
182    /// Switches the color to play.
183    #[inline]
184    pub fn switch_turn(&self) -> Self {
185        let [a, b, c, d, mut e] = self.bitboards();
186        e.0 ^= 2;
187        Self::from_bitboards_inner([a, b, c, d, e])
188    }
189
190    /// Sets the castling rights for the given color and side.
191    #[inline]
192    pub fn set_castling_rights(&self, color: Color, castle: Castle, value: bool) -> Self {
193        let [a, b, c, d, mut e] = self.bitboards();
194        let square = match (color, castle) {
195            (Color::White, Castle::Short) => Square::H1,
196            (Color::White, Castle::Long) => Square::A1,
197            (Color::Black, Castle::Short) => Square::H8,
198            (Color::Black, Castle::Long) => Square::A8,
199        };
200        match value {
201            false => e.remove(square),
202            true => e.insert(square),
203        }
204        Self::from_bitboards_inner([a, b, c, d, e])
205    }
206
207    /// Sets the en passant target square.
208    #[inline]
209    pub fn set_en_passant_target_square(&self, square: Option<Square>) -> Self {
210        let [a, b, c, d, mut e] = self.bitboards();
211        let mask = Rank::Third.bitboard() | Rank::Sixth.bitboard();
212        e &= !mask;
213        if let Some(square) = square {
214            e |= mask & square.bitboard();
215        }
216        Self::from_bitboards_inner([a, b, c, d, e])
217    }
218
219    /// Returns the mirror image of the position.
220    ///
221    /// The mirror of a position is the position obtained after reflecting the placement of pieces
222    /// horizontally, inverting the color of all the pieces, inverting the turn, and reflecting the
223    /// castling rights as well as the en passant target square.
224    ///
225    /// For example, the mirror image of `rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b Kq e3`
226    /// is `rnbqkbnr/pppp1ppp/8/4p3/8/8/PPPPPPPP/RNBQKBNR w Qk e6`.
227    #[inline]
228    pub fn mirror(&self) -> Self {
229        let mask = Square::B1.bitboard();
230        let [a, b, c, d, e] = self.bitboards();
231        let [a, b, c, d] = [a, b, c, d].map(Bitboard::mirror);
232        let turn = (e & mask) ^ mask;
233        let e = (e & !mask).mirror() | turn;
234        Self::from_bitboards_inner([a, b, c, (a | b | c) ^ d, e])
235    }
236
237    /// Checks for the validity of the position, returning a set of potential reasons.
238    pub fn illegal_reasons(&self) -> IllegalPositionReasons {
239        let mut reasons = IllegalPositionReasons::new();
240
241        let [a, b, c, _, _] = self.bitboards();
242        let blockers = a | b | c;
243        let pieces = self.pieces();
244
245        if Color::all()
246            .into_iter()
247            .any(|color| pieces.get(color).king().is_empty())
248        {
249            reasons.insert(IllegalPositionReason::MissingKing);
250        }
251
252        if pieces.get(!self.turn()).king().any(|enemy_king| {
253            let pieces = pieces.get(self.turn());
254            !(lookup::king(enemy_king) & pieces.king()
255                | lookup::bishop(enemy_king, blockers) & (pieces.queen() | pieces.bishop())
256                | lookup::rook(enemy_king, blockers) & (pieces.queen() | pieces.rook())
257                | lookup::knight(enemy_king) & pieces.knight()
258                | lookup::pawn_attack(!self.turn(), enemy_king) & pieces.pawn())
259            .is_empty()
260        }) {
261            reasons.insert(IllegalPositionReason::HangingKing);
262        }
263
264        if Color::all().into_iter().any(|color| {
265            !(pieces.get(color).pawn() & (Rank::First.bitboard() | Rank::Eighth.bitboard()))
266                .is_empty()
267        }) {
268            reasons.insert(IllegalPositionReason::PawnOnBackRank);
269        }
270
271        if Color::all().into_iter().any(|color| {
272            let pieces = pieces.get(color);
273            (pieces.pawn().len()
274                + pieces.queen().len().saturating_sub(1)
275                + pieces.bishop().len().saturating_sub(2)
276                + pieces.knight().len().saturating_sub(2)
277                + pieces.rook().len().saturating_sub(2))
278            .saturating_sub(8)
279                + pieces.king().len().saturating_sub(1)
280                > 0
281        }) {
282            reasons.insert(IllegalPositionReason::TooMuchMaterial);
283        }
284
285        if Color::all().into_iter().any(|color| {
286            Castle::all().into_iter().any(|castle| {
287                self.castling_rights(color, castle)
288                    && !(pieces
289                        .get(color)
290                        .king()
291                        .contains(Square::from_coords(File::E, color.home_rank()))
292                        && pieces.get(color).rook().contains(Square::from_coords(
293                            castle.rook_origin_file(),
294                            color.home_rank(),
295                        )))
296            })
297        }) {
298            reasons.insert(IllegalPositionReason::InvalidCastlingRights);
299        }
300
301        if self.en_passant_target_square().is_some_and(|en_passant| {
302            let (target_rank, pawn_rank) = match self.turn() {
303                Color::White => (Rank::Sixth, Rank::Fifth),
304                Color::Black => (Rank::Third, Rank::Fourth),
305            };
306            let pawn_square = Square::from_coords(en_passant.file(), pawn_rank);
307            en_passant.rank() != target_rank
308                || blockers.contains(en_passant)
309                || !pieces.get(!self.turn()).pawn().contains(pawn_square)
310        }) {
311            reasons.insert(IllegalPositionReason::InvalidEnPassant);
312        }
313
314        if self.en_passant_target_square().is_some_and(|en_passant| {
315            let blockers = blockers
316                & !en_passant.bitboard().trans(match self.turn() {
317                    Color::White => Direction::South,
318                    Color::Black => Direction::North,
319                });
320            pieces
321                .get(self.turn())
322                .king()
323                .first()
324                .is_some_and(|king_square| {
325                    !(lookup::bishop(king_square, blockers)
326                        & (pieces.get(!self.turn()).queen() | pieces.get(!self.turn()).bishop()))
327                    .is_empty()
328                })
329        }) {
330            reasons.insert(IllegalPositionReason::ImpossibleEnPassantPin);
331        }
332
333        reasons
334    }
335
336    /// Tries to convert the position into the [`Position`] type.
337    ///
338    /// Some unreachable positions are rejected, see [`IllegalPositionReason`] for details.
339    #[inline]
340    pub fn into_position(self) -> Result<Position, IllegalPosition> {
341        let reasons = self.illegal_reasons();
342        if reasons.is_empty() {
343            Ok(Position::from_setup_inner(self))
344        } else {
345            Err(IllegalPosition {
346                setup: self,
347                reasons,
348            })
349        }
350    }
351
352    /// Converts the position into the [`Position`] type without checking if it is valid.
353    ///
354    /// **Safety:** if the position is not valid, using it might cause undefined behavior.
355    #[inline]
356    pub unsafe fn into_position_unchecked(self) -> Position {
357        Position::from_setup_inner(self)
358    }
359
360    /// Returns the position encoded by the given bitboards, or `None` if the bitboards are not
361    /// compatible with the format described in [`AsSetup::bitboards`].
362    #[inline]
363    pub fn from_bitboards(bitboards: [Bitboard; 5]) -> Option<Self> {
364        sanity_check(bitboards).then_some(Self { bitboards })
365    }
366
367    /// Returns the position encoded by the given bitboards, without checking if the format is
368    /// respected.
369    ///
370    /// **Safety:** if the bitboards are ill-formed, then using the position obtained from this
371    /// method might cause undefined behavior.
372    #[inline]
373    pub unsafe fn from_bitboards_unchecked(bitboards: [Bitboard; 5]) -> Self {
374        Self::from_bitboards_inner(bitboards)
375    }
376
377    #[inline]
378    pub(crate) fn from_bitboards_inner(bitboards: [Bitboard; 5]) -> Self {
379        debug_assert!(sanity_check(bitboards));
380        Self { bitboards }
381    }
382
383    #[inline]
384    pub(crate) fn role(&self, square: Square) -> Option<Role> {
385        let mask = square.bitboard();
386        let [a, b, c, _, _] = self.bitboards();
387        let bit0 = (a & mask).0 >> square as u8;
388        let bit1 = (b & mask).0 >> square as u8;
389        let bit2 = (c & mask).0 >> square as u8;
390        Role::new((bit0 | bit1 << 1 | bit2 << 2) as u8)
391    }
392
393    #[inline]
394    pub(crate) fn color(&self, square: Square) -> Color {
395        let [_, _, _, x, _] = self.bitboards();
396        match x.contains(square) {
397            false => Color::White,
398            true => Color::Black,
399        }
400    }
401}
402
403#[inline]
404pub(crate) fn sanity_check(bitboards: [Bitboard; 5]) -> bool {
405    let [a, b, c, d, e] = bitboards;
406    let mut res = true;
407    res &= (a & b & c).is_empty();
408    res &= (d & !(a | b | c)).is_empty();
409    res &= (e & !Bitboard(0x8100FF0000FF0083)).is_empty();
410    res &= (e & Bitboard(0x0000FF0000FF0000)).len() <= 1;
411    res
412}
413
414pub(crate) struct TextRecord<'a>(pub(crate) &'a Setup);
415impl<'a> core::fmt::Display for TextRecord<'a> {
416    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
417        self.0.write_text_record(f)
418    }
419}
420impl<'a> core::fmt::Debug for TextRecord<'a> {
421    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
422        use core::fmt::Write;
423        f.write_char('"')?;
424        self.0.write_text_record(f)?;
425        f.write_char('"')?;
426        Ok(())
427    }
428}
429
430impl core::fmt::Debug for Setup {
431    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
432        f.debug_tuple("Setup").field(&TextRecord(&self)).finish()
433    }
434}
435
436impl Default for Setup {
437    #[inline]
438    fn default() -> Self {
439        Self::new()
440    }
441}
442
443/// An invalid position.
444///
445/// This is an illegal position that can't be represented with the [`Position`] type.
446#[derive(Debug)]
447pub struct IllegalPosition {
448    pub setup: Setup,
449    pub reasons: IllegalPositionReasons,
450}
451impl core::fmt::Display for IllegalPosition {
452    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
453        use core::fmt::Write;
454        let setup = &self.setup;
455        write!(f, "`{}` is illegal:", &TextRecord(setup))?;
456        let mut first = true;
457        for reason in self.reasons {
458            if !first {
459                f.write_char(',')?;
460            }
461            first = false;
462            f.write_char(' ')?;
463            f.write_str(reason.to_str())?;
464        }
465        Ok(())
466    }
467}
468impl core::error::Error for IllegalPosition {}
469
470/// A set of [`IllegalPositionReason`]s.
471#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
472pub struct IllegalPositionReasons {
473    set: u8,
474}
475
476impl core::fmt::Debug for IllegalPositionReasons {
477    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
478        f.debug_list().entries(*self).finish()
479    }
480}
481
482impl IllegalPositionReasons {
483    /// Returns `true` if the given reason appears in the set.
484    pub fn contains(&self, reason: IllegalPositionReason) -> bool {
485        self.set & (1 << reason as u8) != 0
486    }
487
488    /// Returns `true` if the set is empty.
489    pub fn is_empty(&self) -> bool {
490        self.set == 0
491    }
492
493    fn new() -> Self {
494        Self { set: 0 }
495    }
496
497    fn insert(&mut self, reason: IllegalPositionReason) {
498        self.set |= 1 << reason as u8;
499    }
500}
501
502impl Iterator for IllegalPositionReasons {
503    type Item = IllegalPositionReason;
504    fn next(&mut self) -> Option<Self::Item> {
505        if self.set == 0 {
506            None
507        } else {
508            let i = self.set.trailing_zeros();
509            self.set &= !(1 << i);
510            IllegalPositionReason::new(i as u8)
511        }
512    }
513}
514
515#[test]
516fn illegal_position_reasons() {
517    let mut reasons = IllegalPositionReasons { set: 64 };
518    assert_eq!(
519        reasons.next(),
520        Some(IllegalPositionReason::ImpossibleEnPassantPin)
521    );
522    assert_eq!(reasons.next(), None);
523    assert_eq!(IllegalPositionReasons { set: 63 }.count(), 6);
524}
525
526/// Reasons for an illegal position to be rejected by eschac.
527#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
528pub enum IllegalPositionReason {
529    /// One of the colors misses its king.
530    MissingKing,
531    /// The opponent's king is in check.
532    HangingKing,
533    /// There is a pawn on the first or eighth rank.
534    PawnOnBackRank,
535    /// Some castling rights are invalid regarding the positions of the rooks and kings.
536    InvalidCastlingRights,
537    /// The en passant target square is invalid, either because:
538    /// - it is not on the correct rank
539    /// - it is occupied
540    /// - it is not behind an opponent's pawn
541    InvalidEnPassant,
542    /// There is an impossible number of pieces.
543    ///
544    /// Enforcing this enables to put an upper limit on the number of legal moves on any position,
545    /// allowing to reduce the size of [`Moves`].
546    TooMuchMaterial,
547    /// The pawn that can be taken en passant is pinned diagonally to the playing king.
548    ///
549    /// This can't happen on a legal position, as it would imply that the king could have be taken
550    /// on that move. Enforcing this makes it unnecessary to test for a discovery check on the
551    /// diagonal when taking en passant.
552    ImpossibleEnPassantPin,
553}
554
555impl IllegalPositionReason {
556    fn new(index: u8) -> Option<Self> {
557        let reason = match index {
558            0 => Self::MissingKing,
559            1 => Self::HangingKing,
560            2 => Self::PawnOnBackRank,
561            3 => Self::InvalidCastlingRights,
562            4 => Self::InvalidEnPassant,
563            5 => Self::TooMuchMaterial,
564            6 => Self::ImpossibleEnPassantPin,
565            _ => return None,
566        };
567        debug_assert!(reason as u8 == index);
568        Some(reason)
569    }
570
571    fn to_str(self) -> &'static str {
572        match self {
573            Self::MissingKing => "missing king",
574            Self::HangingKing => "hanging king",
575            Self::PawnOnBackRank => "pawn on back rank",
576            Self::InvalidCastlingRights => "invalid castling rights",
577            Self::InvalidEnPassant => "invalid en passant",
578            Self::TooMuchMaterial => "too much material",
579            Self::ImpossibleEnPassantPin => "illegal en passant",
580        }
581    }
582}
583
584impl core::fmt::Display for IllegalPositionReason {
585    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
586        f.write_str(self.to_str())
587    }
588}
589
590/// An error when trying to parse a position record.
591#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
592pub struct ParseRecordError {
593    /// The index where the error was detected.
594    pub byte: usize,
595}
596impl core::fmt::Display for ParseRecordError {
597    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
598        write!(f, "invalid text record (at byte {})", self.byte)
599    }
600}
601impl core::error::Error for ParseRecordError {}
602
603/// Common getters for [`Setup`] and [`Position`].
604pub trait AsSetup {
605    fn as_setup(&self) -> &Setup;
606    /// Returns the inner bitboard representation of the position.
607    ///
608    /// In order, those bitboards contain:
609    /// - squares occupied by pawns, bishops & queens
610    /// - squares occupied by knights, bishops & kings
611    /// - squares occupied by rooks, queens & kings
612    /// - squares occupied by black pieces
613    /// - color to play, castling rights and en passant target square[^1]
614    ///
615    /// [^1]: The color to play is represented with the `B1` square: if it's set then it's
616    /// black's turn, else it's white's turn. The castling rights are represented by the
617    /// squares of the corresponding rook. Finally, there can be one en passant target square
618    /// on either the third or sixth rank.
619    #[inline]
620    fn bitboards(&self) -> [Bitboard; 5] {
621        self.as_setup().bitboards
622    }
623    /// Returns the piece on the square, if any.
624    #[inline]
625    fn get(&self, square: Square) -> Option<Piece> {
626        let setup = self.as_setup();
627        Some(Piece {
628            role: setup.role(square)?,
629            color: setup.color(square),
630        })
631    }
632    /// Returns the color to play.
633    #[inline]
634    fn turn(&self) -> Color {
635        let [_, _, _, _, x] = self.bitboards();
636        Color::new(((x.0 >> 1) & 1) as u8).unwrap()
637    }
638    /// Returns `true` if castling is available for the given color and side.
639    #[inline]
640    fn castling_rights(&self, color: Color, castle: Castle) -> bool {
641        let [_, _, _, _, x] = self.bitboards();
642        let square = match (color, castle) {
643            (Color::White, Castle::Short) => Square::H1,
644            (Color::White, Castle::Long) => Square::A1,
645            (Color::Black, Castle::Short) => Square::H8,
646            (Color::Black, Castle::Long) => Square::A8,
647        };
648        x.contains(square)
649    }
650    /// Returns the optional en passant target square.
651    #[inline]
652    fn en_passant_target_square(&self) -> Option<Square> {
653        let [_, _, _, _, x] = self.bitboards();
654        (x & (Rank::Third.bitboard() | Rank::Sixth.bitboard())).first()
655    }
656    /// Returns the squares occupied by each type & color of piece.
657    #[inline]
658    fn pieces(&self) -> ByColor<ByRole<Bitboard>> {
659        let [a, b, c, d, _] = self.bitboards();
660        let t = [
661            a & !b & !c,
662            !a & b & !c,
663            a & b & !c,
664            !a & !b & c,
665            a & !b & c,
666            !a & b & c,
667        ];
668        ByColor([ByRole(t.map(|x| x & !d)), ByRole(t.map(|x| x & d))])
669    }
670    /// Returns the text record of the position
671    /// (see [`write_text_record`](AsSetup::write_text_record)).
672    fn to_text_record(&self) -> String {
673        let mut record = String::with_capacity(81);
674        self.write_text_record(&mut record).unwrap();
675        record
676    }
677    /// Writes the text record of the position.
678    #[inline]
679    fn write_text_record<W: core::fmt::Write>(&self, w: &mut W) -> core::fmt::Result {
680        for rank in Rank::all().into_iter().rev() {
681            let mut count = 0;
682            for file in File::all() {
683                match self.get(Square::from_coords(file, rank)) {
684                    Some(piece) => {
685                        if count > 0 {
686                            w.write_char(char::from_u32('0' as u32 + count).unwrap())?;
687                        }
688                        count = 0;
689                        w.write_char(match piece.color {
690                            Color::White => piece.role.to_char_uppercase(),
691                            Color::Black => piece.role.to_char_lowercase(),
692                        })?;
693                    }
694                    None => {
695                        count += 1;
696                    }
697                }
698            }
699            if count > 0 {
700                w.write_char(char::from_u32('0' as u32 + count).unwrap())?;
701            }
702            if rank != Rank::First {
703                w.write_char('/')?;
704            }
705        }
706
707        w.write_char(' ')?;
708
709        w.write_char(match self.turn() {
710            Color::White => 'w',
711            Color::Black => 'b',
712        })?;
713
714        w.write_char(' ')?;
715
716        let mut no_castle_available = true;
717        if self.castling_rights(Color::White, Castle::Short) {
718            w.write_char('K')?;
719            no_castle_available = false;
720        }
721        if self.castling_rights(Color::White, Castle::Long) {
722            w.write_char('Q')?;
723            no_castle_available = false;
724        }
725        if self.castling_rights(Color::Black, Castle::Short) {
726            w.write_char('k')?;
727            no_castle_available = false;
728        }
729        if self.castling_rights(Color::Black, Castle::Long) {
730            w.write_char('q')?;
731            no_castle_available = false;
732        }
733        if no_castle_available {
734            w.write_char('-')?;
735        }
736
737        w.write_char(' ')?;
738
739        match self.en_passant_target_square() {
740            Some(sq) => {
741                w.write_char(sq.file().to_char())?;
742                w.write_char(sq.rank().to_char())?;
743            }
744            None => {
745                w.write_char('-')?;
746            }
747        }
748
749        Ok(())
750    }
751}
752
753impl AsSetup for Setup {
754    #[inline]
755    fn as_setup(&self) -> &Setup {
756        self
757    }
758}