chess/chess_move.rs
1use crate::board::Board;
2use crate::error::Error;
3use crate::file::File;
4use crate::movegen::MoveGen;
5use crate::piece::Piece;
6use crate::rank::Rank;
7use crate::square::Square;
8
9use std::cmp::Ordering;
10use std::fmt;
11use std::str::FromStr;
12
13/// Represent a ChessMove in memory
14#[derive(Clone, Copy, Eq, PartialOrd, PartialEq, Default, Debug, Hash)]
15pub struct ChessMove {
16 source: Square,
17 dest: Square,
18 promotion: Option<Piece>,
19}
20
21impl ChessMove {
22 /// Create a new chess move, given a source `Square`, a destination `Square`, and an optional
23 /// promotion `Piece`
24 #[inline]
25 pub fn new(source: Square, dest: Square, promotion: Option<Piece>) -> ChessMove {
26 ChessMove {
27 source: source,
28 dest: dest,
29 promotion: promotion,
30 }
31 }
32
33 /// Get the source square (square the piece is currently on).
34 #[inline]
35 pub fn get_source(&self) -> Square {
36 self.source
37 }
38
39 /// Get the destination square (square the piece is going to).
40 #[inline]
41 pub fn get_dest(&self) -> Square {
42 self.dest
43 }
44
45 /// Get the promotion piece (maybe).
46 #[inline]
47 pub fn get_promotion(&self) -> Option<Piece> {
48 self.promotion
49 }
50 /// Convert a SAN (Standard Algebraic Notation) move into a `ChessMove`
51 ///
52 /// ```
53 /// use chess::{Board, ChessMove, Square};
54 ///
55 /// let board = Board::default();
56 /// assert_eq!(
57 /// ChessMove::from_san(&board, "e4").expect("e4 is valid in the initial position"),
58 /// ChessMove::new(Square::E2, Square::E4, None)
59 /// );
60 /// ```
61 pub fn from_san(board: &Board, move_text: &str) -> Result<ChessMove, Error> {
62 // Castles first...
63 if move_text == "O-O" || move_text == "O-O-O" {
64 let rank = board.side_to_move().to_my_backrank();
65 let source_file = File::E;
66 let dest_file = if move_text == "O-O" { File::G } else { File::C };
67
68 let m = ChessMove::new(
69 Square::make_square(rank, source_file),
70 Square::make_square(rank, dest_file),
71 None,
72 );
73 if MoveGen::new_legal(&board).any(|l| l == m) {
74 return Ok(m);
75 } else {
76 return Err(Error::InvalidSanMove);
77 }
78 }
79
80 // forms of SAN moves
81 // a4 (Pawn moves to a4)
82 // exd4 (Pawn on e file takes on d4)
83 // xd4 (Illegal, source file must be specified)
84 // 1xd4 (Illegal, source file (not rank) must be specified)
85 // Nc3 (Knight (or any piece) on *some square* to c3
86 // Nb1c3 (Knight (or any piece) on b1 to c3
87 // Nbc3 (Knight on b file to c3)
88 // N1c3 (Knight on first rank to c3)
89 // Nb1xc3 (Knight on b1 takes on c3)
90 // Nbxc3 (Knight on b file takes on c3)
91 // N1xc3 (Knight on first rank takes on c3)
92 // Nc3+ (Knight moves to c3 with check)
93 // Nc3# (Knight moves to c3 with checkmate)
94
95 // Because I'm dumb, I'm wondering if a hash table of all possible moves would be stupid.
96 // There are only 186624 possible moves in SAN notation.
97 //
98 // Would this even be faster? Somehow I doubt it because caching, but maybe, I dunno...
99 // This could take the form of a:
100 // struct CheckOrCheckmate {
101 // Neither,
102 // Check,
103 // CheckMate,
104 // }
105 // struct FromSan {
106 // piece: Piece,
107 // source: Vec<Square>, // possible source squares
108 // // OR
109 // source_rank: Option<Rank>,
110 // source_file: Option<File>,
111 // dest: Square,
112 // takes: bool,
113 // check: CheckOrCheckmate
114 // }
115 //
116 // This could be kept internally as well, and never tell the user about such an abomination
117 //
118 // I estimate this table would take around 2 MiB, but I had to approximate some things. It
119 // may be less
120
121 // This can be described with the following format
122 // [Optional Piece Specifier] ("" | "N" | "B" | "R" | "Q" | "K")
123 // [Optional Source Specifier] ( "" | "a-h" | "1-8" | ("a-h" + "1-8"))
124 // [Optional Takes Specifier] ("" | "x")
125 // [Full Destination Square] ("a-h" + "0-8")
126 // [Optional Promotion Specifier] ("" | "N" | "B" | "R" | "Q")
127 // [Optional Check(mate) Specifier] ("" | "+" | "#")
128 // [Optional En Passant Specifier] ("" | " e.p.")
129
130 let error = Error::InvalidSanMove;
131 let mut cur_index: usize = 0;
132 let moving_piece = match move_text
133 .get(cur_index..(cur_index + 1))
134 .ok_or(error.clone())?
135 {
136 "N" => {
137 cur_index += 1;
138 Piece::Knight
139 }
140 "B" => {
141 cur_index += 1;
142 Piece::Bishop
143 }
144 "Q" => {
145 cur_index += 1;
146 Piece::Queen
147 }
148 "R" => {
149 cur_index += 1;
150 Piece::Rook
151 }
152 "K" => {
153 cur_index += 1;
154 Piece::King
155 }
156 _ => Piece::Pawn,
157 };
158
159 let mut source_file = match move_text
160 .get(cur_index..(cur_index + 1))
161 .ok_or(error.clone())?
162 {
163 "a" => {
164 cur_index += 1;
165 Some(File::A)
166 }
167 "b" => {
168 cur_index += 1;
169 Some(File::B)
170 }
171 "c" => {
172 cur_index += 1;
173 Some(File::C)
174 }
175 "d" => {
176 cur_index += 1;
177 Some(File::D)
178 }
179 "e" => {
180 cur_index += 1;
181 Some(File::E)
182 }
183 "f" => {
184 cur_index += 1;
185 Some(File::F)
186 }
187 "g" => {
188 cur_index += 1;
189 Some(File::G)
190 }
191 "h" => {
192 cur_index += 1;
193 Some(File::H)
194 }
195 _ => None,
196 };
197
198 let mut source_rank = match move_text
199 .get(cur_index..(cur_index + 1))
200 .ok_or(error.clone())?
201 {
202 "1" => {
203 cur_index += 1;
204 Some(Rank::First)
205 }
206 "2" => {
207 cur_index += 1;
208 Some(Rank::Second)
209 }
210 "3" => {
211 cur_index += 1;
212 Some(Rank::Third)
213 }
214 "4" => {
215 cur_index += 1;
216 Some(Rank::Fourth)
217 }
218 "5" => {
219 cur_index += 1;
220 Some(Rank::Fifth)
221 }
222 "6" => {
223 cur_index += 1;
224 Some(Rank::Sixth)
225 }
226 "7" => {
227 cur_index += 1;
228 Some(Rank::Seventh)
229 }
230 "8" => {
231 cur_index += 1;
232 Some(Rank::Eighth)
233 }
234 _ => None,
235 };
236
237 let takes = if let Some(s) = move_text.get(cur_index..(cur_index + 1)) {
238 match s {
239 "x" => {
240 cur_index += 1;
241 true
242 }
243 _ => false,
244 }
245 } else {
246 false
247 };
248
249 let dest = if let Some(s) = move_text.get(cur_index..(cur_index + 2)) {
250 if let Ok(q) = Square::from_str(s) {
251 cur_index += 2;
252 q
253 } else {
254 let sq = Square::make_square(
255 source_rank.ok_or(error.clone())?,
256 source_file.ok_or(error.clone())?,
257 );
258 source_rank = None;
259 source_file = None;
260 sq
261 }
262 } else {
263 let sq = Square::make_square(
264 source_rank.ok_or(error.clone())?,
265 source_file.ok_or(error.clone())?,
266 );
267 source_rank = None;
268 source_file = None;
269 sq
270 };
271
272 let promotion = if let Some(s) = move_text.get(cur_index..(cur_index + 1)) {
273 match s {
274 "N" => {
275 cur_index += 1;
276 Some(Piece::Knight)
277 }
278 "B" => {
279 cur_index += 1;
280 Some(Piece::Bishop)
281 }
282 "R" => {
283 cur_index += 1;
284 Some(Piece::Rook)
285 }
286 "Q" => {
287 cur_index += 1;
288 Some(Piece::Queen)
289 }
290 _ => None,
291 }
292 } else {
293 None
294 };
295
296 if let Some(s) = move_text.get(cur_index..(cur_index + 1)) {
297 let _maybe_check_or_mate = match s {
298 "+" => {
299 cur_index += 1;
300 Some(false)
301 }
302 "#" => {
303 cur_index += 1;
304 Some(true)
305 }
306 _ => None,
307 };
308 }
309
310 let ep = if let Some(s) = move_text.get(cur_index..) {
311 s == " e.p."
312 } else {
313 false
314 };
315
316 //if ep {
317 // cur_index += 5;
318 //}
319
320 // Ok, now we have all the data from the SAN move, in the following structures
321 // moveing_piece, source_rank, source_file, taks, dest, promotion, maybe_check_or_mate, and
322 // ep
323
324 let mut found_move: Option<ChessMove> = None;
325 for m in &mut MoveGen::new_legal(board) {
326 // check that the move has the properties specified
327 if board.piece_on(m.get_source()) != Some(moving_piece) {
328 continue;
329 }
330
331 if let Some(rank) = source_rank {
332 if m.get_source().get_rank() != rank {
333 continue;
334 }
335 }
336
337 if let Some(file) = source_file {
338 if m.get_source().get_file() != file {
339 continue;
340 }
341 }
342
343 if m.get_dest() != dest {
344 continue;
345 }
346
347 if m.get_promotion() != promotion {
348 continue;
349 }
350
351 if found_move.is_some() {
352 return Err(error);
353 }
354
355 // takes is complicated, because of e.p.
356 if !takes {
357 if board.piece_on(m.get_dest()).is_some() {
358 continue;
359 }
360 }
361
362 if !ep && takes {
363 if board.piece_on(m.get_dest()).is_none() {
364 continue;
365 }
366 }
367
368 found_move = Some(m);
369 }
370
371 found_move.ok_or(error.clone())
372 }
373}
374
375impl fmt::Display for ChessMove {
376 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
377 match self.promotion {
378 None => write!(f, "{}{}", self.source, self.dest),
379 Some(x) => write!(f, "{}{}{}", self.source, self.dest, x),
380 }
381 }
382}
383
384impl Ord for ChessMove {
385 fn cmp(&self, other: &ChessMove) -> Ordering {
386 if self.source != other.source {
387 self.source.cmp(&other.source)
388 } else if self.dest != other.dest {
389 self.dest.cmp(&other.dest)
390 } else if self.promotion != other.promotion {
391 match self.promotion {
392 None => Ordering::Less,
393 Some(x) => match other.promotion {
394 None => Ordering::Greater,
395 Some(y) => x.cmp(&y),
396 },
397 }
398 } else {
399 Ordering::Equal
400 }
401 }
402}
403
404/// Convert a UCI `String` to a move. If invalid, return `None`
405/// ```
406/// use chess::{ChessMove, Square, Piece};
407/// use std::str::FromStr;
408///
409/// let mv = ChessMove::new(Square::E7, Square::E8, Some(Piece::Queen));
410///
411/// assert_eq!(ChessMove::from_str("e7e8q").expect("Valid Move"), mv);
412/// ```
413impl FromStr for ChessMove {
414 type Err = Error;
415
416 fn from_str(s: &str) -> Result<Self, Self::Err> {
417 let source = Square::from_str(s.get(0..2).ok_or(Error::InvalidUciMove)?)?;
418 let dest = Square::from_str(s.get(2..4).ok_or(Error::InvalidUciMove)?)?;
419
420 let mut promo = None;
421 if s.len() == 5 {
422 promo = Some(match s.chars().last().ok_or(Error::InvalidUciMove)? {
423 'q' => Piece::Queen,
424 'r' => Piece::Rook,
425 'n' => Piece::Knight,
426 'b' => Piece::Bishop,
427 _ => return Err(Error::InvalidUciMove),
428 });
429 }
430
431 Ok(ChessMove::new(source, dest, promo))
432 }
433}
434
435#[test]
436fn test_basic_moves() {
437 let board = Board::default();
438 assert_eq!(
439 ChessMove::from_san(&board, "e4").expect("e4 is valid in the initial position"),
440 ChessMove::new(Square::E2, Square::E4, None)
441 );
442}