1use regex::Regex;
6
7macro_rules! pos_col {
8 ($cap:expr, $col:expr) => {
9 Some($cap[$col].chars().next().unwrap() as usize - 0x61)
10 };
11}
12
13macro_rules! pos_row {
14 ($cap:expr, $row:expr) => {
15 Some(7 - ($cap[$row].parse::<usize>().unwrap() - 1))
16 };
17}
18
19macro_rules! pos {
20 ($cap:expr, $col:expr, $row:expr) => {
21 Position::new(pos_col!($cap, $col), pos_row!($cap, $row))
22 };
23}
24
25macro_rules! check_type {
26 ($cap:expr, $i:expr) => {{
27 let val = $cap.get($i).map_or("", |v| v.as_str());
28 if val == "+" {
29 Some(CheckType::Check);
30 } else if val == "#" {
31 Some(CheckType::Mate);
32 }
33 None
34 }};
35}
36
37macro_rules! annotation {
38 ($cap:expr, $i:expr) => {
39 Annotation::from_str($cap.get($i).map_or("", |v| v.as_str())).ok()
40 };
41}
42
43macro_rules! promotion {
44 ($cap:expr, $i:expr) => {
45 Piece::from_str($cap.get($i).map_or("fail", |v| v.as_str())).ok();
46 };
47}
48
49pub const POS_NONE: Position = Position { x: None, y: None };
53
54#[derive(Debug, Eq, PartialEq)]
55pub enum SanError {
56 IllegalInput(String),
57 RegexExhausted(String),
58}
59
60use std::fmt::{self, Display, Formatter};
61impl Display for SanError {
62 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
63 write!(f, "{:?}", self)
64 }
65}
66
67impl std::error::Error for SanError {}
68
69pub type Result<T> = std::result::Result<T, SanError>;
70
71trait StrEnum {
75 type Output;
76 fn to_str(&self) -> &str;
77 fn from_str(value: &str) -> Result<Self::Output>;
78}
79
80#[derive(Debug, Eq, PartialEq)]
81pub enum Piece {
82 Pawn,
83 Bishop,
84 King,
85 Knight,
86 Queen,
87 Rook,
88}
89
90impl StrEnum for Piece {
91 type Output = Piece;
92
93 fn to_str(&self) -> &str {
94 match self {
95 Piece::Pawn => "",
96 Piece::Bishop => "B",
97 Piece::King => "K",
98 Piece::Knight => "N",
99 Piece::Queen => "Q",
100 Piece::Rook => "R",
101 }
102 }
103
104 fn from_str(value: &str) -> Result<Piece> {
105 match value {
106 "" => Ok(Piece::Pawn),
107 "B" => Ok(Piece::Bishop),
108 "K" => Ok(Piece::King),
109 "N" => Ok(Piece::Knight),
110 "Q" => Ok(Piece::Queen),
111 "R" => Ok(Piece::Rook),
112 _ => Err(SanError::IllegalInput(format!("invalid piece: {}", value))),
113 }
114 }
115}
116
117#[derive(Debug, Eq, PartialEq)]
118pub enum Annotation {
119 Blunder,
120 Mistake,
121 Interesting,
122 Good,
123 Brilliant,
124}
125
126impl StrEnum for Annotation {
127 type Output = Annotation;
128
129 fn to_str(&self) -> &str {
130 match self {
131 Annotation::Blunder => "??",
132 Annotation::Mistake => "?",
133 Annotation::Interesting => "?!",
134 Annotation::Good => "!",
135 Annotation::Brilliant => "!!",
136 }
137 }
138
139 fn from_str(value: &str) -> Result<Annotation> {
140 match value {
141 "??" => Ok(Annotation::Blunder),
142 "?" => Ok(Annotation::Mistake),
143 "?!" => Ok(Annotation::Interesting),
144 "!" => Ok(Annotation::Good),
145 "!!" => Ok(Annotation::Brilliant),
146 _ => Err(SanError::IllegalInput(format!(
147 "invalid annotation: {}",
148 value
149 ))),
150 }
151 }
152}
153
154#[derive(Debug, Eq, PartialEq)]
155pub enum CastleType {
156 Kingside,
157 Queenside,
158}
159
160impl StrEnum for CastleType {
161 type Output = CastleType;
162
163 fn to_str(&self) -> &str {
164 match self {
165 CastleType::Kingside => "O-O",
166 CastleType::Queenside => "O-O-O",
167 }
168 }
169
170 fn from_str(value: &str) -> Result<CastleType> {
171 match value {
172 "O-O" => Ok(CastleType::Kingside),
173 "O-O-O" => Ok(CastleType::Queenside),
174 _ => Err(SanError::IllegalInput(format!(
175 "invalid castling move: {}",
176 value
177 ))),
178 }
179 }
180}
181
182#[derive(Debug, Eq, PartialEq)]
188pub struct Position {
189 pub x: Option<usize>,
190 pub y: Option<usize>,
191}
192
193impl Position {
194 pub fn new(x: Option<usize>, y: Option<usize>) -> Position {
195 Position { x, y }
196 }
197
198 pub fn of(x: usize, y: usize) -> Position {
199 Position {
200 x: Some(x),
201 y: Some(y),
202 }
203 }
204}
205
206impl ToString for Position {
207 fn to_string(&self) -> String {
208 let mut res = String::new();
209 if let Some(x) = self.x {
210 res.push(char::from(b'a' + (x as u8)));
211 }
212 if let Some(y) = self.y {
213 res.push(char::from(b'8' - (y as u8)));
214 }
215 res
216 }
217}
218
219#[derive(Debug, Eq, PartialEq)]
220pub enum MoveKind {
221 Normal(Position, Position),
225 Castle(CastleType),
226}
227
228#[derive(Debug, Eq, PartialEq)]
229pub enum CheckType {
230 Check,
231 Mate,
232}
233
234#[derive(Debug)]
241pub struct Move {
242 pub move_kind: MoveKind,
243 pub piece: Piece,
244 pub promotion: Option<Piece>,
245 pub annotation: Option<Annotation>,
246 pub check_type: Option<CheckType>,
247 pub is_capture: bool,
248}
249
250impl Move {
251 pub fn new(piece: Piece, move_kind: MoveKind) -> Move {
252 Move {
253 move_kind,
254 piece,
255 promotion: None,
256 annotation: None,
257 check_type: None,
258 is_capture: false,
259 }
260 }
261
262 pub fn compile(&self) -> String {
266 let mut res = String::new();
267
268 match &self.move_kind {
269 MoveKind::Castle(t) => res.push_str(t.to_str()),
270 MoveKind::Normal(src, dst) => {
271 res.push_str(self.piece.to_str());
272 res.push_str(&src.to_string());
273 if self.is_capture {
274 res.push('x');
275 }
276 res.push_str(&dst.to_string());
277 }
278 }
279
280 if let Some(piece) = &self.promotion {
281 res.push('=');
282 res.push_str(piece.to_str());
283 }
284
285 if let Some(ct) = &self.check_type {
286 match ct {
287 CheckType::Check => res.push('+'),
288 CheckType::Mate => res.push('#'),
289 }
290 }
291
292 if let Some(ann) = &self.annotation {
293 res.push_str(ann.to_str());
294 }
295
296 return res;
297 }
298
299 pub fn parse(value: &str) -> Result<Move> {
303 let re = Regex::new(r"^(O-O|O-O-O)(\+|\#)?(\?\?|\?|\?!|!|!!)?$").unwrap();
305 if re.is_match(value) {
306 let cap = re.captures(value).unwrap();
307 let mut mov = Move::new(
308 Piece::King,
309 MoveKind::Castle(CastleType::from_str(&cap[1])?),
310 );
311 mov.check_type = check_type!(cap, 2);
312 mov.annotation = annotation!(cap, 3);
313 return Ok(mov);
314 }
315
316 let re = Regex::new(r"^([a-h])([1-8])(\+|\#)?(\?\?|\?|\?!|!|!!)?$").unwrap();
318 if re.is_match(value) {
319 let cap = re.captures(value).unwrap();
320 let mut mov = Move::new(Piece::Pawn, MoveKind::Normal(POS_NONE, pos!(cap, 1, 2)));
321 mov.check_type = check_type!(cap, 3);
322 mov.annotation = annotation!(cap, 4);
323 return Ok(mov);
324 }
325
326 let re = Regex::new(r"^([a-h])([1-8])([a-h])([1-8])(\+|\#)?(\?\?|\?|\?!|!|!!)?$").unwrap();
328 if re.is_match(value) {
329 let cap = re.captures(value).unwrap();
330 let mut mov = Move::new(
331 Piece::Pawn,
332 MoveKind::Normal(pos!(cap, 1, 2), pos!(cap, 3, 4)),
333 );
334 mov.check_type = check_type!(cap, 5);
335 mov.annotation = annotation!(cap, 6);
336 return Ok(mov);
337 }
338
339 let re = Regex::new(r"^([KQBNR])([a-h])([1-8])(\+|\#)?(\?\?|\?|\?!|!|!!)?$").unwrap();
341 if re.is_match(value) {
342 let cap = re.captures(value).unwrap();
343 let mut mov = Move::new(
344 Piece::from_str(&cap[1])?,
345 MoveKind::Normal(POS_NONE, pos!(cap, 2, 3)),
346 );
347 mov.check_type = check_type!(cap, 4);
348 mov.annotation = annotation!(cap, 5);
349 return Ok(mov);
350 }
351
352 let re =
354 Regex::new(r"^([KQBNR])([a-h])([a-h])([1-8])(\+|\#)?(\?\?|\?|\?!|!|!!)?$").unwrap();
355 if re.is_match(value) {
356 let cap = re.captures(value).unwrap();
357 let mut mov = Move::new(
358 Piece::from_str(&cap[1])?,
359 MoveKind::Normal(Position::new(pos_col!(cap, 2), None), pos!(cap, 3, 4)),
360 );
361 mov.check_type = check_type!(cap, 5);
362 mov.annotation = annotation!(cap, 6);
363 return Ok(mov);
364 }
365
366 let re =
368 Regex::new(r"^([KQBNR])([0-9])([a-h])([1-8])(\+|\#)?(\?\?|\?|\?!|!|!!)?$").unwrap();
369 if re.is_match(value) {
370 let cap = re.captures(value).unwrap();
371 let mut mov = Move::new(
372 Piece::from_str(&cap[1])?,
373 MoveKind::Normal(Position::new(None, pos_row!(cap, 2)), pos!(cap, 3, 4)),
374 );
375 mov.check_type = check_type!(cap, 5);
376 mov.annotation = annotation!(cap, 6);
377 return Ok(mov);
378 }
379
380 let re = Regex::new(r"^([KQBNR])([a-h])([0-9])([a-h])([1-8])(\+|\#)?(\?\?|\?|\?!|!|!!)?$")
382 .unwrap();
383 if re.is_match(value) {
384 let cap = re.captures(value).unwrap();
385 let mut mov = Move::new(
386 Piece::from_str(&cap[1])?,
387 MoveKind::Normal(pos!(cap, 2, 3), pos!(cap, 4, 5)),
388 );
389 mov.check_type = check_type!(cap, 6);
390 mov.annotation = annotation!(cap, 7);
391 return Ok(mov);
392 }
393
394 let re = Regex::new(r"^([a-h])x([a-h])([1-8])(?:=?([KQBNR]))?(\+|\#)?(\?\?|\?|\?!|!|!!)?$")
396 .unwrap();
397 if re.is_match(value) {
398 let cap = re.captures(value).unwrap();
399 let mut mov = Move::new(
400 Piece::Pawn,
401 MoveKind::Normal(Position::new(pos_col!(cap, 1), None), pos!(cap, 2, 3)),
402 );
403 mov.is_capture = true;
404 mov.promotion = promotion!(cap, 4);
405 mov.check_type = check_type!(cap, 5);
406 mov.annotation = annotation!(cap, 6);
407 return Ok(mov);
408 }
409
410 let re = Regex::new(
412 r"^([a-h])([1-8])x([a-h])([1-8])(?:=?([KQBNR]))?(\+|\#)?(\?\?|\?|\?!|!|!!)?$",
413 )
414 .unwrap();
415 if re.is_match(value) {
416 let cap = re.captures(value).unwrap();
417 let mut mov = Move::new(
418 Piece::Pawn,
419 MoveKind::Normal(pos!(cap, 1, 2), pos!(cap, 3, 4)),
420 );
421 mov.is_capture = true;
422 mov.promotion = promotion!(cap, 5);
423 mov.check_type = check_type!(cap, 6);
424 mov.annotation = annotation!(cap, 7);
425 return Ok(mov);
426 }
427
428 let re = Regex::new(r"^([KQBNR])x([a-h])([1-8])(\+|\#)?(\?\?|\?|\?!|!|!!)?$").unwrap();
430 if re.is_match(value) {
431 let cap = re.captures(value).unwrap();
432 let mut mov = Move::new(
433 Piece::from_str(&cap[1])?,
434 MoveKind::Normal(POS_NONE, pos!(cap, 2, 3)),
435 );
436 mov.is_capture = true;
437 mov.check_type = check_type!(cap, 4);
438 mov.annotation = annotation!(cap, 5);
439 return Ok(mov);
440 }
441
442 let re =
444 Regex::new(r"^([KQBNR])([a-h])x([a-h])([1-8])(\+|\#)?(\?\?|\?|\?!|!|!!)?$").unwrap();
445 if re.is_match(value) {
446 let cap = re.captures(value).unwrap();
447 let mut mov = Move::new(
448 Piece::from_str(&cap[1])?,
449 MoveKind::Normal(Position::new(pos_col!(cap, 2), None), pos!(cap, 3, 4)),
450 );
451 mov.is_capture = true;
452 mov.check_type = check_type!(cap, 5);
453 mov.annotation = annotation!(cap, 6);
454 return Ok(mov);
455 }
456
457 let re =
459 Regex::new(r"^([KQBNR])([0-9])x([a-h])([1-8])(\+|\#)?(\?\?|\?|\?!|!|!!)?$").unwrap();
460 if re.is_match(value) {
461 let cap = re.captures(value).unwrap();
462 let mut mov = Move::new(
463 Piece::from_str(&cap[1])?,
464 MoveKind::Normal(Position::new(None, pos_row!(cap, 2)), pos!(cap, 3, 4)),
465 );
466 mov.is_capture = true;
467 mov.check_type = check_type!(cap, 5);
468 mov.annotation = annotation!(cap, 6);
469 return Ok(mov);
470 }
471
472 let re = Regex::new(r"^([KQBNR])([a-h])([0-9])x([a-h])([1-8])(\+|\#)?(\?\?|\?|\?!|!|!!)?$")
474 .unwrap();
475 if re.is_match(value) {
476 let cap = re.captures(value).unwrap();
477 let mut mov = Move::new(
478 Piece::from_str(&cap[1])?,
479 MoveKind::Normal(pos!(cap, 2, 3), pos!(cap, 4, 5)),
480 );
481 mov.is_capture = true;
482 mov.check_type = check_type!(cap, 6);
483 mov.annotation = annotation!(cap, 7);
484 return Ok(mov);
485 }
486
487 let re = Regex::new(r"^([a-h])([1-8])=?([KQBNR])(\+|\#)?(\?\?|\?|\?!|!|!!)?$").unwrap();
489 if re.is_match(value) {
490 let cap = re.captures(value).unwrap();
491 let mut mov = Move::new(Piece::Pawn, MoveKind::Normal(POS_NONE, pos!(cap, 1, 2)));
492 mov.promotion = promotion!(cap, 3);
493 mov.check_type = check_type!(cap, 4);
494 mov.annotation = annotation!(cap, 5);
495 return Ok(mov);
496 }
497
498 Err(SanError::RegexExhausted(format!(
499 "could not parse: {}",
500 value
501 )))
502 }
503}
504
505#[cfg(test)]
506mod tests;