Skip to main content

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