From 0daece72d3b67c2850e117850f3582a759f5a9d4 Mon Sep 17 00:00:00 2001 From: dece Date: Thu, 4 Jun 2020 19:19:24 +0200 Subject: [PATCH] rules: handle pawn promotion --- src/board.rs | 8 ++--- src/cli.rs | 2 +- src/engine.rs | 2 +- src/notation.rs | 32 +++++++++++++++--- src/rules.rs | 86 +++++++++++++++++++++++++++++++------------------ 5 files changed, 88 insertions(+), 42 deletions(-) diff --git a/src/board.rs b/src/board.rs index 8623744..3af81ae 100644 --- a/src/board.rs +++ b/src/board.rs @@ -180,8 +180,8 @@ pub fn find_king(board: &Board, color: u8) -> Pos { (0, 0) } -/// A movement, with before/after positions. -pub type Move = (Pos, Pos); +/// A movement, with before/after positions and optional promotion. +pub type Move = (Pos, Pos, Option); /// Apply a move `m` to a copy of `board`. pub fn apply(board: &Board, m: &Move) -> Board { @@ -307,12 +307,12 @@ mod tests { set_square(&mut b, &pos("d4"), SQ_WH_N); set_square(&mut b, &pos("f4"), SQ_BL_N); // Move white knight in a position attacked by black knight. - apply_into(&mut b, &((pos("d4"), pos("e6")))); + apply_into(&mut b, &(pos("d4"), pos("e6"), None)); assert_eq!(get_square(&b, &pos("d4")), SQ_E); assert_eq!(get_square(&b, &pos("e6")), SQ_WH_N); assert_eq!(num_pieces(&b), 2); // Sack it with black knight - apply_into(&mut b, &((pos("f4"), pos("e6")))); + apply_into(&mut b, &(pos("f4"), pos("e6"), None)); assert_eq!(get_square(&b, &pos("e6")), SQ_BL_N); assert_eq!(num_pieces(&b), 1); } diff --git a/src/cli.rs b/src/cli.rs index 6dd497f..9c14d19 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -48,7 +48,7 @@ fn get_player_move() -> board::Move { let from = if let Some(s) = get_input("From: ") { board::pos(&s) } else { continue }; let to = if let Some(s) = get_input("To: ") { board::pos(&s) } else { continue }; if board::is_valid_pos(from) && board::is_valid_pos(to) { - return (from, to) + return (from, to, None) // TODO this does not handle promotion. } println!("Bad input."); } diff --git a/src/engine.rs b/src/engine.rs index ab1dcf6..b618f7e 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -289,7 +289,7 @@ fn analyze( let moves = rules::get_player_legal_moves(&state.board, state.color); let mut rng = rand::thread_rng(); let best_move = moves.iter().choose(&mut rng).and_then(|m| Some(*m)); - thread::sleep(time::Duration::from_millis(1000u64)); + thread::sleep(time::Duration::from_millis(300u64)); tx.send(Cmd::TmpBestMove(best_move)).unwrap(); // thread::sleep(time::Duration::from_secs(1)); diff --git a/src/notation.rs b/src/notation.rs index 5d0906b..dfced69 100644 --- a/src/notation.rs +++ b/src/notation.rs @@ -8,11 +8,31 @@ pub fn move_to_string(m: &board::Move) -> String { let mut move_string = String::new(); move_string.push_str(&board::pos_string(&m.0)); move_string.push_str(&board::pos_string(&m.1)); + if let Some(prom) = m.2 { + move_string.push(match prom { + board::SQ_Q => 'q', + board::SQ_B => 'b', + board::SQ_N => 'n', + board::SQ_R => 'r', + _ => panic!("What are you doing? Promote to a legal piece.") + }); + } move_string } pub fn parse_move(m_str: &str) -> board::Move { - (board::pos(&m_str[0..2]), board::pos(&m_str[2..4])) + let prom = if m_str.len() == 5 { + Some(match m_str.as_bytes()[4] { + b'b' => board::SQ_B, + b'n' => board::SQ_N, + b'r' => board::SQ_R, + b'q' => board::SQ_Q, + _ => panic!("What is the opponent doing? This is illegal, I'm out."), + }) + } else { + None + }; + (board::pos(&m_str[0..2]), board::pos(&m_str[2..4]), prom) } pub const FEN_START: &str = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; @@ -53,13 +73,17 @@ mod tests { #[test] fn test_move_to_string() { - assert_eq!(move_to_string(&((0, 0), (3, 3))), "a1d4"); - assert_eq!(move_to_string(&((7, 7), (0, 7))), "h8a8"); + assert_eq!(move_to_string(&((0, 0), (3, 3), None)), "a1d4"); + assert_eq!(move_to_string(&((7, 7), (0, 7), None)), "h8a8"); + assert_eq!(move_to_string(&((7, 6), (7, 7), Some(board::SQ_Q))), "h7h8q"); + assert_eq!(move_to_string(&((7, 6), (7, 7), Some(board::SQ_N))), "h7h8n"); } #[test] fn test_parse_move() { - assert_eq!(parse_move("a1d4"), ((0, 0), (3, 3))); + assert_eq!(parse_move("a1d4"), ((0, 0), (3, 3), None)); + assert_eq!(parse_move("a7a8q"), ((0, 6), (0, 7), Some(board::SQ_Q))); + assert_eq!(parse_move("a7a8r"), ((0, 6), (0, 7), Some(board::SQ_R))); } #[test] diff --git a/src/rules.rs b/src/rules.rs index b3da6c1..8eef8f8 100644 --- a/src/rules.rs +++ b/src/rules.rs @@ -40,21 +40,28 @@ pub fn get_piece_moves(board: &Board, at: &Pos) -> Vec { fn get_pawn_moves(board: &Board, at: &Pos, piece: u8) -> Vec { let (f, r) = *at; let mut moves = vec!(); - let movement: i8 = if is_white(piece) { 1 } else { -1 }; + // Direction: positive for white, negative for black. + let dir: i8 = if is_white(piece) { 1 } else { -1 }; // Check 1 or 2 square forward. let move_len = if (is_white(piece) && r == 1) || (is_black(piece) && r == 6) { 2 } else { 1 }; for i in 1..=move_len { - let forward_r = r + movement * i; - if movement > 0 && forward_r > POS_MAX { + let forward_r = r + dir * i; + if dir > 0 && forward_r > POS_MAX { return moves } - if movement < 0 && forward_r < POS_MIN { + if dir < 0 && forward_r < POS_MIN { return moves } let forward: Pos = (f, forward_r); // If forward square is empty (and we are not jumping over an occupied square), add it. - if is_empty(board, &forward) && (i == 1 || is_empty(board, &(f, forward_r - 1))) { - moves.push((*at, forward)) + if is_empty(board, &forward) && (i == 1 || is_empty(board, &(f, forward_r - dir))) { + // Pawns that get to the opposite rank automatically promote as queens. + let prom = if (dir > 0 && forward_r == POS_MAX) || (dir < 0 && forward_r == POS_MIN) { + Some(SQ_Q) + } else { + None + }; + moves.push((*at, forward, prom)) } // Check diagonals for pieces to attack. if i == 1 { @@ -92,7 +99,7 @@ fn get_bishop_moves(board: &Board, at: &Pos, piece: u8) -> Vec { continue } if is_empty(board, &p) { - moves.push((*at, p)); + moves.push((*at, p, None)); } else { if let Some(m) = move_on_enemy(piece, at, get_square(board, &p), &p) { moves.push(m); @@ -113,7 +120,7 @@ fn get_knight_moves(board: &Board, at: &Pos, piece: u8) -> Vec { continue } if is_empty(board, &p) { - moves.push((*at, p)); + moves.push((*at, p, None)); } else if let Some(m) = move_on_enemy(piece, at, get_square(board, &p), &p) { moves.push(m); } @@ -135,7 +142,7 @@ fn get_rook_moves(board: &Board, at: &Pos, piece: u8) -> Vec { continue } if is_empty(board, &p) { - moves.push((*at, p)); + moves.push((*at, p, None)); } else { if let Some(m) = move_on_enemy(piece, at, get_square(board, &p), &p) { moves.push(m); @@ -164,7 +171,7 @@ fn get_king_moves(board: &Board, at: &Pos, piece: u8) -> Vec { continue } if is_empty(board, &p) { - moves.push((*at, p)); + moves.push((*at, p, None)); } else if let Some(m) = move_on_enemy(piece, at, get_square(board, &p), &p) { moves.push(m); } @@ -177,7 +184,16 @@ fn get_king_moves(board: &Board, at: &Pos, piece: u8) -> Vec { fn move_on_enemy(piece1: u8, pos1: &Pos, piece2: u8, pos2: &Pos) -> Option { let color1 = get_color(piece1); if is_color(piece2, opposite(color1)) { - Some((*pos1, *pos2)) + // Automatic queen promotion for pawns moving to the opposite rank. + let prom = if + is_piece(piece1, SQ_P) && + ((is_white(piece1) && pos2.1 == POS_MAX) || (is_black(piece1) && pos2.1 == POS_MIN)) + { + Some(SQ_Q) + } else { + None + }; + Some((*pos1, *pos2, prom)) } else { None } @@ -231,20 +247,20 @@ mod tests { // Check that a pawn (here white queen's pawn) can move forward if the road is free. set_square(&mut b, &pos("d3"), SQ_WH_P); let moves = get_piece_moves(&b, &pos("d3")); - assert!(moves.len() == 1 && moves.contains( &(pos("d3"), pos("d4")) )); + assert!(moves.len() == 1 && moves.contains( &(pos("d3"), pos("d4"), None) )); // Check that a pawn (here white king's pawn) can move 2 square forward on first move. set_square(&mut b, &pos("e2"), SQ_WH_P); let moves = get_piece_moves(&b, &pos("e2")); assert_eq!(moves.len(), 2); - assert!(moves.contains( &(pos("e2"), pos("e3")) )); - assert!(moves.contains( &(pos("e2"), pos("e4")) )); + assert!(moves.contains( &(pos("e2"), pos("e3"), None) )); + assert!(moves.contains( &(pos("e2"), pos("e4"), None) )); // Check that a pawn cannot move forward if a piece is blocking its path. // 1. black pawn 2 square forward; only 1 square forward available from start pos. set_square(&mut b, &pos("e4"), SQ_BL_P); let moves = get_piece_moves(&b, &pos("e2")); - assert!(moves.len() == 1 && moves.contains( &(pos("e2"), pos("e3")) )); + assert!(moves.len() == 1 && moves.contains( &(pos("e2"), pos("e3"), None) )); // 2. black pawn 1 square forward; no square available. set_square(&mut b, &pos("e3"), SQ_BL_P); let moves = get_piece_moves(&b, &pos("e2")); @@ -257,12 +273,18 @@ mod tests { // Check that a pawn can take a piece diagonally. set_square(&mut b, &pos("f3"), SQ_BL_P); let moves = get_piece_moves(&b, &pos("e2")); - assert!(moves.len() == 1 && moves.contains( &(pos("e2"), pos("f3")) )); + assert!(moves.len() == 1 && moves.contains( &(pos("e2"), pos("f3"), None) )); set_square(&mut b, &pos("d3"), SQ_BL_P); let moves = get_piece_moves(&b, &pos("e2")); assert_eq!(moves.len(), 2); - assert!(moves.contains( &(pos("e2"), pos("f3")) )); - assert!(moves.contains( &(pos("e2"), pos("d3")) )); + assert!(moves.contains( &(pos("e2"), pos("f3"), None) )); + assert!(moves.contains( &(pos("e2"), pos("d3"), None) )); + + // Check that a pawn moving to the last rank leads to queen promotion. + // 1. by simply moving forward. + set_square(&mut b, &pos("a7"), SQ_WH_P); + let moves = get_piece_moves(&b, &pos("a7")); + assert!(moves.len() == 1 && moves.contains( &(pos("a7"), pos("a8"), Some(SQ_Q)) )); } #[test] @@ -274,22 +296,22 @@ mod tests { let moves = get_piece_moves(&b, &pos("d4")); assert_eq!(moves.len(), 13); // Going top-right. - assert!(moves.contains( &(pos("d4"), pos("e5")) )); - assert!(moves.contains( &(pos("d4"), pos("f6")) )); - assert!(moves.contains( &(pos("d4"), pos("g7")) )); - assert!(moves.contains( &(pos("d4"), pos("h8")) )); + assert!(moves.contains( &(pos("d4"), pos("e5"), None) )); + assert!(moves.contains( &(pos("d4"), pos("f6"), None) )); + assert!(moves.contains( &(pos("d4"), pos("g7"), None) )); + assert!(moves.contains( &(pos("d4"), pos("h8"), None) )); // Going bottom-right. - assert!(moves.contains( &(pos("d4"), pos("e3")) )); - assert!(moves.contains( &(pos("d4"), pos("f2")) )); - assert!(moves.contains( &(pos("d4"), pos("g1")) )); + assert!(moves.contains( &(pos("d4"), pos("e3"), None) )); + assert!(moves.contains( &(pos("d4"), pos("f2"), None) )); + assert!(moves.contains( &(pos("d4"), pos("g1"), None) )); // Going bottom-left. - assert!(moves.contains( &(pos("d4"), pos("c3")) )); - assert!(moves.contains( &(pos("d4"), pos("b2")) )); - assert!(moves.contains( &(pos("d4"), pos("a1")) )); + assert!(moves.contains( &(pos("d4"), pos("c3"), None) )); + assert!(moves.contains( &(pos("d4"), pos("b2"), None) )); + assert!(moves.contains( &(pos("d4"), pos("a1"), None) )); // Going top-left. - assert!(moves.contains( &(pos("d4"), pos("c5")) )); - assert!(moves.contains( &(pos("d4"), pos("b6")) )); - assert!(moves.contains( &(pos("d4"), pos("a7")) )); + assert!(moves.contains( &(pos("d4"), pos("c5"), None) )); + assert!(moves.contains( &(pos("d4"), pos("b6"), None) )); + assert!(moves.contains( &(pos("d4"), pos("a7"), None) )); // When blocking sight to one square with friendly piece, lose 2 moves. set_square(&mut b, &pos("b2"), SQ_WH_P); @@ -375,7 +397,7 @@ mod tests { set_square(&mut b, &pos("d6"), SQ_BL_R); assert!(is_attacked(&b, &pos("d4"))); // Move the rook on another file, no more attack. - apply_into(&mut b, &(pos("d6"), pos("e6"))); + apply_into(&mut b, &(pos("d6"), pos("e6"), None)); assert!(!is_attacked(&b, &pos("d4"))); } }