simbelmyne_chess/
san.rs

1use crate::board::Board;
2use crate::constants::FILES;
3use crate::constants::RANKS;
4use crate::movegen::castling::CastleType;
5use crate::movegen::legal_moves::All;
6use crate::movegen::moves::Move;
7use crate::piece::PieceType;
8use std::fmt::Write;
9
10pub trait ToSan {
11  fn to_san(self, board: &Board) -> String;
12}
13
14impl ToSan for Move {
15  /// Render a move in Short Algebraic Notation
16  fn to_san(self, board: &Board) -> String {
17    use PieceType::*;
18    let us = board.current;
19    let piece_type = board.get_at(self.src()).unwrap().piece_type();
20    let blockers = board.all_occupied();
21
22    // Check modifier
23    let check_str = CheckState::new(&board.play_move(self)).to_san(board);
24
25    // If the move is a castling move, we simply return the appropriate
26    // string.
27    if self.is_castle() {
28      let castle_type = CastleType::from_move(self).unwrap();
29      let castle_str = castle_type.to_san(board);
30
31      return format!("{castle_str}{check_str}");
32    }
33
34    // Piece name
35    let piece_str = board
36      .get_at(self.src())
37      .expect("Not a legal move: {self}")
38      .piece_type()
39      .to_san(board);
40
41    // Target square
42    let target_str = self.tgt().to_string();
43
44    // Capture marker
45    let capture_str = match board.get_at(self.get_capture_sq()) {
46      Some(_) => "x",
47      None => "",
48    };
49
50    // Disambiguation
51
52    let sources = match piece_type {
53      Pawn => self.tgt().pawn_squares(!us, blockers),
54      Knight => self.tgt().knight_squares(),
55      Bishop => self.tgt().bishop_squares(blockers),
56      Rook => self.tgt().rook_squares(blockers),
57      Queen => self.tgt().queen_squares(blockers),
58      King => self.tgt().king_squares(),
59    };
60
61    let piece_bb = board.get_bb(piece_type, us);
62    let ambiguous = (sources & piece_bb).count() > 1;
63
64    let disambiguation_str = if piece_type == Pawn && self.is_capture() {
65      let sq_str = self.src().to_string();
66      sq_str[..1].to_string()
67    } else if ambiguous {
68      let sq_str = self.src().to_string();
69      let file = FILES[self.src().file()];
70      let rank = RANKS[self.src().rank()];
71      let ambiguous_file = (file & piece_bb).count() > 1;
72      let ambiguous_rank = (rank & piece_bb).count() > 1;
73
74      if ambiguous_file && ambiguous_rank {
75        sq_str
76      } else if ambiguous_file {
77        sq_str[1..].to_string()
78      } else {
79        sq_str[..1].to_string()
80      }
81    } else {
82      "".to_string()
83    };
84
85    // Promotion flag
86    let promo_str = if let Some(promo) = self.get_promo_type() {
87      format!("={}", promo.to_san(board))
88    } else {
89      format!("")
90    };
91
92    format!("{piece_str}{disambiguation_str}{capture_str}{target_str}{promo_str}{check_str}")
93  }
94}
95
96impl ToSan for CastleType {
97  fn to_san(self, _board: &Board) -> String {
98    match self {
99      Self::WK | Self::BK => "O-O",
100      Self::WQ | Self::BQ => "O-O-O",
101    }
102    .to_string()
103  }
104}
105
106impl ToSan for PieceType {
107  fn to_san(self, _board: &Board) -> String {
108    match self {
109      Self::Pawn => "",
110      Self::Knight => "N",
111      Self::Bishop => "B",
112      Self::Rook => "R",
113      Self::Queen => "Q",
114      Self::King => "K",
115    }
116    .to_string()
117  }
118}
119
120impl PieceType {
121  pub fn from_san(s: &str) -> Self {
122    match s {
123      "" => Self::Pawn,
124      "N" => Self::Knight,
125      "B" => Self::Bishop,
126      "R" => Self::Rook,
127      "Q" => Self::Queen,
128      "K" => Self::King,
129      _ => panic!("Not valid SAN piece type: {s}"),
130    }
131  }
132}
133
134#[derive(Copy, Clone)]
135enum CheckState {
136  Check,
137  Checkmate,
138  None,
139}
140
141impl CheckState {
142  pub fn new(board: &Board) -> Self {
143    if !board.in_check() {
144      return Self::None;
145    } else if board.legal_moves::<All>().len() == 0 {
146      return Self::Checkmate;
147    } else {
148      return Self::Check;
149    }
150  }
151}
152impl ToSan for CheckState {
153  fn to_san(self, _board: &Board) -> String {
154    match self {
155      Self::Check => "+",
156      Self::Checkmate => "#",
157      Self::None => "",
158    }
159    .to_string()
160  }
161}
162
163#[cfg(test)]
164mod tests {
165  use super::*;
166  use crate::movegen::moves::BareMove;
167  use colored::Colorize;
168  use std::str::FromStr;
169
170  const SAN_SUITE: [&str; 9] = [
171        "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1; e2e4; e4",
172        "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1; e2a6; Bxa6",
173        "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R b KQkq - 0 1; f6d5; Nfxd5",
174        "1k6/8/8/8/8/5Q1Q/8/K6Q w - - 0 1; h3f1; Qh3f1",
175        "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1; e1c1; O-O-O",
176        "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1; e1g1; O-O",
177        "r3k2r/p1ppqpb1/b3pnp1/1N1PN3/1pn1P3/5Q1p/PPPBBPPP/R3K2R w KQkq - 2 2; b5d6; Nd6+",
178        "r3k2r/p1ppqpb1/b3pnp1/1N1PN3/1pn1P3/5Q1p/PPPBBPPP/R3K2R w KQkq - 2 2; b5c7; Nxc7+",
179        "1k6/4Q3/8/8/8/8/8/K6R w - - 0 1; h1h8; Rh8#"
180    ];
181
182  #[test]
183  fn test_san() {
184    for pos in SAN_SUITE {
185      let mut parts = pos.split(";");
186      let fen = parts.next().unwrap().trim();
187      let uci = parts.next().unwrap().trim();
188      let expected = parts.next().unwrap().trim();
189      let board: Board = fen.parse().unwrap();
190      let bare_move = BareMove::from_str(uci).expect("Invalid move: {uci}");
191      let mv = Move::from_bare(bare_move, &board).expect("Invalid move: {uci}");
192      let san = mv.to_san(&board);
193
194      if san != expected {
195        panic!("Expected {}, found {}", expected.blue(), san.red());
196      }
197    }
198  }
199}
200
201impl<T: IntoIterator<Item = Move>> ToSan for T {
202  fn to_san(self, board: &Board) -> String {
203    let mut board = board.clone();
204    let mut san = String::new();
205
206    for mv in self {
207      let _ = write!(san, "{} ", mv.to_san(&board));
208      board = board.play_move(mv);
209    }
210
211    san.trim_end().to_string()
212  }
213}