vprytz_chess/lib.rs
1//! Author: Vilhelm Prytz <vilhelm@prytznet.se> or <vprytz@kth.se>
2//!
3//! This is a chess library written in Rust.
4//! It is a work in progress and is not yet ready for use.
5//!
6//! # Usage
7//!
8//! # Functions
9//!
10//! | **Function** | **Description** |
11//! | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
12//! | `pub fn new() -> Game` | Initialises a new board with pieces. |
13//! | `pub fn make_move(&mut self, from: String, to: String) -> Option<GameState>` | If the current game state is `InProgress` and the move is legal, move a piece and return the resulting state of the game. |
14//! | **Not yet implemeted** `pub fn set_promotion(&mut self, piece: String) -> ()` | Set the piece type that a peasant becames following a promotion. |
15//! | `pub fn get_game_state(&self) -> GameState` | Get the current game state. |
16//! | `pub fn get_possible_moves(&self, position: String) -> Optional<Vec<String>>` | If a piece is standing on the given tile, return all possible new positions of that piece. Don't forget to the rules for check. _(optional)_ Don't forget to include en passent and castling. |
17//!
18//! # Generate this README
19//!
20//! You need [cargo-readme](https://github.com/livioribeiro/cargo-readme) to generate this README.
21//!
22//! ```bash
23//! cargo readme > README.md
24//! ```
25
26use std::fmt;
27
28const BOARD_SIZE: usize = 8;
29
30/// Possible states of the game is represented using this enum.
31#[derive(Copy, Clone, Debug, PartialEq)]
32pub enum GameState {
33 InProgress,
34 Check,
35 GameOver,
36}
37
38/// Possible colors for pieces is represented using this enum.
39#[derive(Copy, Clone, Debug, PartialEq)]
40pub enum Color {
41 White,
42 Black,
43}
44
45// Possible types of pieces is represented using this enum.
46#[derive(Copy, Clone, Debug, PartialEq)]
47pub enum PieceType {
48 King,
49 Queen,
50 Rook,
51 Bishop,
52 Knight,
53 Pawn,
54}
55
56// copyed example from https://users.rust-lang.org/t/how-can-i-implement-fmt-display-for-enum/24111/2
57impl fmt::Display for PieceType {
58 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
59 match *self {
60 PieceType::King => write!(f, "K "),
61 PieceType::Queen => write!(f, "Q "),
62 PieceType::Rook => write!(f, "R "),
63 PieceType::Bishop => write!(f, "B "),
64 PieceType::Knight => write!(f, "Kn"),
65 PieceType::Pawn => write!(f, "P "),
66 }
67 }
68}
69
70/// Represents a piece on the board.
71/// A piece has a color, a type and a variable that tells if it has moved at least once or not.
72/// A piece can be moved by calling the make_move() function.
73/// # Examples
74/// ```
75/// use vprytz_chess::Piece;
76/// use vprytz_chess::Color;
77/// use vprytz_chess::PieceType;
78/// let mut piece = Piece {
79/// color: vprytz_chess::Color::White,
80/// piece: vprytz_chess::PieceType::King,
81/// untouched: true,
82/// };
83/// ```
84#[derive(Copy, Clone, Debug, PartialEq)]
85pub struct Piece {
86 pub color: Color,
87 pub piece: PieceType,
88 pub untouched: bool,
89}
90
91// copied base example from https://doc.rust-lang.org/rust-by-example/hello/print/print_display.html
92impl fmt::Display for Piece {
93 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
94 write!(f, "{}", self.piece)
95 }
96}
97
98/// Represents a chess game, holding a board (2d array with all pieces) and the current state of the game.
99/// A new game can be created by calling the new() function.
100/// # Examples
101/// ```
102/// use vprytz_chess::Game;
103/// let mut game = Game::new();
104/// // call functions on game, to move pieces and so on
105/// ```
106pub struct Game {
107 state: GameState,
108 board: [[Option<Piece>; BOARD_SIZE]; BOARD_SIZE],
109}
110
111impl Game {
112 /// Initialises a new board with pieces.
113 /// # Examples
114 /// ```
115 /// use vprytz_chess::Game;
116 /// let mut game = Game::new();
117 /// ```
118 pub fn new() -> Game {
119 let mut game = Game {
120 state: GameState::InProgress,
121 board: [[None; BOARD_SIZE]; BOARD_SIZE],
122 };
123 // add pieces
124 game.setup_initial_board();
125
126 // return board
127 game
128 }
129
130 /// Sets up the initial board with pieces, called by new().
131 /// # Examples
132 /// ```
133 /// use vprytz_chess::Game;
134 /// let mut game = Game::new();
135 /// game.setup_initial_board(); // redundant since new() calls this function, but can be called again to "reset" board later on
136 /// ```
137 pub fn setup_initial_board(&mut self) -> () {
138 // calling this will also "reset board"
139 let white_pawn = Some(Piece {
140 color: Color::White,
141 piece: PieceType::Pawn,
142 untouched: true,
143 });
144 let black_pawn = Some(Piece {
145 color: Color::Black,
146 piece: PieceType::Pawn,
147 untouched: true,
148 });
149 let white_rook = Some(Piece {
150 color: Color::White,
151 piece: PieceType::Rook,
152 untouched: true,
153 });
154 let black_rook = Some(Piece {
155 color: Color::Black,
156 piece: PieceType::Rook,
157 untouched: true,
158 });
159 let white_knight = Some(Piece {
160 color: Color::White,
161 piece: PieceType::Knight,
162 untouched: true,
163 });
164 let black_knight = Some(Piece {
165 color: Color::Black,
166 piece: PieceType::Knight,
167 untouched: true,
168 });
169 let white_bishop = Some(Piece {
170 color: Color::White,
171 piece: PieceType::Bishop,
172 untouched: true,
173 });
174 let black_bishop = Some(Piece {
175 color: Color::Black,
176 piece: PieceType::Bishop,
177 untouched: true,
178 });
179 let white_queen = Some(Piece {
180 color: Color::White,
181 piece: PieceType::Queen,
182 untouched: true,
183 });
184 let black_queen = Some(Piece {
185 color: Color::Black,
186 piece: PieceType::Queen,
187 untouched: true,
188 });
189 let white_king = Some(Piece {
190 color: Color::White,
191 piece: PieceType::King,
192 untouched: true,
193 });
194 let black_king = Some(Piece {
195 color: Color::Black,
196 piece: PieceType::King,
197 untouched: true,
198 });
199 self.board[0] = [
200 black_rook,
201 black_knight,
202 black_bishop,
203 black_queen,
204 black_king,
205 black_bishop,
206 black_knight,
207 black_rook,
208 ];
209 self.board[1] = [black_pawn; BOARD_SIZE];
210
211 // empty pieces, this is here to clear any pieces that may have been moved here (reset board)
212 self.board[2] = [None; BOARD_SIZE];
213 self.board[3] = [None; BOARD_SIZE];
214 self.board[4] = [None; BOARD_SIZE];
215 self.board[5] = [None; BOARD_SIZE];
216
217 self.board[6] = [white_pawn; BOARD_SIZE];
218 self.board[7] = [
219 white_rook,
220 white_knight,
221 white_bishop,
222 white_queen,
223 white_king,
224 white_bishop,
225 white_knight,
226 white_rook,
227 ];
228 }
229
230 /// If the current game state is InProgress and the move is legal,
231 /// move a piece and return the resulting state of the game.
232 /// # Examples
233 /// ```
234 /// use vprytz_chess::Game;
235 /// let mut game = Game::new();
236 /// game.make_move("D2".to_string(), "D4".to_string()); // move white pawn at D2 to D4 (will only be allowed if move is legal, checked by get_possible_moves())
237 /// ```
238 pub fn make_move(&mut self, from: String, to: String) -> Option<GameState> {
239 // check if move is legal by checking if "to" position is in get_possible_moves()
240 let possible_moves = self.get_possible_moves(from.to_string());
241
242 // check that "to" is in possible_moves
243 // sourcde https://stackoverflow.com/questions/58368801/how-do-i-check-if-a-thing-is-in-a-vector#58368936
244 if possible_moves.unwrap().contains(&to) {
245 // move piece
246 let from_index = self.pos_to_index(from.to_string());
247 let to_index = self.pos_to_index(to.to_string());
248
249 let piece = self.board[from_index.0][from_index.1];
250
251 // set piece as touched
252 let mut piece = piece.unwrap();
253 piece.untouched = false;
254
255 self.board[from_index.0][from_index.1] = None;
256 self.board[to_index.0][to_index.1] = Some(piece);
257
258 // check if game is over
259 // if self.is_checkmate() {
260 // self.state = GameState::GameOver;
261 // } else if self.is_check() {
262 // self.state = GameState::Check;
263 // }
264
265 return Some(self.get_game_state());
266 } else {
267 return None;
268 }
269 }
270
271 /// Set the piece type that a peasant becames following a promotion.
272 pub fn set_promotion(&mut self, piece: String) -> () {
273 ()
274 }
275
276 /// Get the current game state.
277 /// # Examples
278 /// ```
279 /// use vprytz_chess::Game;
280 /// let mut game = Game::new();
281 /// game.get_game_state(); // returns GameState::InProgress
282 /// ```
283 pub fn get_game_state(&self) -> GameState {
284 self.state
285 }
286
287 /// If a piece is standing on the given tile, return all possible
288 /// new positions of that piece.
289 /// # Examples
290 /// ```
291 /// use vprytz_chess::Game;
292 /// let mut game = Game::new();
293 /// game.get_possible_moves("D2".to_string()); // returns all possible moves for white pawn at D2
294 /// ```
295 /// # TODO
296 /// - check if move is legal (e.g. if king is in check after move)
297 /// - check if pawn can be promoted
298 /// - check if pawn can be captured en passant
299 /// - check for all types of pieces (not yet done)
300 /// # Panics
301 /// Panics if the given position is not on the board.
302 /// # Errors
303 /// Returns None if there is no piece on the given position.
304 pub fn get_possible_moves(&self, postion: String) -> Option<Vec<String>> {
305 let pos = self.pos_to_index(postion);
306
307 // get piece at given position
308 let piece = self.board[pos.0][pos.1];
309
310 let op: i32 = match piece.unwrap().color {
311 Color::White => 1,
312 Color::Black => -1,
313 };
314
315 // different move sets for different PieceTypes
316 match piece {
317 //
318 // PAWN
319 //
320 Some(Piece {
321 piece: PieceType::Pawn,
322 ..
323 }) => {
324 let mut vec: Vec<String> = Vec::with_capacity(5);
325
326 // add possible moves only if they are empty
327 if self.board[(pos.0 as i32 - 1 * op) as usize][pos.1].is_none() {
328 vec.push(self.index_to_pos(((pos.0 as i32 - 1 * op) as usize, pos.1)));
329 // forward (up/down) one
330 }
331
332 if self.board[(pos.0 as i32 - 2 * op) as usize][pos.1].is_none()
333 && self.board[(pos.0 as i32 - 1 * op) as usize][pos.1].is_none()
334 && piece.unwrap().untouched
335 {
336 vec.push(self.index_to_pos(((pos.0 as i32 - 2 * op) as usize, pos.1)));
337 // forward (up/down) two (only if first move!)
338 }
339
340 // attack moves only if the specified positions is occupied by an enemy piece
341 // we check that there is something there and that the piece there actually has a different color
342 // than our piece
343 if self.board[(pos.0 as i32 - 1 * op) as usize][pos.1 + 1].is_some()
344 && self.board[(pos.0 as i32 - 1 * op) as usize][pos.1 + 1]
345 .unwrap()
346 .color
347 != piece.unwrap().color
348 {
349 vec.push(self.index_to_pos(((pos.0 as i32 - 1 * op) as usize, pos.1 + 1)));
350 // forward (up/down) one and right (attack right)
351 }
352
353 if self.board[(pos.0 as i32 - 1 * op) as usize][pos.1 - 1].is_some()
354 && self.board[(pos.0 as i32 - 1 * op) as usize][pos.1 - 1]
355 .unwrap()
356 .color
357 != piece.unwrap().color
358 {
359 vec.push(self.index_to_pos(((pos.0 as i32 - 1 * op) as usize, pos.1 - 1)));
360 // forward (up/down) one and left (attack left)
361 }
362
363 return Some(vec);
364 }
365 //
366 // ROOK
367 //
368 Some(Piece {
369 piece: PieceType::Rook,
370 ..
371 }) => {
372 let vec: Vec<String> = Vec::with_capacity(5);
373
374 // get all possible moves in all directions
375
376 return Some(vec);
377 }
378 //
379 // BISHOP
380 //
381 Some(Piece {
382 piece: PieceType::Bishop,
383 ..
384 }) => {
385 let vec: Vec<String> = Vec::with_capacity(5);
386
387 return Some(vec);
388 }
389 //
390 // KNIGHT
391 //
392 Some(Piece {
393 piece: PieceType::Knight,
394 ..
395 }) => {
396 let mut vec: Vec<String> = Vec::with_capacity(5);
397
398 // make list of positions to check
399 let positions = [
400 // forward and left/right
401 ((pos.0 as i32 - 2 * op), (pos.1 as i32 + 1)), // two pieces "forward" and one right
402 ((pos.0 as i32 - 2 * op), (pos.1 as i32 - 1)), // two pieces "forward" and one left
403 // backwards and left/right
404 ((pos.0 as i32 + 2 * op), (pos.1 as i32 + 1)), // two pieces "backward" and one right
405 ((pos.0 as i32 + 2 * op), (pos.1 as i32 - 1)), // two pieces "backward" and one left
406 // left and up/down
407 ((pos.0 as i32 + 1 * op), (pos.1 as i32 - 2)), // two pieces "left" and one "down"
408 ((pos.0 as i32 - 1 * op), (pos.1 as i32 - 2)), // two pieces "left" and one "up"
409 // right and up/down
410 ((pos.0 as i32 + 1 * op), (pos.1 as i32 + 2)), // two pieces "right" and one "down"
411 ((pos.0 as i32 - 1 * op), (pos.1 as i32 + 2)), // two pieces "right" and one "up"
412 ];
413
414 // check for each position that it is on the board and that it is either empty or occupied by an enemy piece
415 for pos in positions.iter() {
416 if pos.0 < 8
417 && pos.0 >= 0
418 && pos.1 < 8
419 && pos.1 >= 0
420 && (self.board[pos.0 as usize][pos.1 as usize].is_none()
421 || self.board[pos.0 as usize][pos.1 as usize].unwrap().color
422 != piece.unwrap().color)
423 {
424 vec.push(self.index_to_pos((pos.0 as usize, pos.1 as usize)));
425 }
426 }
427
428 return Some(vec);
429 }
430 //
431 // QUEEN
432 //
433 Some(Piece {
434 piece: PieceType::Queen,
435 ..
436 }) => {
437 let vec: Vec<String> = Vec::with_capacity(5);
438
439 return Some(vec);
440 }
441 //
442 // KING
443 //
444 Some(Piece {
445 piece: PieceType::King,
446 ..
447 }) => {
448 let mut vec: Vec<String> = Vec::with_capacity(5);
449
450 // make list of positions to check
451 let positions = [
452 // one step, each direction (including diagonally)
453 ((pos.0 as i32 - 1 * op), (pos.1 as i32 + 1)), // one square "forward" and one right
454 ((pos.0 as i32 - 1 * op), (pos.1 as i32 - 1)), // one square "forward" and one left
455 ((pos.0 as i32 + 1 * op), (pos.1 as i32 + 1)), // one square "backward" and one right
456 ((pos.0 as i32 + 1 * op), (pos.1 as i32 - 1)), // one square "backward" and one left
457 ((pos.0 as i32 + 1 * op), (pos.1 as i32)), // one square "backward"
458 ((pos.0 as i32 - 1 * op), (pos.1 as i32)), // one square "forward"
459 ((pos.0 as i32), (pos.1 as i32 + 1)), // one square "right"
460 ((pos.0 as i32), (pos.1 as i32 - 1)), // one square "left"
461 ];
462
463 // TODO: check that any of the speicifed moves causes an enemy piece to be able to attack the king
464
465 // check for each position that it is on the board and that it is either empty or occupied by an enemy piece
466 for pos in positions.iter() {
467 if pos.0 < 8
468 && pos.0 >= 0
469 && pos.1 < 8
470 && pos.1 >= 0
471 && (self.board[pos.0 as usize][pos.1 as usize].is_none()
472 || self.board[pos.0 as usize][pos.1 as usize].unwrap().color
473 != piece.unwrap().color)
474 {
475 vec.push(self.index_to_pos((pos.0 as usize, pos.1 as usize)));
476 }
477 }
478
479 return Some(vec);
480 }
481 None => return None,
482 }
483 }
484
485 /// Converts a string position on the board to a tuple of the row and column (index for 2d array)
486 /// # Arguments
487 /// * `pos` - A string representing the position on the board
488 /// # Returns
489 /// * A tuple of the row and column (index for 2d array)
490 fn pos_to_index(&self, pos: String) -> (usize, usize) {
491 // convert pos to lowercase non-borrowed string and then chars
492 let pos = pos.to_lowercase();
493 let mut chars = pos.chars();
494
495 // when using "as usize", A will be 97, B will be 98 and so on ...
496 // meaning if we subtract 97 we will get the correct index
497 let y = chars.next().unwrap() as usize - 97;
498
499 // same here, but instead of subtracting 97 we subtract 49
500 // since 49 is the ascii value of 1
501 // our array increases index from top to bottom, so we need to
502 // include "7 -" since chess uses increasing from bottom to top
503 let x = 7 - (chars.next().unwrap() as usize - 49);
504
505 (x, y)
506 }
507
508 // convert index in 2d array to two letter position
509 fn index_to_pos(&self, index: (usize, usize)) -> String {
510 // basically the reverse of pos_to_index
511 // we convert the index to "ascii value" and then to char
512 // we use as u8 since usize cannot be converted to char directly
513 let y = (index.1 + 97) as u8 as char;
514 let x = (7 - index.0 + 49) as u8 as char;
515 format!("{}{}", y.to_uppercase(), x)
516 }
517}
518
519/// Implement print routine for Game.
520///
521/// Output example:
522/// |:----------------------:|
523/// | R Kn B K Q B Kn R |
524/// | P P P P P P P P |
525/// | * * * * * * * * |
526/// | * * * * * * * * |
527/// | * * * * * * * * |
528/// | * * * * * * * * |
529/// | P P P P P P P P |
530/// | R Kn B K Q B Kn R |
531/// |:----------------------:|
532impl fmt::Debug for Game {
533 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
534 /* build board representation string */
535 let mut board = String::new();
536
537 // add top border
538 board.push_str("\n|:----------------------:|");
539
540 // iterate over board and print each piece as letter representation
541 for row in self.board.iter() {
542 board.push_str("\n|");
543 for piece in row.iter() {
544 match piece {
545 Some(p) => board.push_str(&format!(" {}", p)),
546 None => board.push_str(" * "),
547 }
548 }
549 board.push_str("|");
550 }
551
552 // add bottom border
553 board.push_str("\n|:----------------------:|");
554
555 write!(f, "{}", board)
556 }
557}
558
559// --------------------------
560// ######### TESTS ##########
561// --------------------------
562
563#[cfg(test)]
564mod tests {
565 use super::Game;
566 use super::GameState;
567
568 // check test framework
569 #[test]
570 fn it_works() {
571 assert_eq!(2 + 2, 4);
572 }
573
574 // example test
575 // check that game state is in progress after initialisation
576 #[test]
577 fn game_in_progress_after_init() {
578 let game = Game::new();
579
580 println!("{:?}", game);
581
582 assert_eq!(game.get_game_state(), GameState::InProgress);
583 }
584
585 // test converting pos to index
586 #[test]
587 fn convert_pos_to_index() {
588 let game = Game::new();
589
590 assert_eq!(game.pos_to_index("a1".to_string()), (7, 0));
591 assert_eq!(game.pos_to_index("B1".to_string()), (7, 1));
592 assert_eq!(game.pos_to_index("A8".to_string()), (0, 0));
593 assert_eq!(game.pos_to_index("H8".to_string()), (0, 7));
594 assert_eq!(game.pos_to_index("H1".to_string()), (7, 7));
595 }
596
597 // test index to pos
598 #[test]
599 fn convert_index_to_pos() {
600 let game = Game::new();
601
602 assert_eq!(game.index_to_pos((0, 0)), "A8");
603 assert_eq!(game.index_to_pos((7, 0)), "A1");
604 assert_eq!(game.index_to_pos((0, 7)), "H8");
605 assert_eq!(game.index_to_pos((7, 7)), "H1");
606 }
607
608 // test some pawn
609 #[test]
610 fn test_pawn_moves() {
611 let mut game = Game::new();
612
613 // test pawn moves
614 // try white pawn
615 assert_eq!(
616 game.get_possible_moves("D2".to_string()).unwrap().sort(),
617 vec!["D3".to_string(), "D4".to_string(),].sort()
618 );
619 // try black pawn
620 assert_eq!(
621 game.get_possible_moves("D7".to_string()).unwrap().sort(),
622 vec!["D6".to_string(), "D5".to_string(),].sort()
623 );
624
625 // try white pawn at the very left
626 assert_eq!(
627 game.get_possible_moves("D2".to_string()).unwrap().sort(),
628 vec!["D3".to_string(), "D4".to_string(),].sort()
629 );
630
631 // try moving D2 pawn to D4
632 assert_eq!(
633 game.make_move("D2".to_string(), "D4".to_string()),
634 Some(GameState::InProgress)
635 );
636
637 println!("{:?}", game);
638
639 // check that we have right moves for this newly moved pawn
640 assert_eq!(
641 game.get_possible_moves("D4".to_string()).unwrap().sort(),
642 vec!["D5".to_string(), "D3".to_string(),].sort()
643 );
644 // then move a black pawn down, C7 to C5
645 assert_eq!(
646 game.make_move("C7".to_string(), "C5".to_string()),
647 Some(GameState::InProgress)
648 );
649 println!("{:?}", game);
650
651 // now check that the white pawn has right moves, that it can attack the black pawn
652 assert_eq!(
653 game.get_possible_moves("D4".to_string()).unwrap().sort(),
654 vec!["D5".to_string(), "D3".to_string(), "C5".to_string()].sort()
655 );
656 // then attack the black pawn
657 assert_eq!(
658 game.make_move("D4".to_string(), "C5".to_string()),
659 Some(GameState::InProgress)
660 );
661 println!("{:?}", game);
662 }
663 // test some knight moves
664 #[test]
665 fn test_knight_moves() {
666 let mut game = Game::new();
667
668 println!("{:?}", game);
669
670 assert_eq!(
671 game.get_possible_moves("B1".to_string()).unwrap().sort(),
672 vec!["A3".to_string(), "C3".to_string(),].sort()
673 );
674 // move B1 to C3
675 assert_eq!(
676 game.make_move("B1".to_string(), "C3".to_string()),
677 Some(GameState::InProgress)
678 );
679 assert_eq!(
680 game.get_possible_moves("C3".to_string()).unwrap().sort(),
681 vec![
682 "B5".to_string(),
683 "D5".to_string(),
684 "A4".to_string(),
685 "E4".to_string(),
686 ]
687 .sort()
688 );
689 println!("{:?}", game);
690 // move B7 to B5
691 assert_eq!(
692 game.make_move("B7".to_string(), "B5".to_string()),
693 Some(GameState::InProgress)
694 );
695 println!("{:?}", game);
696 // get the pawn with the knight by moving it to B5
697 assert_eq!(
698 game.make_move("C3".to_string(), "B5".to_string()),
699 Some(GameState::InProgress)
700 );
701 println!("{:?}", game);
702 }
703
704 // test some king moves
705 #[test]
706 fn test_king_moves() {
707 use super::Color;
708 use super::Piece;
709 use super::PieceType;
710
711 let mut game = Game::new();
712
713 // assert that king cannot move
714 assert_eq!(game.get_possible_moves("E1".to_string()), Some(vec![]));
715
716 // create fake king in middle of board
717 game.board[3][3] = Some(Piece {
718 piece: PieceType::King,
719 color: Color::White,
720 untouched: true,
721 });
722
723 // assert that this newly created (fake) king can move only one square in any direction
724 assert_eq!(
725 game.get_possible_moves("D5".to_string()).unwrap().sort(),
726 vec![
727 "D6".to_string(),
728 "D4".to_string(),
729 "C6".to_string(),
730 "C5".to_string(),
731 "C4".to_string(),
732 "E6".to_string(),
733 "E5".to_string(),
734 "E4".to_string(),
735 ]
736 .sort()
737 );
738
739 println!("{:?}", game);
740 }
741}