1mod board_manip;
2mod chess_move;
3mod coords;
4mod engine;
5mod piece;
6mod player;
7mod position;
8mod uci_long;
9
10use core::panic;
11
12pub use crate::board_manip::{move_piece, piece_at, put_piece_at, take_piece_at};
13pub use crate::chess_move::{ChessMove, Move};
14pub use crate::coords::{all_squares, cards, eight_degrees, inter_cards, Coords, Direction};
15pub use crate::engine::{BasicEvaluationPlayer, BetterEvaluationPlayer, FirstMovePlayer, Planner};
16#[cfg(feature = "rng")]
17pub use crate::engine::{RandomCapturePrioPlayer, RandomPlayer};
18pub use crate::piece::{Piece, PieceColor, PieceKind};
19pub use crate::player::Player;
20pub use crate::position::Position;
21
22#[derive(Debug)]
23pub struct Game {
24 pub current_position: Position,
25 pub checkmated: Option<PieceColor>,
26 pub stalemate: bool,
27}
28
29impl Game {
30 pub fn start() -> Game {
31 let mut board = Vec::new();
32 for i in 0..8 {
33 let mut row = Vec::new();
34 for j in 0..8 {
35 row.push(Piece::from_initial_position(j, i));
36 }
37 board.push(row);
38 }
39 Game {
40 current_position: Position::initial(),
41 checkmated: None,
42 stalemate: false,
43 }
44 }
45
46 pub fn empty() -> Game {
47 Game {
48 current_position: Position::empty_board(),
49 checkmated: None,
50 stalemate: false,
51 }
52 }
53 pub fn make_move(&mut self, chess_move: &ChessMove) {
54 if self.current_position.is_move_legal(chess_move) {
55 self.current_position = self.current_position.after_move(chess_move);
56 if self.current_position.is_checkmate() {
57 self.checkmated = Some(self.current_position.to_move.clone());
58 }
59 self.stalemate = self.current_position.is_stalemate()
60 }
61 }
62
63 pub fn from_starting_position(starting_position: Position) -> Game {
64 let checkmated = starting_position.checkmated();
65 let stalemate = starting_position.is_stalemate();
66 Game {
67 current_position: starting_position,
68 checkmated,
69 stalemate,
70 }
71 }
72}
73
74#[derive(Debug)]
75pub enum GameResult {
76 WhiteWin,
77 BlackWin,
78 Stalemate,
79 TimedOut,
80}
81
82pub fn play_engine_game(
83 white_player: Box<dyn Player>,
84 black_player: Box<dyn Player>,
85) -> GameResult {
86 let mut game = Game::start();
87 let mut turn_counter = 0;
88
89 while game.checkmated.is_none() && !game.current_position.is_stalemate() && turn_counter < 300 {
90 let offered_move = match game.current_position.to_move {
91 PieceColor::White => white_player.offer_move(&game.current_position),
92 PieceColor::Black => black_player.offer_move(&game.current_position),
93 };
94 if !game.current_position.is_move_legal(&offered_move) {
95 panic!("engine offered illegal move");
96 } else {
97 game.make_move(&offered_move);
98 turn_counter += 1;
99 }
100 }
101 if let Some(color) = game.checkmated {
102 match color {
103 PieceColor::White => GameResult::BlackWin,
104 PieceColor::Black => GameResult::WhiteWin,
105 }
106 } else if game.current_position.is_stalemate() {
107 GameResult::Stalemate
108 } else {
109 GameResult::TimedOut
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use core::panic;
116 use std::{collections::HashSet, hash::RandomState};
117
118 use super::*;
119
120 #[test]
121 fn pawn_homerow() {
122 let position = Position::from_fen("8/8/8/8/8/8/4P3/8 w - - 0 1");
123 let pawn_location = Coords { y: 6, x: 4 };
124 assert_eq!(
125 position.legal_moves_from_origin(&pawn_location),
126 vec![
127 ChessMove::RegularMove(Move {
128 origin: pawn_location,
129 destination: Coords { y: 5, x: 4 }
130 }),
131 ChessMove::PawnSkip(Move {
132 origin: pawn_location,
133 destination: Coords { y: 4, x: 4 }
134 })
135 ]
136 )
137 }
138
139 #[test]
140 fn pawn_not_homerow() {
141 let position = Position::from_fen("8/8/8/8/8/4P3/8/8 w - - 0 1");
142 let pawn_location = Coords { y: 5, x: 4 };
143 assert_eq!(
144 position.legal_moves_from_origin(&pawn_location),
145 vec![ChessMove::RegularMove(Move {
146 origin: pawn_location,
147 destination: Coords { y: 4, x: 4 }
148 })]
149 )
150 }
151
152 #[test]
153 fn pawn_not_homerow_with_capture() {
154 let position = Position::from_fen("8/8/8/8/5p2/4P3/8/8 w - - 0 1");
155 let pawn_location = Coords { y: 5, x: 4 };
156 let opposing_pawn_location = Coords { y: 4, x: 5 };
157 assert_eq!(
158 position.legal_moves_from_origin(&pawn_location),
159 vec![
160 ChessMove::RegularMove(Move {
161 origin: pawn_location,
162 destination: Coords { y: 4, x: 4 }
163 }),
164 ChessMove::RegularMove(Move {
165 origin: pawn_location,
166 destination: opposing_pawn_location
167 })
168 ]
169 )
170 }
171
172 #[test]
173 fn pawn_not_homerow_blocked() {
174 let position = Position::from_fen("8/8/8/8/4p3/4P3/8/8 w - - 0 1");
175 let pawn_location = Coords { y: 5, x: 4 };
176 assert_eq!(position.legal_moves_from_origin(&pawn_location), vec![])
177 }
178
179 #[test]
180 fn pawn_edge_of_board_horizontal_blocked() {
181 let position = Position::from_fen("8/8/7P/7P/8/8/8/8 w - - 0 1");
182 let pawn_location = Coords { y: 3, x: 7 };
183 assert_eq!(position.legal_moves_from_origin(&pawn_location), vec![])
184 }
185
186 #[test]
187 fn pawn_not_homerow_with_capture_blocked() {
188 let position = Position::from_fen("8/8/8/8/4pp2/4P3/8/8 w - - 0 1");
189 let pawn_location = Coords { y: 5, x: 4 };
190 let opposing_pawn_location = Coords { y: 4, x: 5 };
191 assert_eq!(
192 position.legal_moves_from_origin(&pawn_location),
193 vec![ChessMove::RegularMove(Move {
194 origin: pawn_location,
195 destination: opposing_pawn_location
196 })]
197 )
198 }
199
200 #[test]
201 fn pawn_homerow_blocked() {
202 let position = Position::from_fen("8/8/8/8/8/4p3/4P3/8 w - - 0 1");
203 let pawn_location = Coords { y: 6, x: 4 };
204 assert_eq!(position.legal_moves_from_origin(&pawn_location), vec![])
205 }
206
207 #[test]
208 fn pawn_homerow_second_square_blocked() {
209 let position = Position::from_fen("8/8/8/8/4p3/8/4P3/8 w - - 0 1");
210 let pawn_location = Coords { y: 6, x: 4 };
211 assert_eq!(
212 position.legal_moves_from_origin(&pawn_location),
213 vec![ChessMove::RegularMove(Move {
214 origin: pawn_location,
215 destination: Coords { y: 5, x: 4 }
216 })]
217 )
218 }
219
220 #[test]
221 fn pawn_homerow_with_capture_blocked() {
222 let position = Position::from_fen("8/8/8/8/8/4pp2/4P3/8 w - - 0 1");
223 let pawn_location = Coords { y: 6, x: 4 };
224 let capture_location = Coords { y: 5, x: 5 };
225 assert_eq!(
226 position.legal_moves_from_origin(&pawn_location),
227 vec![ChessMove::RegularMove(Move {
228 origin: pawn_location,
229 destination: capture_location
230 })]
231 )
232 }
233
234 #[test]
235 fn rook_middle_board() {
236 let mut position = Position::empty_board();
237 position.board[4][4] = Some(Piece {
238 kind: PieceKind::Rook,
239 color: PieceColor::White,
240 });
241 let rook_location = Coords { y: 4, x: 4 };
242 let mut legal_moves = vec![];
243
244 for j in 0..8 {
245 if j != 4 {
246 legal_moves.push(ChessMove::RegularMove(Move {
247 origin: rook_location,
248 destination: Coords { y: 4, x: j },
249 }));
250 }
251 }
252 for i in 0..8 {
253 if i != 4 {
254 legal_moves.push(ChessMove::RegularMove(Move {
255 origin: rook_location,
256 destination: Coords { x: 4, y: i },
257 }));
258 }
259 }
260
261 let legal_move_set: HashSet<ChessMove, RandomState> =
262 HashSet::from_iter(legal_moves.iter().cloned());
263 let found_moves: HashSet<ChessMove, RandomState> =
264 HashSet::from_iter(position.legal_moves_from_origin(&rook_location));
265 let diff: HashSet<&ChessMove, RandomState> =
266 legal_move_set.symmetric_difference(&found_moves).collect();
267
268 assert_eq!(diff, HashSet::new())
269 }
270
271 #[test]
272 fn rook_middle_board_boxed_in_opposite_color() {
273 let mut position = Position::empty_board();
274 position.board[4][4] = Some(Piece {
275 kind: PieceKind::Rook,
276 color: PieceColor::White,
277 });
278 position.board[5][4] = Some(Piece {
279 kind: PieceKind::Rook,
280 color: PieceColor::Black,
281 });
282 position.board[3][4] = Some(Piece {
283 kind: PieceKind::Rook,
284 color: PieceColor::Black,
285 });
286 position.board[4][5] = Some(Piece {
287 kind: PieceKind::Rook,
288 color: PieceColor::Black,
289 });
290 position.board[4][3] = Some(Piece {
291 kind: PieceKind::Rook,
292 color: PieceColor::Black,
293 });
294 let rook_location = Coords { y: 4, x: 4 };
295 let up = Coords { y: 5, x: 4 };
296 let down = Coords { y: 3, x: 4 };
297 let left = Coords { y: 4, x: 3 };
298 let right = Coords { y: 4, x: 5 };
299
300 let legal_moves = vec![
301 ChessMove::RegularMove(Move {
302 origin: rook_location,
303 destination: up,
304 }),
305 ChessMove::RegularMove(Move {
306 origin: rook_location,
307 destination: down,
308 }),
309 ChessMove::RegularMove(Move {
310 origin: rook_location,
311 destination: left,
312 }),
313 ChessMove::RegularMove(Move {
314 origin: rook_location,
315 destination: right,
316 }),
317 ];
318
319 assert_eq!(
320 position.legal_moves_from_origin(&rook_location),
321 legal_moves
322 );
323 }
324
325 #[test]
326 fn rook_middle_board_boxed_in_own_color() {
327 let mut position = Position::empty_board();
328 position.board[4][4] = Some(Piece {
329 kind: PieceKind::Rook,
330 color: PieceColor::White,
331 });
332 position.board[5][4] = Some(Piece {
333 kind: PieceKind::Rook,
334 color: PieceColor::White,
335 });
336 position.board[3][4] = Some(Piece {
337 kind: PieceKind::Rook,
338 color: PieceColor::White,
339 });
340 position.board[4][5] = Some(Piece {
341 kind: PieceKind::Rook,
342 color: PieceColor::White,
343 });
344 position.board[4][3] = Some(Piece {
345 kind: PieceKind::Rook,
346 color: PieceColor::White,
347 });
348 let rook_location = Coords { y: 4, x: 4 };
349
350 let legal_moves = vec![];
351
352 assert_eq!(
353 position.legal_moves_from_origin(&rook_location),
354 legal_moves
355 );
356 }
357
358 #[test]
359 fn knight_middle_board() {
360 let mut position = Position::empty_board();
361 position.board[3][3] = Some(Piece {
362 kind: PieceKind::Knight,
363 color: PieceColor::White,
364 });
365 let knight_location = Coords { y: 3, x: 3 };
366
367 let legal_moves: HashSet<ChessMove, RandomState> = HashSet::from_iter(
368 vec![
369 ChessMove::RegularMove(Move {
370 origin: knight_location,
371 destination: Coords { y: 5, x: 4 },
372 }),
373 ChessMove::RegularMove(Move {
374 origin: knight_location,
375 destination: Coords { y: 4, x: 5 },
376 }),
377 ChessMove::RegularMove(Move {
378 origin: knight_location,
379 destination: Coords { y: 5, x: 2 },
380 }),
381 ChessMove::RegularMove(Move {
382 origin: knight_location,
383 destination: Coords { y: 4, x: 1 },
384 }),
385 ChessMove::RegularMove(Move {
386 origin: knight_location,
387 destination: Coords { y: 1, x: 4 },
388 }),
389 ChessMove::RegularMove(Move {
390 origin: knight_location,
391 destination: Coords { y: 2, x: 5 },
392 }),
393 ChessMove::RegularMove(Move {
394 origin: knight_location,
395 destination: Coords { y: 1, x: 2 },
396 }),
397 ChessMove::RegularMove(Move {
398 origin: knight_location,
399 destination: Coords { y: 2, x: 1 },
400 }),
401 ]
402 .iter()
403 .cloned(),
404 );
405
406 let found_moves: HashSet<ChessMove, RandomState> = HashSet::from_iter(
407 position
408 .legal_moves_from_origin(&knight_location)
409 .iter()
410 .cloned(),
411 );
412
413 let diff: HashSet<&ChessMove, RandomState> =
414 legal_moves.symmetric_difference(&found_moves).collect();
415
416 assert_eq!(diff, HashSet::new())
417 }
418
419 #[test]
420 fn knight_corner() {
421 let mut position = Position::empty_board();
422 position.board[0][0] = Some(Piece {
423 kind: PieceKind::Knight,
424 color: PieceColor::White,
425 });
426 let knight_location = Coords { y: 0, x: 0 };
427
428 let legal_moves: HashSet<ChessMove, RandomState> = HashSet::from_iter(
429 vec![
430 ChessMove::RegularMove(Move {
431 origin: knight_location,
432 destination: Coords { y: 2, x: 1 },
433 }),
434 ChessMove::RegularMove(Move {
435 origin: knight_location,
436 destination: Coords { y: 1, x: 2 },
437 }),
438 ]
439 .iter()
440 .cloned(),
441 );
442
443 let found_moves: HashSet<ChessMove, RandomState> = HashSet::from_iter(
444 position
445 .legal_moves_from_origin(&knight_location)
446 .iter()
447 .cloned(),
448 );
449
450 let diff: HashSet<&ChessMove, RandomState> =
451 legal_moves.symmetric_difference(&found_moves).collect();
452
453 assert_eq!(diff, HashSet::new())
454 }
455
456 #[test]
457 fn knight_corner_blocked() {
458 let mut position = Position::empty_board();
459 position.board[0][0] = Some(Piece {
460 kind: PieceKind::Knight,
461 color: PieceColor::White,
462 });
463 position.board[1][2] = Some(Piece {
464 kind: PieceKind::Knight,
465 color: PieceColor::White,
466 });
467 position.board[2][1] = Some(Piece {
468 kind: PieceKind::Knight,
469 color: PieceColor::White,
470 });
471 let knight_location = Coords { y: 0, x: 0 };
472
473 assert_eq!(position.legal_moves_from_origin(&knight_location).len(), 0)
474 }
475
476 #[test]
477 fn bishob_middle_board() {
478 let mut position = Position::empty_board();
479 position.board[3][3] = Some(Piece {
480 kind: PieceKind::Bishop,
481 color: PieceColor::White,
482 });
483 let bishop_location = Coords { y: 3, x: 3 };
484 let mut legal_moves = vec![];
485
486 for j in 0..8 {
487 if j != 3 {
488 legal_moves.push(ChessMove::RegularMove(Move {
489 origin: bishop_location,
490 destination: Coords { y: j, x: j },
491 }));
492 }
493 }
494
495 for i in 0..7 {
496 if i != 3 {
497 legal_moves.push(ChessMove::RegularMove(Move {
498 origin: bishop_location,
499 destination: Coords { y: 6 - i, x: i },
500 }));
501 }
502 }
503
504 let legal_move_set: HashSet<ChessMove, RandomState> =
505 HashSet::from_iter(legal_moves.iter().cloned());
506 let found_move_set: HashSet<ChessMove, RandomState> = HashSet::from_iter(
507 position
508 .legal_moves_from_origin(&bishop_location)
509 .iter()
510 .cloned(),
511 );
512 let difference_set: HashSet<&ChessMove, RandomState> = legal_move_set
513 .symmetric_difference(&found_move_set)
514 .collect();
515 assert_eq!(difference_set, HashSet::new());
516 }
517
518 #[test]
519 fn king_middle_board() {
520 let mut position = Position::empty_board();
521 position.board[3][3] = Some(Piece {
522 kind: PieceKind::King,
523 color: PieceColor::White,
524 });
525 let king_location = Coords { y: 3, x: 3 };
526 let legal_moves = HashSet::from([
527 ChessMove::RegularMove(Move {
528 origin: king_location.clone(),
529 destination: Coords { y: 3, x: 4 },
530 }),
531 ChessMove::RegularMove(Move {
532 origin: king_location.clone(),
533 destination: Coords { y: 3, x: 2 },
534 }),
535 ChessMove::RegularMove(Move {
536 origin: king_location.clone(),
537 destination: Coords { y: 2, x: 3 },
538 }),
539 ChessMove::RegularMove(Move {
540 origin: king_location.clone(),
541 destination: Coords { y: 4, x: 3 },
542 }),
543 ChessMove::RegularMove(Move {
544 origin: king_location.clone(),
545 destination: Coords { y: 4, x: 4 },
546 }),
547 ChessMove::RegularMove(Move {
548 origin: king_location.clone(),
549 destination: Coords { y: 2, x: 2 },
550 }),
551 ChessMove::RegularMove(Move {
552 origin: king_location.clone(),
553 destination: Coords { y: 4, x: 2 },
554 }),
555 ChessMove::RegularMove(Move {
556 origin: king_location.clone(),
557 destination: Coords { y: 2, x: 4 },
558 }),
559 ]);
560
561 let found_moves: HashSet<ChessMove, RandomState> = HashSet::from_iter(
562 position
563 .legal_moves_from_origin(&king_location)
564 .iter()
565 .cloned(),
566 );
567
568 let diff: HashSet<&ChessMove, RandomState> =
569 legal_moves.symmetric_difference(&found_moves).collect();
570
571 assert_eq!(diff, HashSet::new())
572 }
573
574 #[test]
575 fn cannot_move_out_of_turn() {
576 let mut position = Position::empty_board();
577 position.board[3][3] = Some(Piece {
578 kind: PieceKind::King,
579 color: PieceColor::Black,
580 });
581 let king_location = Coords { y: 3, x: 3 };
582 assert_eq!(position.legal_moves_from_origin(&king_location).len(), 0);
583 }
584
585 #[test]
586 fn cannot_move_into_check() {
587 let mut position = Position::empty_board();
588
589 position.board[0][0] = Some(Piece {
590 kind: PieceKind::King,
591 color: PieceColor::White,
592 });
593 position.board[2][2] = Some(Piece {
594 kind: PieceKind::Knight,
595 color: PieceColor::Black,
596 });
597 let king_location = Coords { y: 0, x: 0 };
598 assert!(!position.is_move_legal(&ChessMove::RegularMove(Move {
599 origin: king_location,
600 destination: Coords { y: 0, x: 1 },
601 }),));
602 }
603
604 #[test]
605 fn detects_checkmate() {
606 let mut position = Position::empty_board();
607
608 position.board[0][0] = Some(Piece {
609 kind: PieceKind::King,
610 color: PieceColor::White,
611 });
612 position.board[1][1] = Some(Piece {
613 kind: PieceKind::Queen,
614 color: PieceColor::Black,
615 });
616 position.board[2][2] = Some(Piece {
617 kind: PieceKind::Queen,
618 color: PieceColor::Black,
619 });
620 assert!(position.is_checkmate());
621 }
622
623 #[test]
624 fn can_castle_right() {
625 let position = Position::from_fen("8/8/8/8/8/8/8/4K2R w KQ - 0 1");
626 assert!(position
627 .legal_moves_from_origin(&Coords { y: 7, x: 4 })
628 .contains(&ChessMove::CastleRight));
629 assert!(position.is_move_legal(&ChessMove::CastleRight,))
630 }
631
632 #[test]
633 fn castle_right() {
634 let position = Position::from_fen("8/8/8/8/8/8/8/4K2R w KQ - 0 1");
635
636 let after_castle_right = position.after_move(&ChessMove::CastleRight);
637 assert!(
638 piece_at(&after_castle_right.board, &Coords { y: 7, x: 6 }).is_some_and(|piece| piece
639 == Piece {
640 kind: PieceKind::King,
641 color: PieceColor::White
642 })
643 );
644 assert!(
645 piece_at(&after_castle_right.board, &Coords { y: 7, x: 5 }).is_some_and(|piece| piece
646 == Piece {
647 kind: PieceKind::Rook,
648 color: PieceColor::White
649 })
650 );
651 }
652
653 #[test]
654 fn make_move() {
655 let mut game = Game::empty();
656
657 game.current_position.board[0][0] = Some(Piece {
658 kind: PieceKind::King,
659 color: PieceColor::White,
660 });
661 let king_location = Coords { x: 0, y: 0 };
662 game.make_move(&ChessMove::RegularMove(Move {
663 origin: king_location,
664 destination: Coords { x: 0, y: 1 },
665 }));
666 assert!(piece_at(&game.current_position.board, &king_location).is_none());
667 assert_eq!(
668 piece_at(&game.current_position.board, &Coords { x: 0, y: 1 })
669 .unwrap()
670 .kind,
671 PieceKind::King
672 );
673 }
674
675 #[test]
676 fn scholars_mate() {
677 let mut game = Game::start();
678
679 game.make_move(&ChessMove::PawnSkip(Move {
680 origin: Coords { x: 4, y: 6 },
681 destination: Coords { x: 4, y: 4 },
682 }));
683 assert!(game.current_position.to_move == PieceColor::Black);
684 game.make_move(&ChessMove::PawnSkip(Move {
685 origin: Coords { x: 4, y: 1 },
686 destination: Coords { x: 4, y: 3 },
687 }));
688 game.make_move(&ChessMove::RegularMove(Move {
689 origin: Coords { x: 3, y: 7 },
690 destination: Coords { x: 7, y: 3 },
691 }));
692 assert!(game.current_position.to_move == PieceColor::Black);
693 game.make_move(&ChessMove::RegularMove(Move {
694 origin: Coords { x: 1, y: 0 },
695 destination: Coords { x: 2, y: 2 },
696 }));
697 game.make_move(&ChessMove::RegularMove(Move {
698 origin: Coords { x: 5, y: 7 },
699 destination: Coords { x: 2, y: 4 },
700 }));
701 assert!(game.current_position.to_move == PieceColor::Black);
702 game.make_move(&ChessMove::RegularMove(Move {
703 origin: Coords { x: 6, y: 0 },
704 destination: Coords { x: 5, y: 2 },
705 }));
706 game.make_move(&ChessMove::RegularMove(Move {
707 origin: Coords { x: 7, y: 3 },
708 destination: Coords { x: 5, y: 1 },
709 }));
710 assert!(game.current_position.to_move == PieceColor::Black);
711
712 assert!(game.checkmated == Some(PieceColor::Black));
713 }
714
715 #[test]
716 fn pawn_skip_is_legal() {
717 let position = Position::initial();
718 assert!(position.is_move_legal(&ChessMove::PawnSkip(Move {
719 origin: Coords { x: 4, y: 6 },
720 destination: Coords { x: 4, y: 4 }
721 }),))
722 }
723
724 #[test]
725 fn promotion() {
726 let position = Position::from_fen("8/P7/8/8/8/8/8/8 w - - 0 1");
727 let pawn_location = Coords { y: 1, x: 0 };
728 dbg!(position.legal_moves_from_origin(&pawn_location));
729 position
730 .legal_moves_from_origin(&pawn_location)
731 .iter()
732 .for_each(|chess_move| match chess_move {
733 ChessMove::Promotion(_, _) => (),
734 _ => panic!("expected only promotions, found {:?}", chess_move),
735 });
736 }
737}