Skip to content

Commit

Permalink
Added Promotion to engine
Browse files Browse the repository at this point in the history
  • Loading branch information
veeso committed Aug 8, 2021
1 parent 0c0911f commit 45f048c
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 20 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Released on ??

- **New features**:
- Added `FromStr` for `Position` (e.g. `assert_eq!(Position::from_str("A1").ok().unwrap() == A1)`)
- Added `get_taken_piece` to `Board` to get the last taken piece after a turn
- Added `Promotion` to engine. (Before it just promoted pawn on last rank to queen, but that's incorrect, since sometimes can prevent checkmates and causing stalemates)
- blah blah
- **API changes**:
- Renamed `print_rating_bar` to `get_rating`. Now it returns the scores as percentage `(white, black)` and not a string
Expand Down
124 changes: 106 additions & 18 deletions src/board/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ pub struct Board {
en_passant: Option<Position>,
/// tracks eventually a taken piece on the last turn
taken_piece: Option<Piece>,
// TODO: promotion: Option<Position>,
/// tracks eventually the possibility to promote a pawn
promotion: Option<Position>,
/// castling rights for white player
white_castling_rights: CastlingRights,
/// castling rights for black player
Expand Down Expand Up @@ -91,6 +92,7 @@ impl Board {
squares: [Square::empty(); 64],
en_passant: None,
taken_piece: None,
promotion: None,
white_castling_rights: CastlingRights::default(),
black_castling_rights: CastlingRights::default(),
turn: WHITE,
Expand Down Expand Up @@ -692,36 +694,62 @@ impl Board {
/// Panics if a promotion must be performed first
pub fn play_move(&self, m: Move) -> MoveResult {
let current_color = self.get_turn();

// TODO: panic if promotion is available
// Panic if promotion is available
if let Some(pos) = self.promotion {
panic!(
"A promotion at '{}' must be performed before moving a piece",
pos
);
}
// Make move
if m == Move::Resign {
MoveResult::Victory(!current_color)
} else if self.is_legal_move(m, current_color) {
// Apply move and change turn
let next_turn = self.apply_move(m).change_turn();
let next_turn: Board = self.apply_move(m);
// If is checkmate, return victory
if next_turn.is_checkmate() {
if next_turn.change_turn().is_checkmate() {
MoveResult::Victory(current_color)
} else if next_turn.is_stalemate() {
} else if next_turn.change_turn().is_stalemate() {
// Check stalemate
MoveResult::Stalemate
} else {
// TODO: check promotion
MoveResult::Continuing(next_turn)
// check for promotion
let next_turn: Board = next_turn.check_available_pawn_promotion();
// If there's a promotion available, return `Promote`; otherwise return `Continuing` changing player's turn
match next_turn.promotion {
Some(pos_promotion) => MoveResult::Promote(next_turn, pos_promotion),
None => MoveResult::Continuing(next_turn.change_turn()),
}
}
} else {
MoveResult::IllegalMove(m)
}
}

/* TODO: implement
/// ### promote
///
/// Promote the pawn on the last line.
/// Panics if there is no pawn to promote.
/// Returns the updated board
pub fn promote(&self, promote: Promotion) -> Board {}
*/
pub fn promote(&self, promotion: Promotion) -> Board {
let mut result = *self;
match result.promotion.take() {
Some(pos) => {
let color: Color = result.get_turn();
let promotion: Piece = match promotion {
Promotion::Bishop => Piece::Bishop(color, pos),
Promotion::Knight => Piece::Knight(color, pos),
Promotion::Queen => Piece::Queen(color, pos),
Promotion::Rook => Piece::Rook(color, pos),
};
result.add_piece(promotion);
// Change turn and return
result.change_turn()
}
None => panic!("There's no promotion available"),
}
}

// -- private

Expand Down Expand Up @@ -757,14 +785,9 @@ impl Board {
}

let from_square = result.get_square(from);
if let Some(mut piece) = from_square.get_piece() {
if let Some(piece) = from_square.get_piece() {
*from_square = Square::empty();

// TODO: remove this block
if piece.is_pawn() && (to.get_row() == 0 || to.get_row() == 7) {
piece = Piece::Queen(piece.get_color(), piece.get_pos());
}

// Check en passant
if piece.is_starting_pawn() && (from.get_row() - to.get_row()).abs() == 2 {
result.en_passant = Some(to.pawn_back(piece.get_color()))
Expand Down Expand Up @@ -861,6 +884,24 @@ impl Board {
result
}

/// ### check_available_pawn_promotion
///
/// Check whether there is a pawn promotion available
fn check_available_pawn_promotion(mut self) -> Self {
let mut promoting_pawn: Option<Position> = None;
// Search for a pawn which can be promoted
for square in self.squares.iter() {
if let Some(piece) = square.get_piece() {
if piece.is_promoting_pawn() && piece.get_color() == self.get_turn() {
promoting_pawn = Some(piece.get_pos());
}
}
}
// Set promotion
self.promotion = promoting_pawn;
self
}

/// ### minimax
///
/// Perform minimax on a certain position, and get the minimum or maximum value
Expand Down Expand Up @@ -1690,7 +1731,32 @@ mod test {
.piece(Piece::King(WHITE, E7))
.build();
assert_eq!(board.play_move(Move::Piece(E7, F8)), MoveResult::Stalemate);
// TODO: promote
// Verify promotion
let board: Board = BoardBuilder::default()
.enable_castling()
.piece(Piece::Pawn(WHITE, B7))
.piece(Piece::King(WHITE, E1))
.piece(Piece::King(BLACK, H8))
.build();
let mut test_board: Board = board.clone().apply_move(Move::Piece(B7, B8)); // Turn won't change
test_board.promotion = Some(B8);
assert_eq!(
board.play_move(Move::Piece(B7, B8)),
MoveResult::Promote(test_board, B8)
);
}

#[test]
#[should_panic]
fn play_move_promotion_available() {
let mut board: Board = BoardBuilder::default()
.enable_castling()
.piece(Piece::Pawn(WHITE, B8))
.piece(Piece::King(WHITE, E1))
.piece(Piece::King(BLACK, H8))
.build();
board.promotion = Some(B8);
board.play_move(Move::Piece(E1, E2));
}

#[test]
Expand All @@ -1714,6 +1780,28 @@ mod test {
assert_eq!(board.get_taken_piece(), None);
}

#[test]
fn promote() {
let mut board: Board = BoardBuilder::default()
.enable_castling()
.piece(Piece::Pawn(WHITE, B8))
.piece(Piece::King(WHITE, E1))
.piece(Piece::King(BLACK, H8))
.build();
board.promotion = Some(B8);
assert_eq!(board.get_turn(), WHITE);
let board: Board = board.promote(Promotion::Queen);
assert_eq!(board.get_piece(B8).unwrap(), Piece::Queen(WHITE, B8));
assert_eq!(board.get_turn(), BLACK);
}

#[test]
#[should_panic]
fn promote_none() {
let board: Board = Board::default();
board.promote(Promotion::Queen);
}

#[test]
fn fmt_board() {
Board::default().to_string();
Expand Down
15 changes: 13 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@
//! ```rust,no_run
//! extern crate harmon;
//!
//! use harmon::{Board, MoveResult, Move};
//! use harmon::{Board, MoveResult, Move, Promotion};
//!
//! fn main() {
//! let board = Board::default();
//! let mut board = Board::default();
//!
//! // Get the best move with 4 moves of lookahead
//! let (best_move, _) = board.get_best_next_move(4);
Expand All @@ -60,6 +60,7 @@
//! MoveResult::Promote(next_board, pos) => {
//! println!("{}", next_board);
//! println!("Pawn promotion available at {}", pos);
//! board = next_board.promote(Promotion::Queen);
//! }
//!
//! MoveResult::Victory(winner) => {
Expand Down Expand Up @@ -100,6 +101,16 @@
//!
//! TODO: fill
//!
//! ## Promoting pawns
//!
//! Whenever you move one of your pawns to the last rank, a `Promotion` variant will be returned.
//! Promotion is handled "asynchronously", because it is handled directly when moving the piece, but requires you to call another
//! function to promote.
//! Once the `Promotion` variant is returned, you have to call the `promote(Promotion)` function to promote the pawn.
//! At this point you can keep playing moves as usual.
//! Be careful though, if you don't promote when a `Promotion` is returned after playing move and, instead, you try to move another piece,
//! the engine will panic.
//!

#![doc(html_playground_url = "https://play.rust-lang.org")]
#![doc(
Expand Down
22 changes: 22 additions & 0 deletions src/piece.rs
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,20 @@ impl Piece {
}
}

/// ### is_promoting_pawn
///
/// Is this piece a pawn which can be promoted?
///
/// This means the pawn is at the last rank
#[inline]
pub fn is_promoting_pawn(&self) -> bool {
if let Self::Pawn(c, pos) = self {
pos.is_promoting_pawn(*c)
} else {
false
}
}

/// ### is_queenside_rook
///
/// Is this piece in the starting position for the queenside rook?
Expand Down Expand Up @@ -905,6 +919,14 @@ mod test {
assert_eq!(Piece::Bishop(BLACK, C7).is_starting_pawn(), false);
}

#[test]
fn is_promoting_pawn() {
assert_eq!(Piece::Pawn(WHITE, G8).is_promoting_pawn(), true);
assert_eq!(Piece::Pawn(WHITE, A1).is_promoting_pawn(), false);
assert_eq!(Piece::Pawn(BLACK, A1).is_promoting_pawn(), true);
assert_eq!(Piece::Pawn(BLACK, G8).is_promoting_pawn(), false);
}

#[test]
fn is_queenside_rook() {
assert_eq!(Piece::Rook(WHITE, A1).is_queenside_rook(), true);
Expand Down
19 changes: 19 additions & 0 deletions src/position.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,17 @@ impl Position {
}
}

/// ### is_promoting_pawn
///
/// Is this pawn on the last rank for the respective player?
#[inline]
pub fn is_promoting_pawn(&self, color: Color) -> bool {
match color {
WHITE => self.row == 7,
BLACK => self.row == 0,
}
}

/// ### is_kingside_rook
///
/// Is this the starting position of the kingside rook?
Expand Down Expand Up @@ -713,6 +724,14 @@ mod test {
assert_eq!(D6.is_starting_pawn(BLACK), false);
}

#[test]
fn is_promoting_pawn() {
assert_eq!(G8.is_promoting_pawn(WHITE), true);
assert_eq!(A1.is_promoting_pawn(WHITE), false);
assert_eq!(A1.is_promoting_pawn(BLACK), true);
assert_eq!(G8.is_promoting_pawn(BLACK), false);
}

#[test]
fn is_kingside_rook() {
assert_eq!(H1.is_kingside_rook(WHITE), true);
Expand Down

0 comments on commit 45f048c

Please sign in to comment.