libchessticot/
lib.rs

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}