duck_chess/
pgn.rs

1use std::fmt::{self, Write};
2
3use crate::types::{PieceKind, Square};
4
5use byte_parser::{ParseIterator, StrParser};
6
7#[derive(Debug, Clone)]
8pub enum Error {
9	NumberExpected,
10	MismatchNumber,
11	IncorrectMove,
12	ExpectedSquare,
13	UnknownPiece(u8),
14}
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct PgnMove {
18	pub piece: PgnPieceMove,
19	pub duck: Option<Square>,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum PgnPieceMove {
24	Piece {
25		piece: PieceKind,
26		from: Option<Square>,
27		to: Square,
28		capture: bool,
29	},
30	Castle {
31		long: bool,
32	},
33}
34
35pub fn parse_moves(s: &str) -> Result<Vec<PgnMove>, Error> {
36	let mut parser = StrParser::new(s.trim());
37	let mut moves = vec![];
38
39	'main_loop: loop {
40		parser.consume_while_byte_fn(u8::is_ascii_whitespace);
41
42		if parser.peek().is_none() {
43			break;
44		}
45
46		let _ = parse_number(&mut parser)?;
47
48		for _ in 0..2 {
49			parser.consume_while_byte_fn(u8::is_ascii_whitespace);
50
51			if let Some(mov) = parse_move(&mut parser)? {
52				moves.push(mov);
53			} else {
54				break 'main_loop;
55			}
56		}
57	}
58
59	Ok(moves)
60}
61
62fn parse_number<'a, I>(iter: &mut I) -> Result<usize, Error>
63where
64	I: ParseIterator<'a>,
65{
66	let mut iter = iter.record();
67
68	// consume digits
69	iter.while_byte_fn(u8::is_ascii_digit)
70		.consume_at_least(1)
71		.map_err(|_| Error::NumberExpected)?;
72
73	let digits = iter.to_str();
74
75	iter.expect_byte(b'.').map_err(|_| Error::NumberExpected)?;
76
77	digits.parse().map_err(|_| Error::NumberExpected)
78}
79
80/// possible
81/// O-O-O O-O hg1 Nf1 Bf1 Rhg1 R5a3 fxg3
82fn parse_move<'a, I>(iter: &mut I) -> Result<Option<PgnMove>, Error>
83where
84	I: ParseIterator<'a>,
85{
86	let move_str = iter
87		.record()
88		.consume_while_byte_fn(|b| b.is_ascii_alphanumeric() || *b == b'-')
89		.to_str();
90
91	if matches!(move_str, "0-0" | "1-0" | "0-1") {
92		return Ok(None);
93	}
94
95	let bytes = move_str.as_bytes();
96	if bytes.len() < 4 {
97		return Err(Error::IncorrectMove);
98	}
99	let (mut bytes, duck_bytes) = bytes.split_at(bytes.len() - 2);
100	let duck = parse_square(duck_bytes[0], duck_bytes[1])?;
101
102	if bytes == b"O-O" {
103		return Ok(Some(PgnMove {
104			piece: PgnPieceMove::Castle { long: false },
105			duck: Some(duck),
106		}));
107	} else if bytes == b"O-O-O" {
108		return Ok(Some(PgnMove {
109			piece: PgnPieceMove::Castle { long: true },
110			duck: Some(duck),
111		}));
112	}
113
114	let mut piece = PieceKind::Pawn;
115	if bytes[0].is_ascii_uppercase() {
116		piece = match bytes[0] {
117			b'R' => PieceKind::Rook,
118			b'N' => PieceKind::Knight,
119			b'B' => PieceKind::Bishop,
120			b'K' => PieceKind::King,
121			b'Q' => PieceKind::Queen,
122			l => return Err(Error::UnknownPiece(l)),
123		};
124
125		bytes = &bytes[1..];
126	}
127
128	let (mut bytes, square_bytes) = bytes.split_at(bytes.len() - 2);
129	let to = parse_square(square_bytes[0], square_bytes[1])?;
130
131	let capture = bytes.ends_with(&[b'x']);
132	if capture {
133		bytes = &bytes[..bytes.len() - 1];
134	}
135
136	let mut from = None;
137	if bytes.len() == 1 {
138		let byte = bytes[0];
139		if byte.is_ascii_digit() {
140			from = Some(parse_square(square_bytes[0], byte)?);
141		} else if byte.is_ascii_alphabetic() && byte.is_ascii_lowercase() {
142			from = Some(parse_square(byte, square_bytes[1])?);
143		} else {
144			return Err(Error::IncorrectMove);
145		}
146	} else if !bytes.is_empty() {
147		return Err(Error::IncorrectMove);
148	}
149
150	Ok(Some(PgnMove {
151		piece: PgnPieceMove::Piece {
152			piece,
153			from,
154			to,
155			capture,
156		},
157		duck: Some(duck),
158	}))
159}
160
161fn parse_square(letter: u8, number: u8) -> Result<Square, Error> {
162	assert_ne!(number, b'0');
163
164	let number = number - b'1';
165
166	let letter_number = letter - b'a';
167	if letter_number >= 8 || number >= 8 {
168		return Err(Error::ExpectedSquare);
169	}
170
171	// reverse number since the chess coordinates system has zero at the bottom
172	let number = 7 - number;
173
174	Ok(Square::from_xy(letter_number, number))
175}
176
177pub struct MovesFormatter<'a>(pub &'a [PgnMove]);
178
179impl fmt::Display for MovesFormatter<'_> {
180	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
181		for (i, moves) in self.0.chunks(2).enumerate() {
182			if i > 0 {
183				write!(f, " ")?;
184			}
185
186			write!(f, "{}.", i + 1)?;
187
188			for mov in moves {
189				write!(f, " {}", mov)?;
190			}
191		}
192
193		Ok(())
194	}
195}
196
197impl fmt::Display for PgnPieceMove {
198	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
199		match self {
200			Self::Piece {
201				piece,
202				from,
203				to,
204				capture,
205			} => {
206				// piece
207				match piece {
208					PieceKind::Rook => f.write_char('R')?,
209					PieceKind::Knight => f.write_char('N')?,
210					PieceKind::Bishop => f.write_char('B')?,
211					PieceKind::Queen => f.write_char('Q')?,
212					PieceKind::King => f.write_char('K')?,
213					_ => {}
214				}
215
216				if let Some(from) = from {
217					if from.x() != to.x() {
218						f.write_char(from.x_letter() as char)?;
219					}
220
221					if from.y() != to.y() {
222						write!(f, "{}", from.y() + 1)?;
223					}
224				}
225
226				if *capture {
227					f.write_char('x')?;
228				}
229
230				write!(f, "{}", to)
231			}
232			Self::Castle { long } => {
233				if *long {
234					f.write_str("O-O-O")
235				} else {
236					f.write_str("O-O")
237				}
238			}
239		}
240	}
241}
242
243impl fmt::Display for PgnMove {
244	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
245		self.piece.fmt(f)?;
246		if let Some(duck) = self.duck {
247			duck.fmt(f)?;
248		}
249
250		Ok(())
251	}
252}
253
254#[cfg(test)]
255mod tests {
256	use super::*;
257
258	/// O-O-O O-O hg1 Nf1 Bf1 Rhg1 R5a3 fxg3
259	#[test]
260	fn test_parsing() {
261		let moves = parse_moves(
262			"
263			1. e2e4 d7d6
264			2. Nb1c3 g7g6
265			3. Ng1f3 Bf8g7
266			4. Bf1e2 c7c6
267		",
268		)
269		.unwrap();
270
271		let correct_moves = vec![
272			PgnMove {
273				piece: PgnPieceMove::Piece {
274					piece: PieceKind::Pawn,
275					from: None,
276					to: Square::E2,
277					capture: false,
278				},
279				duck: Some(Square::E4),
280			},
281			PgnMove {
282				piece: PgnPieceMove::Piece {
283					piece: PieceKind::Pawn,
284					from: None,
285					to: Square::D7,
286					capture: false,
287				},
288				duck: Some(Square::D6),
289			},
290			PgnMove {
291				piece: PgnPieceMove::Piece {
292					piece: PieceKind::Knight,
293					from: None,
294					to: Square::B1,
295					capture: false,
296				},
297				duck: Some(Square::C3),
298			},
299		];
300
301		assert_eq!(&moves[..correct_moves.len()], &correct_moves);
302	}
303}