Skip to main content

chess/movegen/
generator.rs

1use crate::bitboard::{BitBoard, EMPTY};
2use crate::board::Board;
3use crate::chess_move::ChessMove;
4use crate::magic::between;
5use crate::movegen::piece_type::*;
6use crate::piece::{NUM_PROMOTION_PIECES, PROMOTION_PIECES, Piece};
7use crate::square::Square;
8use arrayvec::ArrayVec;
9use nodrop::NoDrop;
10use std::iter::ExactSizeIterator;
11
12#[derive(Copy, Clone, PartialEq, PartialOrd)]
13pub struct SquareAndBitBoard {
14	square: Square,
15	bitboard: BitBoard,
16	promotion: bool,
17}
18
19impl SquareAndBitBoard {
20	pub fn new(sq: Square, bb: BitBoard, promotion: bool) -> SquareAndBitBoard {
21		SquareAndBitBoard { square: sq, bitboard: bb, promotion }
22	}
23}
24
25pub type MoveList = NoDrop<ArrayVec<SquareAndBitBoard, 18>>;
26
27/// An incremental move generator
28///
29/// This structure enumerates moves slightly slower than board.enumerate_moves(...),
30/// but has some extra features, such as:
31///
32/// * Being an iterator
33/// * Not requiring you to create a buffer
34/// * Only iterating moves that match a certain pattern
35/// * Being iterable multiple times (such as, iterating once for all captures, then iterating again
36///   for all quiets)
37/// * Doing as little work early on as possible, so that if you are not going to look at every move, the
38///   struture moves faster
39/// * Being able to iterate pseudo legal moves, while keeping the (nearly) free legality checks in
40///   place
41///
42/// # Examples
43///
44/// ```
45/// use chess::MoveGen;
46/// use chess::Board;
47/// use chess::EMPTY;
48/// use chess::construct;
49///
50/// // create a board with the initial position
51/// let board = Board::default();
52///
53/// // create an iterable
54/// let mut iterable = MoveGen::new_legal(&board);
55///
56/// // make sure .len() works.
57/// assert_eq!(iterable.len(), 20); // the .len() function does *not* consume the iterator
58///
59/// // lets iterate over targets.
60/// let targets = board.color_combined(!board.side_to_move());
61/// iterable.set_iterator_mask(*targets);
62///
63/// // count the number of targets
64/// let mut count = 0;
65/// for _ in &mut iterable {
66///     count += 1;
67///     // This move captures one of my opponents pieces (with the exception of en passant)
68/// }
69///
70/// // now, iterate over the rest of the moves
71/// iterable.set_iterator_mask(!EMPTY);
72/// for _ in &mut iterable {
73///     count += 1;
74///     // This move does not capture anything
75/// }
76///
77/// // make sure it works
78/// assert_eq!(count, 20);
79///
80/// ```
81pub struct MoveGen {
82	moves: MoveList,
83	promotion_index: usize,
84	iterator_mask: BitBoard,
85	index: usize,
86}
87
88impl MoveGen {
89	#[inline(always)]
90	fn enumerate_moves(board: &Board) -> MoveList {
91		let checkers = *board.checkers();
92		let mask = !board.color_combined(board.side_to_move());
93		let mut movelist = NoDrop::new(ArrayVec::<SquareAndBitBoard, 18>::new());
94
95		if checkers == EMPTY {
96			PawnType::legals::<NotInCheckType>(&mut movelist, board, mask);
97			KnightType::legals::<NotInCheckType>(&mut movelist, board, mask);
98			BishopType::legals::<NotInCheckType>(&mut movelist, board, mask);
99			RookType::legals::<NotInCheckType>(&mut movelist, board, mask);
100			QueenType::legals::<NotInCheckType>(&mut movelist, board, mask);
101			KingType::legals::<NotInCheckType>(&mut movelist, board, mask);
102		} else if checkers.popcnt() == 1 {
103			PawnType::legals::<InCheckType>(&mut movelist, board, mask);
104			KnightType::legals::<InCheckType>(&mut movelist, board, mask);
105			BishopType::legals::<InCheckType>(&mut movelist, board, mask);
106			RookType::legals::<InCheckType>(&mut movelist, board, mask);
107			QueenType::legals::<InCheckType>(&mut movelist, board, mask);
108			KingType::legals::<InCheckType>(&mut movelist, board, mask);
109		} else {
110			KingType::legals::<InCheckType>(&mut movelist, board, mask);
111		}
112
113		movelist
114	}
115
116	/// Create a new `MoveGen` structure, only generating legal moves
117	#[inline(always)]
118	pub fn new_legal(board: &Board) -> MoveGen {
119		MoveGen { moves: MoveGen::enumerate_moves(board), promotion_index: 0, iterator_mask: !EMPTY, index: 0 }
120	}
121
122	/// Never, ever, iterate any moves that land on the following squares
123	pub fn remove_mask(&mut self, mask: BitBoard) {
124		for x in 0..self.moves.len() {
125			self.moves[x].bitboard &= !mask;
126		}
127	}
128
129	/// Never, ever, iterate this move
130	pub fn remove_move(&mut self, chess_move: ChessMove) -> bool {
131		for x in 0..self.moves.len() {
132			if self.moves[x].square == chess_move.get_source() {
133				self.moves[x].bitboard &= !BitBoard::from_square(chess_move.get_dest());
134				return true;
135			}
136		}
137		false
138	}
139
140	/// For now, Only iterate moves that land on the following squares
141	/// Note: Once iteration is completed, you can pass in a mask of ! `EMPTY`
142	///       to get the remaining moves, or another mask
143	pub fn set_iterator_mask(&mut self, mask: BitBoard) {
144		self.iterator_mask = mask;
145		self.index = 0;
146
147		// the iterator portion of this struct relies on the invariant that
148		// the bitboards at the beginning of the moves[] array are the only
149		// ones used.  As a result, we must partition the list such that the
150		// assumption is true.
151
152		// first, find the first non-used moves index, and store that in i
153		let mut i = 0;
154		while i < self.moves.len() && self.moves[i].bitboard & self.iterator_mask != EMPTY {
155			i += 1;
156		}
157
158		// next, find each element past i where the moves are used, and store
159		// that in i.  Then, increment i to point to a new unused slot.
160		for j in (i + 1)..self.moves.len() {
161			if self.moves[j].bitboard & self.iterator_mask != EMPTY {
162				let backup = self.moves[i];
163				self.moves[i] = self.moves[j];
164				self.moves[j] = backup;
165				i += 1;
166			}
167		}
168	}
169
170	/// This function checks the legality *only for moves generated by `MoveGen`*.
171	///
172	/// Calling this function for moves not generated by `MoveGen` will result in possibly
173	/// incorrect results, and making that move on the `Board` will result in undefined behavior.
174	/// This function may panic! if these rules are not followed.
175	///
176	/// If you are validating a move from a user, you should call the .legal() function.
177	pub fn legal_quick(board: &Board, chess_move: ChessMove) -> bool {
178		let piece = board.piece_on(chess_move.get_source()).unwrap();
179		match piece {
180			Piece::Rook => true,
181			Piece::Bishop => true,
182			Piece::Knight => true,
183			Piece::Queen => true,
184			Piece::Pawn => {
185				if chess_move.get_source().get_file() != chess_move.get_dest().get_file() && board.piece_on(chess_move.get_dest()).is_none() {
186					// en-passant
187					PawnType::legal_ep_move(board, chess_move.get_source(), chess_move.get_dest())
188				} else {
189					true
190				}
191			}
192			Piece::King => {
193				let bb = between(chess_move.get_source(), chess_move.get_dest());
194				if bb.popcnt() == 1 {
195					// castles
196					if !KingType::legal_king_move(board, bb.to_square()) {
197						false
198					} else {
199						KingType::legal_king_move(board, chess_move.get_dest())
200					}
201				} else {
202					KingType::legal_king_move(board, chess_move.get_dest())
203				}
204			}
205		}
206	}
207
208	/// Fastest perft test with this structure
209	pub fn movegen_perft_test(board: &Board, depth: usize) -> usize {
210		let iterable = MoveGen::new_legal(board);
211
212		let mut result: usize = 0;
213		if depth == 1 {
214			iterable.len()
215		} else {
216			for m in iterable {
217				let bresult = board.make_move_new(m);
218				result += MoveGen::movegen_perft_test(&bresult, depth - 1);
219			}
220			result
221		}
222	}
223
224	#[cfg(test)]
225	/// Do a perft test after splitting the moves up into two groups
226	pub fn movegen_perft_test_piecewise(board: &Board, depth: usize) -> usize {
227		let mut iterable = MoveGen::new_legal(board);
228
229		let targets = board.color_combined(!board.side_to_move());
230		let mut result: usize = 0;
231
232		if depth == 1 {
233			iterable.set_iterator_mask(*targets);
234			result += iterable.len();
235			iterable.set_iterator_mask(!targets);
236			result += iterable.len();
237			result
238		} else {
239			iterable.set_iterator_mask(*targets);
240			for x in &mut iterable {
241				let mut bresult = std::mem::MaybeUninit::<Board>::uninit();
242				unsafe {
243					board.make_move(x, &mut *bresult.as_mut_ptr());
244					result += MoveGen::movegen_perft_test(&*bresult.as_ptr(), depth - 1);
245				}
246			}
247			iterable.set_iterator_mask(!EMPTY);
248			for x in &mut iterable {
249				let mut bresult = std::mem::MaybeUninit::<Board>::uninit();
250				unsafe {
251					board.make_move(x, &mut *bresult.as_mut_ptr());
252					result += MoveGen::movegen_perft_test(&*bresult.as_ptr(), depth - 1);
253				}
254			}
255			result
256		}
257	}
258}
259
260impl ExactSizeIterator for MoveGen {
261	/// Give the exact length of this iterator
262	fn len(&self) -> usize {
263		let mut result = 0;
264		for i in 0..self.moves.len() {
265			if self.moves[i].bitboard & self.iterator_mask == EMPTY {
266				break;
267			}
268			if self.moves[i].promotion {
269				result += ((self.moves[i].bitboard & self.iterator_mask).popcnt() as usize) * NUM_PROMOTION_PIECES;
270			} else {
271				result += (self.moves[i].bitboard & self.iterator_mask).popcnt() as usize;
272			}
273		}
274		result
275	}
276}
277
278impl Iterator for MoveGen {
279	type Item = ChessMove;
280
281	/// Give a size_hint to some functions that need it
282	fn size_hint(&self) -> (usize, Option<usize>) {
283		let len = self.len();
284		(len, Some(len))
285	}
286
287	/// Find the next chess move.
288	fn next(&mut self) -> Option<ChessMove> {
289		if self.index >= self.moves.len() || self.moves[self.index].bitboard & self.iterator_mask == EMPTY {
290			// are we done?
291			None
292		} else if self.moves[self.index].promotion {
293			let moves = &mut self.moves[self.index];
294
295			let dest = (moves.bitboard & self.iterator_mask).to_square();
296
297			// deal with potential promotions for this pawn
298			let result = ChessMove::new(moves.square, dest, Some(PROMOTION_PIECES[self.promotion_index]));
299			self.promotion_index += 1;
300			if self.promotion_index >= NUM_PROMOTION_PIECES {
301				moves.bitboard ^= BitBoard::from_square(dest);
302				self.promotion_index = 0;
303				if moves.bitboard & self.iterator_mask == EMPTY {
304					self.index += 1;
305				}
306			}
307			Some(result)
308		} else {
309			// not a promotion move, so its a 'normal' move as far as this function is concerned
310			let moves = &mut self.moves[self.index];
311			let dest = (moves.bitboard & self.iterator_mask).to_square();
312
313			moves.bitboard ^= BitBoard::from_square(dest);
314			if moves.bitboard & self.iterator_mask == EMPTY {
315				self.index += 1;
316			}
317			Some(ChessMove::new(moves.square, dest, None))
318		}
319	}
320}
321
322#[cfg(test)]
323use crate::board_builder::BoardBuilder;
324#[cfg(test)]
325use std::collections::HashSet;
326#[cfg(test)]
327use std::convert::TryInto;
328#[cfg(test)]
329use std::str::FromStr;
330
331#[cfg(test)]
332fn movegen_perft_test(fen: String, depth: usize, result: usize) {
333	let board: Board = BoardBuilder::from_str(&fen).unwrap().try_into().unwrap();
334
335	assert_eq!(MoveGen::movegen_perft_test(&board, depth), result);
336	assert_eq!(MoveGen::movegen_perft_test_piecewise(&board, depth), result);
337}
338
339#[test]
340fn movegen_perft_kiwipete() {
341	movegen_perft_test("r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1".to_owned(), 5, 193690690);
342}
343
344#[test]
345fn movegen_perft_1() {
346	movegen_perft_test("8/5bk1/8/2Pp4/8/1K6/8/8 w - d6 0 1".to_owned(), 6, 824064);
347	// Invalid FEN
348}
349
350#[test]
351fn movegen_perft_2() {
352	movegen_perft_test("8/8/1k6/8/2pP4/8/5BK1/8 b - d3 0 1".to_owned(), 6, 824064);
353	// Invalid FEN
354}
355
356#[test]
357fn movegen_perft_3() {
358	movegen_perft_test("8/8/1k6/2b5/2pP4/8/5K2/8 b - d3 0 1".to_owned(), 6, 1440467);
359}
360
361#[test]
362fn movegen_perft_4() {
363	movegen_perft_test("8/5k2/8/2Pp4/2B5/1K6/8/8 w - d6 0 1".to_owned(), 6, 1440467);
364}
365
366#[test]
367fn movegen_perft_5() {
368	movegen_perft_test("5k2/8/8/8/8/8/8/4K2R w K - 0 1".to_owned(), 6, 661072);
369}
370
371#[test]
372fn movegen_perft_6() {
373	movegen_perft_test("4k2r/8/8/8/8/8/8/5K2 b k - 0 1".to_owned(), 6, 661072);
374}
375
376#[test]
377fn movegen_perft_7() {
378	movegen_perft_test("3k4/8/8/8/8/8/8/R3K3 w Q - 0 1".to_owned(), 6, 803711);
379}
380
381#[test]
382fn movegen_perft_8() {
383	movegen_perft_test("r3k3/8/8/8/8/8/8/3K4 b q - 0 1".to_owned(), 6, 803711);
384}
385
386#[test]
387fn movegen_perft_9() {
388	movegen_perft_test("r3k2r/1b4bq/8/8/8/8/7B/R3K2R w KQkq - 0 1".to_owned(), 4, 1274206);
389}
390
391#[test]
392fn movegen_perft_10() {
393	movegen_perft_test("r3k2r/7b/8/8/8/8/1B4BQ/R3K2R b KQkq - 0 1".to_owned(), 4, 1274206);
394}
395
396#[test]
397fn movegen_perft_11() {
398	movegen_perft_test("r3k2r/8/3Q4/8/8/5q2/8/R3K2R b KQkq - 0 1".to_owned(), 4, 1720476);
399}
400
401#[test]
402fn movegen_perft_12() {
403	movegen_perft_test("r3k2r/8/5Q2/8/8/3q4/8/R3K2R w KQkq - 0 1".to_owned(), 4, 1720476);
404}
405
406#[test]
407fn movegen_perft_13() {
408	movegen_perft_test("2K2r2/4P3/8/8/8/8/8/3k4 w - - 0 1".to_owned(), 6, 3821001);
409}
410
411#[test]
412fn movegen_perft_14() {
413	movegen_perft_test("3K4/8/8/8/8/8/4p3/2k2R2 b - - 0 1".to_owned(), 6, 3821001);
414}
415
416#[test]
417fn movegen_perft_15() {
418	movegen_perft_test("8/8/1P2K3/8/2n5/1q6/8/5k2 b - - 0 1".to_owned(), 5, 1004658);
419}
420
421#[test]
422fn movegen_perft_16() {
423	movegen_perft_test("5K2/8/1Q6/2N5/8/1p2k3/8/8 w - - 0 1".to_owned(), 5, 1004658);
424}
425
426#[test]
427fn movegen_perft_17() {
428	movegen_perft_test("4k3/1P6/8/8/8/8/K7/8 w - - 0 1".to_owned(), 6, 217342);
429}
430
431#[test]
432fn movegen_perft_18() {
433	movegen_perft_test("8/k7/8/8/8/8/1p6/4K3 b - - 0 1".to_owned(), 6, 217342);
434}
435
436#[test]
437fn movegen_perft_19() {
438	movegen_perft_test("8/P1k5/K7/8/8/8/8/8 w - - 0 1".to_owned(), 6, 92683);
439}
440
441#[test]
442fn movegen_perft_20() {
443	movegen_perft_test("8/8/8/8/8/k7/p1K5/8 b - - 0 1".to_owned(), 6, 92683);
444}
445
446#[test]
447fn movegen_perft_21() {
448	movegen_perft_test("K1k5/8/P7/8/8/8/8/8 w - - 0 1".to_owned(), 6, 2217);
449}
450
451#[test]
452fn movegen_perft_22() {
453	movegen_perft_test("8/8/8/8/8/p7/8/k1K5 b - - 0 1".to_owned(), 6, 2217);
454}
455
456#[test]
457fn movegen_perft_23() {
458	movegen_perft_test("8/k1P5/8/1K6/8/8/8/8 w - - 0 1".to_owned(), 7, 567584);
459}
460
461#[test]
462fn movegen_perft_24() {
463	movegen_perft_test("8/8/8/8/1k6/8/K1p5/8 b - - 0 1".to_owned(), 7, 567584);
464}
465
466#[test]
467fn movegen_perft_25() {
468	movegen_perft_test("8/8/2k5/5q2/5n2/8/5K2/8 b - - 0 1".to_owned(), 4, 23527);
469}
470
471#[test]
472fn movegen_perft_26() {
473	movegen_perft_test("8/5k2/8/5N2/5Q2/2K5/8/8 w - - 0 1".to_owned(), 4, 23527);
474}
475
476#[test]
477fn movegen_issue_15() {
478	let board = BoardBuilder::from_str("rnbqkbnr/ppp2pp1/4p3/3N4/3PpPp1/8/PPP3PP/R1B1KBNR b KQkq f3 0 1").unwrap().try_into().unwrap();
479	let _ = MoveGen::new_legal(&board);
480}
481
482#[cfg(test)]
483fn move_of(m: &str) -> ChessMove {
484	let promo = if m.len() > 4 {
485		Some(match m.as_bytes()[4] {
486			b'q' => Piece::Queen,
487			b'r' => Piece::Rook,
488			b'b' => Piece::Bishop,
489			b'n' => Piece::Knight,
490			_ => panic!("unrecognized uci move: {}", m),
491		})
492	} else {
493		None
494	};
495	ChessMove::new(Square::from_str(&m[..2]).unwrap(), Square::from_str(&m[2..4]).unwrap(), promo)
496}
497
498#[test]
499fn test_masked_move_gen() {
500	let board = Board::from_str("r1bqkb1r/pp3ppp/5n2/2ppn1N1/4pP2/1BN1P3/PPPP2PP/R1BQ1RK1 w kq - 0 9").unwrap();
501
502	let mut capture_moves = MoveGen::new_legal(&board);
503	let targets = *board.color_combined(!board.side_to_move());
504	capture_moves.set_iterator_mask(targets);
505
506	let expected = vec![move_of("f4e5"), move_of("b3d5"), move_of("g5e4"), move_of("g5f7"), move_of("g5h7"), move_of("c3e4"), move_of("c3d5")];
507
508	assert_eq!(capture_moves.collect::<HashSet<_>>(), expected.into_iter().collect());
509}