diff --git a/README.md b/README.md index 5fae89b..7841b4d 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ time that no illegal move is played, with no runtime checks and no potential pan use eschac::prelude::*; // read a position from a text record -let setup = "7k/4P1rp/5Q2/5p2/1Pp1bP2/8/r4K1P/6R1 w - -".parse::()?; +let setup = Setup::from_text_record("7k/4P1rp/5Q2/5p2/1Pp1bP2/8/r4K1P/6R1 w - -")?; let position = setup.validate()?; // read a move in algebraic notation diff --git a/src/bitboard.rs b/src/bitboard.rs index 41c3467..722a01b 100644 --- a/src/bitboard.rs +++ b/src/bitboard.rs @@ -57,7 +57,7 @@ impl Bitboard { self.0 & (1 << square as u8) != 0 } - /// Returns the mirror of the bitboard (see [`Setup::mirror`]). + /// Returns the mirror of the bitboard (see [`Setup::mirror`](crate::setup::Setup::mirror)). #[inline] pub fn mirror(self) -> Bitboard { let [a, b, c, d, e, f, g, h] = self.0.to_le_bytes(); diff --git a/src/lib.rs b/src/lib.rs index df4f342..de60b7e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,7 +27,7 @@ //! use eschac::prelude::*; //! //! // read a position from a text record -//! let setup = "7k/4P1rp/5Q2/5p2/1Pp1bP2/8/r4K1P/6R1 w - -".parse::()?; +//! let setup = Setup::from_text_record("7k/4P1rp/5Q2/5p2/1Pp1bP2/8/r4K1P/6R1 w - -")?; //! let position = setup.validate()?; //! //! // read a move in algebraic notation diff --git a/src/position.rs b/src/position.rs index e822fd5..b187631 100644 --- a/src/position.rs +++ b/src/position.rs @@ -65,7 +65,7 @@ const MAX_LEGAL_MOVES: usize = 218; impl std::fmt::Debug for Position { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { f.debug_tuple("Position") - .field(&self.as_setup().to_string()) + .field(&self.as_setup().to_text_record()) .finish() } } @@ -96,12 +96,26 @@ impl Position { /// ``` /// # use eschac::setup::Setup; /// # |s: &str| -> Option { - /// s.parse::().ok().and_then(|pos| pos.validate().ok()) + /// Setup::from_text_record(s).ok().and_then(|pos| pos.validate().ok()) /// # }; /// ``` #[inline] pub fn from_text_record(s: &str) -> Option { - s.parse::().ok().and_then(|pos| pos.validate().ok()) + Setup::from_text_record(s) + .ok() + .and_then(|pos| pos.validate().ok()) + } + + /// Returns the text record of the position. + /// + /// This is a shortcut for: + /// ``` + /// # |position: eschac::position::Position| { + /// position.as_setup().to_text_record() + /// # }; + #[inline] + pub fn to_text_record(&self) -> String { + self.as_setup().to_text_record() } /// Returns all the legal moves on the position. diff --git a/src/setup.rs b/src/setup.rs index 294d218..50d161b 100644 --- a/src/setup.rs +++ b/src/setup.rs @@ -13,13 +13,13 @@ use crate::position::*; /// It must be validated and converted to a [`Position`] using the [`Setup::validate`] method /// before generating moves. /// -/// This type implements [`FromStr`](std::str::FromStr) and [`Display`](std::fmt::Display) to parse -/// and print positions from text records. +/// ## Reading positions from text /// /// Forsyth-Edwards Notation (FEN) is typically used to describe chess positions as text. eschac /// uses a slightly different notation, which simply removes the last two fields of the FEN string /// (i.e. the halfmove clock and the fullmove number) as the [`Position`] type does not keep -/// track of those. +/// track of those. For example, the starting position is recorded as +/// `rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq -`. #[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct Setup { pub(crate) w: Bitboard, @@ -48,9 +48,15 @@ impl Setup { } } + /// Reads a position from a text record. + #[inline] + pub fn from_text_record(record: &str) -> Result { + Self::from_ascii_record(record.as_bytes()) + } + /// Reads a position from an ascii record. - pub fn from_ascii(s: &[u8]) -> Result { - let mut s = s.iter().copied().peekable(); + pub fn from_ascii_record(record: &[u8]) -> Result { + let mut s = record.iter().copied().peekable(); let mut setup = Setup::new(); (|| { let mut accept_empty_square = true; @@ -92,7 +98,7 @@ impl Setup { (file == 8).then_some(())?; Some(()) })() - .ok_or(ParseSetupError::InvalidBoard)?; + .ok_or(ParseRecordError::InvalidBoard)?; (|| { match s.next()? { b'w' => setup.set_turn(Color::White), @@ -101,7 +107,7 @@ impl Setup { } (s.next()? == b' ').then_some(()) })() - .ok_or(ParseSetupError::InvalidTurn)?; + .ok_or(ParseRecordError::InvalidTurn)?; (|| { if s.next_if_eq(&b'-').is_none() { if s.next_if_eq(&b'K').is_some() { @@ -119,7 +125,7 @@ impl Setup { } (s.next()? == b' ').then_some(()) })() - .ok_or(ParseSetupError::InvalidCastlingRights)?; + .ok_or(ParseRecordError::InvalidCastlingRights)?; (|| { match s.next()? { b'-' => (), @@ -130,10 +136,93 @@ impl Setup { } s.next().is_none().then_some(()) })() - .ok_or(ParseSetupError::InvalidEnPassantTargetSquare)?; + .ok_or(ParseRecordError::InvalidEnPassantTargetSquare)?; Ok(setup) } + /// Returns the text record of the position. + pub fn to_text_record(&self) -> String { + let mut record = String::with_capacity(81); + self.write_text_record(&mut record).unwrap(); + record + } + + /// Writes the text record of the position. + pub fn write_text_record(&self, w: &mut W) -> std::fmt::Result + where + W: std::fmt::Write, + { + for rank in Rank::all().into_iter().rev() { + let mut count = 0; + for file in File::all() { + match self.get(Square::from_coords(file, rank)) { + Some(piece) => { + if count > 0 { + w.write_char(char::from_u32('0' as u32 + count).unwrap())?; + } + count = 0; + w.write_char(match piece.color { + Color::White => piece.role.to_char_uppercase(), + Color::Black => piece.role.to_char_lowercase(), + })?; + } + None => { + count += 1; + } + } + } + if count > 0 { + w.write_char(char::from_u32('0' as u32 + count).unwrap())?; + } + if rank != Rank::First { + w.write_char('/')?; + } + } + + w.write_char(' ')?; + + w.write_char(match self.turn { + Color::White => 'w', + Color::Black => 'b', + })?; + + w.write_char(' ')?; + + let mut no_castle_available = true; + if self.castling_rights(Color::White, CastlingSide::Short) { + w.write_char('K')?; + no_castle_available = false; + } + if self.castling_rights(Color::White, CastlingSide::Long) { + w.write_char('Q')?; + no_castle_available = false; + } + if self.castling_rights(Color::Black, CastlingSide::Short) { + w.write_char('k')?; + no_castle_available = false; + } + if self.castling_rights(Color::Black, CastlingSide::Long) { + w.write_char('q')?; + no_castle_available = false; + } + if no_castle_available { + w.write_char('-')?; + } + + w.write_char(' ')?; + + match self.en_passant.try_into_square() { + Some(sq) => { + w.write_str(sq.to_str())?; + } + None => { + w.write_char('-')?; + } + } + + Ok(()) + } + /// Returns the occupancy of a square. #[inline] pub fn get(&self, square: Square) -> Option { @@ -428,117 +517,12 @@ impl Setup { impl std::fmt::Debug for Setup { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { - f.debug_tuple("Setup").field(&self.to_string()).finish() + f.debug_tuple("Setup") + .field(&self.to_text_record()) + .finish() } } -impl std::fmt::Display for Setup { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - use std::fmt::Write; - - for rank in Rank::all().into_iter().rev() { - let mut count = 0; - for file in File::all() { - match self.get(Square::from_coords(file, rank)) { - Some(piece) => { - if count > 0 { - f.write_char(char::from_u32('0' as u32 + count).unwrap())?; - } - count = 0; - f.write_char(match piece.color { - Color::White => piece.role.to_char_uppercase(), - Color::Black => piece.role.to_char_lowercase(), - })?; - } - None => { - count += 1; - } - } - } - if count > 0 { - f.write_char(char::from_u32('0' as u32 + count).unwrap())?; - } - if rank != Rank::First { - f.write_char('/')?; - } - } - - f.write_char(' ')?; - - f.write_char(match self.turn { - Color::White => 'w', - Color::Black => 'b', - })?; - - f.write_char(' ')?; - - let mut no_castle_available = true; - if self.castling_rights(Color::White, CastlingSide::Short) { - f.write_char('K')?; - no_castle_available = false; - } - if self.castling_rights(Color::White, CastlingSide::Long) { - f.write_char('Q')?; - no_castle_available = false; - } - if self.castling_rights(Color::Black, CastlingSide::Short) { - f.write_char('k')?; - no_castle_available = false; - } - if self.castling_rights(Color::Black, CastlingSide::Long) { - f.write_char('q')?; - no_castle_available = false; - } - if no_castle_available { - f.write_char('-')?; - } - - f.write_char(' ')?; - - match self.en_passant.try_into_square() { - Some(sq) => { - f.write_str(sq.to_str())?; - } - None => { - write!(f, "-")?; - } - } - - Ok(()) - } -} - -impl std::str::FromStr for Setup { - type Err = ParseSetupError; - #[inline] - fn from_str(s: &str) -> Result { - Self::from_ascii(s.as_bytes()) - } -} - -/// An error when trying to parse a position record. -/// -/// The variant indicates the field that caused the error. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ParseSetupError { - InvalidBoard, - InvalidTurn, - InvalidCastlingRights, - InvalidEnPassantTargetSquare, -} -impl std::fmt::Display for ParseSetupError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let details = match self { - Self::InvalidBoard => "board", - Self::InvalidTurn => "turn", - Self::InvalidCastlingRights => "castling rights", - Self::InvalidEnPassantTargetSquare => "en passant target square", - }; - write!(f, "invalid text record ({details})") - } -} -impl std::error::Error for ParseSetupError {} - /// An invalid position. /// /// This is an illegal position that can't be represented with the [`Position`] type. @@ -551,7 +535,7 @@ impl std::fmt::Display for IllegalPosition { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { use std::fmt::Write; let setup = &self.setup; - write!(f, "`{setup}` is illegal:")?; + write!(f, "`{}` is illegal:", setup.to_text_record())?; let mut first = true; for reason in self.reasons { if !first { @@ -666,3 +650,26 @@ impl std::fmt::Display for IllegalPositionReason { }) } } + +/// An error when trying to parse a position record. +/// +/// The variant indicates the field that caused the error. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ParseRecordError { + InvalidBoard, + InvalidTurn, + InvalidCastlingRights, + InvalidEnPassantTargetSquare, +} +impl std::fmt::Display for ParseRecordError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let details = match self { + Self::InvalidBoard => "board", + Self::InvalidTurn => "turn", + Self::InvalidCastlingRights => "castling rights", + Self::InvalidEnPassantTargetSquare => "en passant target square", + }; + write!(f, "invalid text record ({details})") + } +} +impl std::error::Error for ParseRecordError {} diff --git a/tests/tests.rs b/tests/tests.rs index e010b81..56b26fe 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -13,13 +13,7 @@ static P6: &'static str = "r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QP fn recursive_check_aux(position: Position, depth: usize) { assert_eq!( position, - position - .as_setup() - .to_string() - .parse::() - .unwrap() - .validate() - .unwrap(), + Position::from_text_record(&position.as_setup().to_text_record()).unwrap(), ); if let Some(passed) = position.pass() { @@ -75,7 +69,7 @@ fn recursive_check_aux(position: Position, depth: usize) { } } fn recursive_check(record: &str) { - recursive_check_aux(record.parse::().unwrap().validate().unwrap(), 4); + recursive_check_aux(Position::from_text_record(record).unwrap(), 4); } #[test] fn recursive_check_1() { @@ -104,34 +98,33 @@ fn recursive_check_6() { #[test] fn setup() { - assert_eq!(Position::new().as_setup().to_string(), P1); - assert_eq!(Setup::new().to_string(), "8/8/8/8/8/8/8/8 w - -"); + assert_eq!(Position::new().as_setup().to_text_record(), P1); + assert_eq!(Setup::new().to_text_record(), "8/8/8/8/8/8/8/8 w - -"); assert_eq!( - "8/8/8/8/1Pp5/8/R1k5/K7 w - b3" - .parse::() + Setup::from_text_record("8/8/8/8/1Pp5/8/R1k5/K7 w - b3") .unwrap() - .to_string(), + .to_text_record(), "8/8/8/8/1Pp5/8/R1k5/K7 w - b3", ); for (record, err) in [ - ("", ParseSetupError::InvalidBoard), - (" w - -", ParseSetupError::InvalidBoard), - ("8/8/8/8/8/8/8 w - -", ParseSetupError::InvalidBoard), - ("1/1/1/1/1/1/1/1 w - -", ParseSetupError::InvalidBoard), + ("", ParseRecordError::InvalidBoard), + (" w - -", ParseRecordError::InvalidBoard), + ("8/8/8/8/8/8/8 w - -", ParseRecordError::InvalidBoard), + ("1/1/1/1/1/1/1/1 w - -", ParseRecordError::InvalidBoard), ( "44/44/44/44/44/44/44/44 w - -", - ParseSetupError::InvalidBoard, + ParseRecordError::InvalidBoard, ), - ("8/8/8/8/8/8/8/8/8 w - -", ParseSetupError::InvalidBoard), - ("p8/8/8/8/8/8/8/8 w - -", ParseSetupError::InvalidBoard), - ("8/8/8/8/8/8/8/8 - - - ", ParseSetupError::InvalidTurn), + ("8/8/8/8/8/8/8/8/8 w - -", ParseRecordError::InvalidBoard), + ("p8/8/8/8/8/8/8/8 w - -", ParseRecordError::InvalidBoard), + ("8/8/8/8/8/8/8/8 - - - ", ParseRecordError::InvalidTurn), ( "8/8/8/8/8/8/8/8 w QQQQ -", - ParseSetupError::InvalidCastlingRights, + ParseRecordError::InvalidCastlingRights, ), ] { - assert_eq!(record.parse::(), Err(err), "{record}"); + assert_eq!(Setup::from_text_record(record), Err(err), "{record}"); } for (record, reason) in [ ( @@ -192,12 +185,12 @@ fn setup() { ), ] { assert!( - record.parse::().map(|record| record.to_string()) == Ok(record.to_string()), + Setup::from_text_record(record).map(|setup| setup.to_text_record()) + == Ok(record.to_string()), "{record}", ); assert!( - record - .parse::() + Setup::from_text_record(record) .unwrap() .validate() .is_err_and(|e| e.reasons().contains(reason)), @@ -212,28 +205,24 @@ fn setup() { "3kr3/8/8/8/4P3/8/8/4K3 b - e3", "8/8/8/3k4/3P4/8/8/3RK3 b - d3", ] { - assert!(record.parse::().is_ok(), "{record}"); + assert!(Setup::from_text_record(record).is_ok(), "{record}"); } } #[test] fn mirror() { assert_eq!(Position::new().pass(), Some(Position::new().mirror())); - let position = "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b Kq e3" - .parse::() - .unwrap() - .validate() - .unwrap(); - let mirror = "rnbqkbnr/pppp1ppp/8/4p3/8/8/PPPPPPPP/RNBQKBNR w Qk e6" - .parse::() - .unwrap() - .validate() - .unwrap(); + let position = + Position::from_text_record("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b Kq e3") + .unwrap(); + let mirror = + Position::from_text_record("rnbqkbnr/pppp1ppp/8/4p3/8/8/PPPPPPPP/RNBQKBNR w Qk e6") + .unwrap(); assert_eq!(mirror, position.mirror()); } fn perft_aux(record: &str, tests: &[u128]) { - let position = record.parse::().unwrap().validate().unwrap(); + let position = Position::from_text_record(record).unwrap(); for (depth, value) in tests.iter().copied().enumerate() { assert_eq!( position.perft(depth), @@ -272,11 +261,7 @@ fn perft_6() { #[test] fn san() { - let position = "8/2KN1p2/5p2/3N1B1k/5PNp/7P/7P/8 w - -" - .parse::() - .unwrap() - .validate() - .unwrap(); + let position = Position::from_text_record("8/2KN1p2/5p2/3N1B1k/5PNp/7P/7P/8 w - -").unwrap(); let san1 = "N7xf6#".parse::().unwrap(); let m1 = san1.to_move(&position).unwrap(); let san2 = "N5xf6#".parse::().unwrap();