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 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 let check_str = CheckState::new(&board.play_move(self)).to_san(board);
24
25 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 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 let target_str = self.tgt().to_string();
43
44 let capture_str = match board.get_at(self.get_capture_sq()) {
46 Some(_) => "x",
47 None => "",
48 };
49
50 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 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}