1use std::fmt;
2use std::str::FromStr;
3
4use crate::errors::PGNParseError;
5use crate::{board, movegen::*};
6use crate::{hash_to_string, log_and_return_error};
7
8#[derive(Debug, PartialEq, Eq, Clone)]
9pub struct Notation {
10 piece: Option<char>,
11 dis_file: Option<char>, dis_rank: Option<char>, capture: bool,
14 to_file: char,
15 to_rank: char,
16 promotion: Option<char>,
17 check: bool,
18 checkmate: bool,
19 castle_str: Option<String>,
20}
21
22impl fmt::Display for Notation {
23 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
24 let mut notation = String::new();
25
26 if let Some(cs) = &self.castle_str {
28 let mut castle_str = cs.clone();
29 if self.checkmate {
30 castle_str.push('#');
31 } else if self.check {
32 castle_str.push('+');
33 }
34 return write!(f, "{}", castle_str);
35 }
36
37 if let Some(piece) = self.piece {
38 notation.push(piece);
39 }
40 if let Some(dis_file) = self.dis_file {
41 notation.push(dis_file);
42 }
43 if let Some(dis_rank) = self.dis_rank {
44 notation.push(dis_rank);
45 }
46 if self.capture {
47 notation.push('x');
48 }
49 notation.push(self.to_file);
50 notation.push(self.to_rank);
51 if let Some(promotion) = self.promotion {
52 notation.push('=');
53 notation.push(promotion);
54 }
55 if self.checkmate {
56 notation.push('#');
57 } else if self.check {
58 notation.push('+');
59 }
60 write!(f, "{}", notation)
61 }
62}
63
64impl FromStr for Notation {
65 type Err = PGNParseError;
66
67 fn from_str(s: &str) -> Result<Self, Self::Err> {
68 Self::validate_ascii(s)?;
70
71 Self::validate_length(s)?;
73
74 let mut notation = Self::new();
76
77 if notation.parse_castling_string(s) {
79 return Ok(notation);
80 }
81
82 notation.parse_notation_string(s)?;
84
85 Ok(notation)
86 }
87}
88
89impl Notation {
91 fn new() -> Notation {
93 Notation {
94 piece: None,
95 dis_file: None,
96 dis_rank: None,
97 capture: false,
98 to_file: ' ',
99 to_rank: ' ',
100 promotion: None,
101 check: false,
102 checkmate: false,
103 castle_str: None,
104 }
105 }
106
107 pub fn from_mv_with_context(
109 bs_context: &board::BoardState,
110 mv: &Move,
111 ) -> Result<Notation, PGNParseError> {
112 let legal_moves = extract_legal_moves(bs_context)?;
113
114 let mut notation = Self::new();
116
117 if legal_moves.contains(mv) {
120 let test_bs = bs_context.next_state(mv).unwrap(); match test_bs.get_gamestate() {
122 board::GameState::Check => notation.check = true, board::GameState::Checkmate => notation.checkmate = true, _ => {}
125 }
126 } else {
127 let err = PGNParseError::NotationParseError(format!("Move not legal: {:?}", mv));
128 log_and_return_error!(err);
129 }
130
131 if let MoveType::Castle(cm) = mv.move_type {
133 notation.castle_str = Some(match cm.get_castle_side() {
134 CastleSide::Short => "O-O".to_string(),
136 CastleSide::Long => "O-O-O".to_string(),
137 });
138 return Ok(notation); }
140
141 notation.piece = ptype_to_piece_char(&mv.piece.ptype);
143
144 notation.to_file = index_to_file_notation(mv.to);
146 notation.to_rank = index_to_rank_notation_unchecked(mv.to);
147
148 notation.capture = mv.move_type.is_capture();
150
151 notation.promotion = mv_type_to_promotion_char(&mv.move_type);
153
154 if matches!(mv.piece.ptype, PieceType::Pawn) && notation.capture {
157 notation.dis_file = Some(index_to_file_notation(mv.from));
159 } else {
160 let same_piece_moves: Vec<&Move> = legal_moves
162 .iter()
163 .filter(|m| m.piece == mv.piece && m.to == mv.to && m.from != mv.from)
164 .collect();
165 if !same_piece_moves.is_empty() {
167 let mv_from_file = index_to_file_notation(mv.from);
169 let mv_from_rank = index_to_rank_notation_unchecked(mv.from);
170 let mut same_file = false;
172 let mut same_rank = false;
173 for other_mv in same_piece_moves {
175 let other_mv_from_file = index_to_file_notation(other_mv.from);
176 let other_mv_from_rank = index_to_rank_notation_unchecked(other_mv.from);
177 if other_mv_from_file == mv_from_file {
178 same_file = true;
179 }
180 if other_mv_from_rank == mv_from_rank {
181 same_rank = true;
182 }
183 }
184 if !same_file {
186 notation.dis_file = Some(mv_from_file);
187 } else if !same_rank {
188 notation.dis_rank = Some(mv_from_rank);
189 } else {
190 notation.dis_file = Some(mv_from_file);
191 notation.dis_rank = Some(mv_from_rank);
192 }
193 }
194 }
195
196 Ok(notation)
197 }
198
199 fn parse_castling_string(&mut self, notation_str: &str) -> bool {
200 let possible_castle_str = notation_str.trim_end_matches(['+', '#']);
201 if possible_castle_str == "O-O" || possible_castle_str == "O-O-O" {
202 self.castle_str = Some(possible_castle_str.to_string());
203 self.check = notation_str.ends_with('+');
204 self.checkmate = notation_str.ends_with('#');
205 return true;
206 }
207 false
208 }
209
210 fn parse_notation_string(&mut self, notation_str: &str) -> Result<(), PGNParseError> {
211 let mut chars = notation_str.char_indices();
212 let mut rank_file_chars: Vec<char> = Vec::new();
213 let mut piece_char: Option<char> = None;
214 let mut capture = false;
215 let mut promotion: Option<char> = None;
216 let mut check = false;
217 let mut checkmate = false;
218
219 while let Some((i, c)) = chars.next() {
220 match c {
221 c if c.is_ascii_uppercase() => {
222 Self::handle_piece_char(c, &mut piece_char, notation_str, i)?;
223 }
224 'x' => {
225 Self::handle_capture_char(&mut capture, notation_str, i)?;
226 }
227 c if c.is_ascii_lowercase() || c.is_ascii_digit() => {
228 rank_file_chars.push(c);
229 }
230 '=' => {
231 promotion = Self::handle_promotion_char(&mut chars, notation_str, i)?;
232 }
233 '+' => {
234 Self::handle_check_char(&mut check, notation_str, i)?;
235 }
236 '#' => {
237 Self::handle_checkmate_char(&mut checkmate, notation_str, i)?;
238 }
239 _ => {
240 let err = PGNParseError::NotationParseError(format!(
241 "Invalid character in notation (char: '{}' at index: {})",
242 c, i
243 ));
244 log_and_return_error!(err)
245 }
246 }
247 }
248
249 self.set_piece_char(piece_char)?;
251
252 self.set_rank_file_chars(&rank_file_chars)?;
254
255 self.set_promotion_char(promotion)?;
257
258 self.capture = capture;
260 self.check = check;
261 self.checkmate = checkmate;
262
263 Ok(())
264 }
265
266 fn set_piece_char(&mut self, piece_char: Option<char>) -> Result<(), PGNParseError> {
267 if let Some(piece) = piece_char {
268 if is_valid_piece(piece) {
269 self.piece = Some(piece);
270 } else {
271 let err =
272 PGNParseError::NotationParseError(format!("Invalid piece char ({})", piece));
273 log_and_return_error!(err)
274 }
275 }
276 Ok(())
277 }
278
279 fn set_promotion_char(&mut self, promotion: Option<char>) -> Result<(), PGNParseError> {
280 if let Some(promotion) = promotion {
281 if is_valid_promotion(promotion) {
282 self.promotion = Some(promotion);
283 } else {
284 let err = PGNParseError::NotationParseError(format!(
285 "Invalid promotion piece char ({})",
286 promotion
287 ));
288 log_and_return_error!(err)
289 }
290 }
291 Ok(())
292 }
293
294 fn set_rank_file_chars(&mut self, rank_file_chars: &[char]) -> Result<(), PGNParseError> {
295 let to_file: char;
296 let to_rank: char;
297 let mut dis_file = None;
298 let mut dis_rank = None;
299 match rank_file_chars.len() {
300 2 => {
301 to_file = rank_file_chars[0];
302 to_rank = rank_file_chars[1];
303 }
304 3 => {
305 if rank_file_chars[0].is_ascii_lowercase() {
306 dis_file = Some(rank_file_chars[0]); } else {
308 dis_rank = Some(rank_file_chars[0]); }
310 to_file = rank_file_chars[1];
311 to_rank = rank_file_chars[2];
312 }
313 4 => {
314 dis_file = Some(rank_file_chars[0]);
315 dis_rank = Some(rank_file_chars[1]);
316 to_file = rank_file_chars[2];
317 to_rank = rank_file_chars[3];
318 }
319 _ => {
320 let err = PGNParseError::NotationParseError(format!(
321 "Invalid move notation char(s) ({:?})",
322 rank_file_chars
323 ));
324 log_and_return_error!(err)
325 }
326 }
327 if !is_valid_file(to_file)
328 || !is_valid_rank(to_rank)
329 || dis_file.map_or(false, |c| !is_valid_file(c))
330 || dis_rank.map_or(false, |c| !is_valid_rank(c))
331 {
332 let err = PGNParseError::NotationParseError(format!(
333 "Invalid rank or file char(s) in vec: ({:?})",
334 rank_file_chars
335 ));
336 log_and_return_error!(err)
337 } else {
338 self.to_file = to_file;
339 self.to_rank = to_rank;
340 self.dis_file = dis_file;
341 self.dis_rank = dis_rank;
342 }
343 Ok(())
344 }
345
346 fn validate_ascii(notation_str: &str) -> Result<(), PGNParseError> {
347 if !notation_str.is_ascii() {
348 let err = PGNParseError::NotationParseError(format!(
349 "Invalid notation string: ({}) is not valid ascii",
350 notation_str
351 ));
352 log_and_return_error!(err)
353 }
354 Ok(())
355 }
356
357 fn validate_length(notation_str: &str) -> Result<(), PGNParseError> {
358 let str_len = notation_str.len();
359 if !(2..=8).contains(&str_len) {
360 let err =
361 PGNParseError::NotationParseError(format!("Invalid notation length ({})", str_len));
362 log_and_return_error!(err)
363 }
364 Ok(())
365 }
366
367 fn handle_piece_char(
368 c: char,
369 piece_char: &mut Option<char>,
370 notation_str: &str,
371 i: usize,
372 ) -> Result<(), PGNParseError> {
373 if piece_char.is_none() {
374 *piece_char = Some(c);
375 } else {
376 let err = PGNParseError::NotationParseError(format!(
377 "Invalid notation, multiple uppercase piece chars (char: '{}' at index: {})",
378 notation_str, i
379 ));
380 log_and_return_error!(err)
381 }
382 Ok(())
383 }
384
385 fn handle_capture_char(
386 capture: &mut bool,
387 notation_str: &str,
388 i: usize,
389 ) -> Result<(), PGNParseError> {
390 if !*capture {
391 if (notation_str.len() - i) < 3 {
393 let err = PGNParseError::NotationParseError(format!(
394 "Invalid notation, no rank or file after capture char (char: '{}' at index: {})",
395 notation_str, i
396 ));
397 log_and_return_error!(err)
398 } else {
399 *capture = true;
400 }
401 } else {
402 let err = PGNParseError::NotationParseError(format!(
403 "Invalid notation, multiple capture chars (char: '{}' at index: {})",
404 notation_str, i
405 ));
406 log_and_return_error!(err)
407 }
408 Ok(())
409 }
410
411 fn handle_promotion_char(
412 chars: &mut std::str::CharIndices,
413 notation_str: &str,
414 i: usize,
415 ) -> Result<Option<char>, PGNParseError> {
416 let n = chars.next();
417 match n {
418 Some((_, c)) => match c {
419 'Q' | 'R' | 'B' | 'N' => Ok(Some(c)),
420 _ => {
421 let err = PGNParseError::NotationParseError(format!(
422 "Invalid promotion piece (char: '{}' at index: {})",
423 c, i
424 ));
425 log_and_return_error!(err)
426 }
427 },
428 None => {
429 let err = PGNParseError::NotationParseError(format!(
430 "Invalid notation, no promotion piece after '=' (char: '{}' at index: {})",
431 notation_str, i
432 ));
433 log_and_return_error!(err)
434 }
435 }
436 }
437
438 fn handle_check_char(
439 check: &mut bool,
440 notation_str: &str,
441 i: usize,
442 ) -> Result<(), PGNParseError> {
443 if !*check {
444 *check = true;
445 } else {
446 let err = PGNParseError::NotationParseError(format!(
447 "Invalid notation, multiple check chars (char: '{}' at index: {})",
448 notation_str, i
449 ));
450 log_and_return_error!(err)
451 }
452 Ok(())
453 }
454
455 fn handle_checkmate_char(
456 checkmate: &mut bool,
457 notation_str: &str,
458 i: usize,
459 ) -> Result<(), PGNParseError> {
460 if !*checkmate {
461 *checkmate = true;
462 } else {
463 let err = PGNParseError::NotationParseError(format!(
464 "Invalid notation, multiple checkmate chars (char: '{}' at index: {})",
465 notation_str, i
466 ));
467 log_and_return_error!(err)
468 }
469 Ok(())
470 }
471}
472
473impl Notation {
474 pub fn to_move_with_context(
476 &self,
477 bs_context: &board::BoardState,
478 ) -> Result<Move, PGNParseError> {
479 let legal_moves = extract_legal_moves(bs_context)?;
480 let possible_moves = self.filter_possible_moves(legal_moves);
481 match possible_moves.len() {
482 1 => Ok(*possible_moves[0]),
483 len if len > 1 => {
484 let mut dis_file_possible_idxs = None;
485 let mut dis_rank_possible_idxs = None;
486
487 if let Some(dis_file_char) = self.dis_file {
488 dis_file_possible_idxs =
489 Some(file_notation_to_indexes_unchecked(dis_file_char));
490 }
491 if let Some(dis_rank_char) = self.dis_rank {
492 dis_rank_possible_idxs =
493 Some(rank_notation_to_indexes_unchecked(dis_rank_char));
494 }
495
496 let mut possible_dis_moves = Vec::new();
497 for mv in &possible_moves {
498 if let (Some(dis_file_idxs), Some(dis_rank_idxs)) =
499 (dis_file_possible_idxs, dis_rank_possible_idxs)
500 {
501 if dis_file_idxs.contains(&mv.from) && dis_rank_idxs.contains(&mv.from) {
502 possible_dis_moves.push(*mv);
503 }
504 } else if let Some(dis_file_idxs) = dis_file_possible_idxs {
505 if dis_file_idxs.contains(&mv.from) {
506 possible_dis_moves.push(*mv);
507 }
508 } else if let Some(dis_rank_idxs) = dis_rank_possible_idxs {
509 if dis_rank_idxs.contains(&mv.from) {
510 possible_dis_moves.push(*mv);
511 }
512 }
513 }
514
515 if possible_dis_moves.len() == 1 {
516 Ok(*possible_dis_moves[0])
517 } else {
518 let err = PGNParseError::MoveNotFound(format!(
519 "No legal move found for notation ({}) in BoardState (hash: {}) => Could not use notation to disambiguate between multiple possible moves: {:?}",
520 self,
521 hash_to_string(bs_context.board_hash),
522 possible_moves
523 ));
524 log_and_return_error!(err)
525 }
526 }
527 _ => {
528 let err = PGNParseError::MoveNotFound(format!(
529 "No legal move found for notation ({}) in BoardState (hash: {})",
530 self,
531 hash_to_string(bs_context.board_hash)
532 ));
533 log_and_return_error!(err)
534 }
535 }
536 }
537
538 fn get_piece_type(&self) -> Option<PieceType> {
539 match self.piece {
540 Some('N') => Some(PieceType::Knight),
541 Some('B') => Some(PieceType::Bishop),
542 Some('R') => Some(PieceType::Rook),
543 Some('Q') => Some(PieceType::Queen),
544 Some('K') => Some(PieceType::King),
545 Some(_) => {
546 unreachable!("Invalid piece char in get_piece_type function")
547 }
548 None => None,
549 }
550 }
551
552 fn get_promotion_piece_type(&self) -> Option<PieceType> {
553 match self.promotion {
554 Some('Q') => Some(PieceType::Queen),
555 Some('R') => Some(PieceType::Rook),
556 Some('B') => Some(PieceType::Bishop),
557 Some('N') => Some(PieceType::Knight),
558 Some(_) => {
559 unreachable!("Invalid promotion char in get_promotion_piece_type function")
560 }
561 None => None,
562 }
563 }
564
565 fn get_castle_side(&self) -> Option<CastleSide> {
566 self.castle_str
567 .as_ref()
568 .map(|castle_str| match castle_str.as_str() {
569 "O-O" => CastleSide::Short,
570 "O-O-O" => CastleSide::Long,
571 _ => {
572 unreachable!("Invalid castle string in get_castle_side function");
573 }
574 })
575 }
576
577 fn filter_possible_moves<'a>(&self, moves: &'a [Move]) -> Vec<&'a Move> {
578 moves
579 .iter()
580 .filter(|mv| {
581 if let Some(castle_side) = self.get_castle_side() {
582 if let MoveType::Castle(cm) = mv.move_type {
583 return cm.get_castle_side() == castle_side;
584 }
585 }
586
587 if let Some(piece) = self.get_piece_type() {
588 if mv.piece.ptype != piece {
589 return false;
590 }
591 } else {
592 if mv.piece.ptype != PieceType::Pawn {
594 return false;
595 }
596 }
597
598 if self.to_file != index_to_file_notation(mv.to)
599 || self.to_rank != index_to_rank_notation_unchecked(mv.to)
600 {
601 return false;
602 }
603 if self.capture && !mv.move_type.is_capture() {
604 return false;
605 }
606 if let Some(promotion) = self.get_promotion_piece_type() {
607 if let MoveType::Promotion(promotion_ptype, _) = mv.move_type {
608 if promotion_ptype != promotion {
609 return false;
610 }
611 } else {
612 return false;
613 }
614 }
615 true
617 })
618 .collect::<Vec<&Move>>()
619 }
620}
621
622fn extract_legal_moves(bs: &board::BoardState) -> Result<&[Move], PGNParseError> {
624 match bs.get_legal_moves() {
625 Ok(moves) => Ok(moves),
626 Err(e) => {
627 let err = PGNParseError::NotationParseError(format!(
628 "Error getting legal moves in BoardState: {}",
629 e
630 ));
631 log_and_return_error!(err)
632 }
633 }
634}
635
636#[inline]
637fn ptype_to_piece_char(ptype: &PieceType) -> Option<char> {
638 match ptype {
639 PieceType::Pawn => None,
640 PieceType::Knight => Some('N'),
641 PieceType::Bishop => Some('B'),
642 PieceType::Rook => Some('R'),
643 PieceType::Queen => Some('Q'),
644 PieceType::King => Some('K'),
645 }
646}
647
648#[inline]
649fn mv_type_to_promotion_char(mv_type: &MoveType) -> Option<char> {
650 if let MoveType::Promotion(promotion, _) = mv_type {
651 match promotion {
652 PieceType::Queen => Some('Q'),
653 PieceType::Rook => Some('R'),
654 PieceType::Bishop => Some('B'),
655 PieceType::Knight => Some('N'),
656 _ => unreachable!("Invalid MoveType. Not possible from crate::movegen"),
657 }
658 } else {
659 None
660 }
661}
662
663#[inline]
664fn is_valid_file(file: char) -> bool {
665 file.is_ascii_lowercase() && ('a'..='h').contains(&file)
666}
667
668#[inline]
669fn is_valid_rank(rank: char) -> bool {
670 rank.is_ascii_digit() && ('1'..='8').contains(&rank)
671}
672
673#[inline]
674fn is_valid_piece(piece: char) -> bool {
675 let valid_pieces = ['P', 'N', 'B', 'R', 'Q', 'K'];
676 piece.is_ascii_uppercase() && valid_pieces.contains(&piece)
677}
678
679#[inline]
680fn is_valid_promotion(promotion: char) -> bool {
681 let valid_promotions = ['Q', 'R', 'B', 'N'];
682 promotion.is_ascii_uppercase() && valid_promotions.contains(&promotion)
683}
684
685#[inline]
686fn index_to_file_notation(i: usize) -> char {
687 match i % 8 {
688 0 => 'a',
689 1 => 'b',
690 2 => 'c',
691 3 => 'd',
692 4 => 'e',
693 5 => 'f',
694 6 => 'g',
695 7 => 'h',
696 _ => ' ',
697 }
698}
699
700#[inline]
701fn index_to_rank_notation_unchecked(i: usize) -> char {
702 let rank_num = 8 - i / 8;
703 char::from_digit(rank_num.try_into().unwrap(), 10).unwrap()
704}
705
706fn rank_notation_to_indexes_unchecked(r: char) -> [usize; 8] {
707 let rank_num = r.to_digit(10).unwrap() as usize;
708 let rank_starts = [56, 48, 40, 32, 24, 16, 8, 0]; let mut indexes = [0; 8];
710 for (i, j) in indexes.iter_mut().enumerate() {
711 *j = rank_starts[rank_num - 1] + i;
712 }
713 indexes
714}
715
716fn file_notation_to_indexes_unchecked(f: char) -> [usize; 8] {
717 let file_offset = match f {
718 'a' => 0,
719 'b' => 1,
720 'c' => 2,
721 'd' => 3,
722 'e' => 4,
723 'f' => 5,
724 'g' => 6,
725 'h' => 7,
726 _ => unreachable!(),
727 };
728 let mut indexes = [0; 8];
729 for (i, j) in indexes.iter_mut().enumerate() {
730 *j = file_offset + i * 8;
731 }
732 indexes
733}
734
735#[cfg(test)]
736mod test {
737 use super::*;
738
739 #[test]
740 fn test_notation_new() {
741 let notation = Notation::new();
742
743 assert!(notation.piece.is_none());
744 assert!(notation.dis_file.is_none());
745 assert!(notation.dis_rank.is_none());
746 assert!(!notation.capture);
747 assert_eq!(notation.to_file, ' ');
748 assert_eq!(notation.to_rank, ' ');
749 assert!(notation.promotion.is_none());
750 assert!(!notation.check);
751 assert!(!notation.checkmate);
752 }
753
754 #[test]
755 fn test_notation_to_string() {
756 let mut notation = Notation::new();
757 notation.piece = Some('N');
758 notation.to_file = 'f';
759 notation.to_rank = '3';
760 notation.capture = true;
761
762 assert_eq!(notation.to_string(), "Nxf3");
763 }
764
765 #[test]
766 fn test_notation_from_mv_with_context() {
767 let bs = board::BoardState::new_starting();
768 let mv = Move {
769 piece: Piece {
770 ptype: PieceType::Knight,
771 pcolour: PieceColour::White,
772 },
773 from: 62,
774 to: 45,
775 move_type: MoveType::Normal,
776 };
777 let notation = Notation::from_mv_with_context(&bs, &mv);
778 assert!(notation.is_ok());
779 assert_eq!(notation.unwrap().to_string(), "Nf3");
780 }
781
782 #[test]
783 fn test_notation_from_str() -> Result<(), PGNParseError> {
784 let notation_str = "Qf3xf5+";
785 let notation = Notation::from_str(notation_str);
786 match notation {
787 Ok(notation) => {
788 assert_eq!(notation.piece, Some('Q'));
789 assert_eq!(notation.to_file, 'f');
790 assert_eq!(notation.to_rank, '5');
791 assert!(notation.capture);
792 assert!(notation.check);
793 assert!(notation.dis_file.is_some());
794 assert!(notation.dis_rank.is_some());
795 assert_eq!(notation.dis_file.unwrap(), 'f');
796 assert_eq!(notation.dis_rank.unwrap(), '3');
797 Ok(())
798 }
799 Err(e) => Err(e),
800 }
801 }
802
803 #[test]
804 fn test_notation_from_str_castle() -> Result<(), PGNParseError> {
805 let notation_str = "O-O";
806 let notation = Notation::from_str(notation_str)?;
807 assert_eq!(notation.castle_str, Some("O-O".to_string()));
808 assert!(!notation.check);
809 assert!(!notation.checkmate);
810
811 let notation_str = "O-O+";
812 let notation = Notation::from_str(notation_str)?;
813 assert_eq!(notation.castle_str, Some("O-O".to_string()));
814 assert!(notation.check);
815 assert!(!notation.checkmate);
816
817 let notation_str = "O-O-O#";
818 let notation = Notation::from_str(notation_str)?;
819 assert_eq!(notation.castle_str, Some("O-O-O".to_string()));
820 assert!(!notation.check);
821 assert!(notation.checkmate);
822
823 Ok(())
824 }
825
826 #[test]
827 fn test_notation_from_str_promotion() -> Result<(), PGNParseError> {
828 let notation_str = "e8=Q";
829 let notation = Notation::from_str(notation_str)?;
830 assert_eq!(notation.piece, None);
831 assert_eq!(notation.to_file, 'e');
832 assert_eq!(notation.to_rank, '8');
833 assert_eq!(notation.promotion, Some('Q'));
834 assert!(!notation.capture);
835 assert!(!notation.check);
836 assert!(!notation.checkmate);
837
838 let notation_str = "e8=Q+";
839 let notation = Notation::from_str(notation_str)?;
840 assert_eq!(notation.piece, None);
841 assert_eq!(notation.to_file, 'e');
842 assert_eq!(notation.to_rank, '8');
843 assert_eq!(notation.promotion, Some('Q'));
844 assert!(!notation.capture);
845 assert!(notation.check);
846 assert!(!notation.checkmate);
847
848 let notation_str = "e8=Q#";
849 let notation = Notation::from_str(notation_str)?;
850 assert_eq!(notation.piece, None);
851 assert_eq!(notation.to_file, 'e');
852 assert_eq!(notation.to_rank, '8');
853 assert_eq!(notation.promotion, Some('Q'));
854 assert!(!notation.capture);
855 assert!(!notation.check);
856 assert!(notation.checkmate);
857
858 Ok(())
859 }
860
861 #[test]
862 fn test_notation_from_str_invalid() {
863 let notation_str = "Qf9";
864 let notation = Notation::from_str(notation_str);
865 assert!(notation.is_err());
866
867 let notation_str = "Qz3";
868 let notation = Notation::from_str(notation_str);
869 assert!(notation.is_err());
870
871 let notation_str = "Qf3x";
872 let notation = Notation::from_str(notation_str);
873 assert!(notation.is_err());
874
875 let notation_str = "Qf3=";
876 let notation = Notation::from_str(notation_str);
877 assert!(notation.is_err());
878
879 let notation_str = "Qf3++";
880 let notation = Notation::from_str(notation_str);
881 assert!(notation.is_err());
882
883 let notation_str = "Qf3##";
884 let notation = Notation::from_str(notation_str);
885 assert!(notation.is_err());
886 }
887
888 #[test]
889 fn test_notation_to_move_with_context() {
890 let bs = board::BoardState::new_starting();
891 let notation = Notation::from_str("Nf3").unwrap();
892 let mv = notation.to_move_with_context(&bs);
893 assert!(mv.is_ok());
894 let mv = mv.unwrap();
895 assert_eq!(mv.piece.ptype, PieceType::Knight);
896 assert_eq!(mv.from, 62);
897 assert_eq!(mv.to, 45);
898
899 let notation = Notation::from_str("e4").unwrap();
900 let mv = notation.to_move_with_context(&bs);
901 assert!(mv.is_ok());
902 let mv = mv.unwrap();
903 assert_eq!(mv.piece.ptype, PieceType::Pawn);
904 assert_eq!(mv.from, 52);
905 assert_eq!(mv.to, 36);
906 }
907
908 #[test]
909 fn test_index_to_file_notation() {
910 assert_eq!(index_to_file_notation(0), 'a');
911 assert_eq!(index_to_file_notation(7), 'h');
912 assert_eq!(index_to_file_notation(35), 'd');
913 }
914
915 #[test]
916 fn test_index_to_rank_notation() {
917 assert_eq!(index_to_rank_notation_unchecked(0), '8');
918 assert_eq!(index_to_rank_notation_unchecked(7), '8');
919 assert_eq!(index_to_rank_notation_unchecked(35), '4');
920 }
921
922 #[test]
923 fn test_rank_notation_to_indexes_unchecked() {
924 assert_eq!(
925 rank_notation_to_indexes_unchecked('1'),
926 [56, 57, 58, 59, 60, 61, 62, 63]
927 );
928 assert_eq!(
929 rank_notation_to_indexes_unchecked('2'),
930 [48, 49, 50, 51, 52, 53, 54, 55]
931 );
932 assert_eq!(
933 rank_notation_to_indexes_unchecked('3'),
934 [40, 41, 42, 43, 44, 45, 46, 47]
935 );
936 assert_eq!(
937 rank_notation_to_indexes_unchecked('4'),
938 [32, 33, 34, 35, 36, 37, 38, 39]
939 );
940 assert_eq!(
941 rank_notation_to_indexes_unchecked('5'),
942 [24, 25, 26, 27, 28, 29, 30, 31]
943 );
944 assert_eq!(
945 rank_notation_to_indexes_unchecked('6'),
946 [16, 17, 18, 19, 20, 21, 22, 23]
947 );
948 assert_eq!(
949 rank_notation_to_indexes_unchecked('7'),
950 [8, 9, 10, 11, 12, 13, 14, 15]
951 );
952 assert_eq!(
953 rank_notation_to_indexes_unchecked('8'),
954 [0, 1, 2, 3, 4, 5, 6, 7]
955 );
956 }
957
958 #[test]
959 fn test_file_notation_to_indexes_unchecked() {
960 assert_eq!(
961 file_notation_to_indexes_unchecked('a'),
962 [0, 8, 16, 24, 32, 40, 48, 56]
963 );
964 assert_eq!(
965 file_notation_to_indexes_unchecked('b'),
966 [1, 9, 17, 25, 33, 41, 49, 57]
967 );
968 assert_eq!(
969 file_notation_to_indexes_unchecked('c'),
970 [2, 10, 18, 26, 34, 42, 50, 58]
971 );
972 assert_eq!(
973 file_notation_to_indexes_unchecked('d'),
974 [3, 11, 19, 27, 35, 43, 51, 59]
975 );
976 assert_eq!(
977 file_notation_to_indexes_unchecked('e'),
978 [4, 12, 20, 28, 36, 44, 52, 60]
979 );
980 assert_eq!(
981 file_notation_to_indexes_unchecked('f'),
982 [5, 13, 21, 29, 37, 45, 53, 61]
983 );
984 assert_eq!(
985 file_notation_to_indexes_unchecked('g'),
986 [6, 14, 22, 30, 38, 46, 54, 62]
987 );
988 assert_eq!(
989 file_notation_to_indexes_unchecked('h'),
990 [7, 15, 23, 31, 39, 47, 55, 63]
991 );
992 }
993}