use std::{time::{Instant, Duration}, f32::INFINITY, cmp::Ordering}; use crate::{bitboard::pop_count, board::*}; use super::ttable::{NodeType, TranspositionTableItem}; static A_FILE: Bitboard = 0x0101010101010101; const VALUE_WIN: f32 = 20_000.0; #[derive(Debug, Default, PartialEq)] pub struct PerftResult { leaf_nodes: u64, captures: u64, en_passants: u64, castles: u64, checks: u64, } const PAWN_BONUS: [f32; 64] = [ // A B C D E F G H 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.50, 0.50, 0.50, 0.50, 0.50, 0.50, 0.50, 0.50, 0.10, 0.10, 0.20, 0.30, 0.30, 0.20, 0.10, 0.10, 0.05, 0.05, 0.10, 0.25, 0.25, 0.10, 0.05, 0.05, 0.00, 0.00, 0.00, 0.20, 0.20, 0.00, 0.00, 0.00, 0.05, -0.05, -0.10, 0.00, 0.00, -0.10, -0.05, 0.05, 0.05, 0.10, 0.10, -0.20, -0.20, 0.10, 0.10, 0.05, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00 ]; const KNIGHT_BONUS: [f32; 64] = [ // A B C D E F G H -0.50, -0.40, -0.30, -0.30, -0.30, -0.30, -0.40, -0.50, -0.40, -0.20, 0.00, 0.00, 0.00, 0.00, -0.20, -0.40, -0.30, 0.00, 0.10, 0.15, 0.15, 0.10, 0.00, -0.30, -0.30, 0.05, 0.15, 0.20, 0.20, 0.15, 0.05, -0.30, -0.30, 0.00, 0.15, 0.20, 0.20, 0.15, 0.00, -0.30, -0.30, 0.05, 0.10, 0.15, 0.15, 0.10, 0.05, -0.30, -0.40, -0.20, 0.00, 0.05, 0.05, 0.00, -0.20, -0.40, -0.50, -0.40, -0.30, -0.30, -0.30, -0.30, -0.40, -0.50, ]; const BISHOP_BONUS: [f32; 64] = [ // A B C D E F G H -0.20, -0.10, -0.10, -0.10, -0.10, -0.10, -0.10, -0.20, -0.10, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, -0.10, -0.10, 0.00, 0.05, 0.10, 0.10, 0.05, 0.00, -0.10, -0.10, 0.05, 0.05, 0.10, 0.10, 0.05, 0.05, -0.10, -0.10, 0.00, 0.10, 0.10, 0.10, 0.10, 0.00, -0.10, -0.10, 0.10, 0.10, 0.10, 0.10, 0.10, 0.10, -0.10, -0.10, 0.25, 0.00, 0.00, 0.00, 0.00, 0.25, -0.10, -0.20, -0.10, -0.10, -0.10, -0.10, -0.10, -0.10, -0.20, ]; const ROOK_BONUS: [f32; 64] = [ // A B C D E F G H 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.05, 0.30, 0.30, 0.30, 0.30, 0.30, 0.30, 0.05, -0.05, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, -0.05, -0.05, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, -0.05, -0.05, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, -0.05, -0.05, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, -0.05, -0.05, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, -0.05, 0.00, 0.00, 0.00, 0.05, 0.05, 0.00, 0.00, 0.00 ]; const QUEEN_BONUS: [f32; 64] = [ // A B C D E F G H -0.20, -0.10, -0.10, -0.05, -0.05, -0.10, -0.10, -0.20, -0.10, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, -0.10, -0.10, 0.00, 0.05, 0.05, 0.05, 0.05, 0.00, -0.10, -0.05, 0.00, 0.05, 0.05, 0.05, 0.05, 0.00, -0.05, 0.00, 0.00, 0.05, 0.05, 0.05, 0.05, 0.00, -0.05, -0.10, 0.05, 0.05, 0.05, 0.05, 0.05, 0.00, -0.10, -0.10, 0.00, 0.05, 0.00, 0.00, 0.00, 0.00, -0.10, -0.20, -0.10, -0.10, -0.05, -0.05, -0.10, -0.10, -0.20 ]; const KING_BONUS: [f32; 64] = [ // A B C D E F G H -0.30, -0.40, -0.40, -0.50, -0.50, -0.40, -0.40, -0.30, -0.30, -0.40, -0.40, -0.50, -0.50, -0.40, -0.40, -0.30, -0.30, -0.40, -0.40, -0.50, -0.50, -0.40, -0.40, -0.30, -0.30, -0.40, -0.40, -0.50, -0.50, -0.40, -0.40, -0.30, -0.20, -0.30, -0.30, -0.40, -0.40, -0.30, -0.30, -0.20, -0.10, -0.20, -0.20, -0.20, -0.20, -0.20, -0.20, -0.10, 0.20, 0.20, 0.00, -0.10, -0.10, 0.00, 0.20, 0.20, 0.20, 0.10, 0.30, -0.20, 0.00, 0.10, 0.30, 0.20 ]; impl Board { pub fn perft(&mut self, depth: u8, print: bool) -> PerftResult { let mut result = PerftResult::default(); if depth == 0 { result.leaf_nodes = 1; return result; } let color = self.color(); let moves = self.generate_pseudolegal_moves(color); if print { println!("Running perft for depth {}. Color to move is {:?}\n{} moves available", depth, color, moves.len()); println!("{} moves available", moves.len()); } for mov in moves { let ep_target_before = self.ep_target.clone(); let castling_rights_before = self.castling_rights.clone(); let hash_before = self.hash.clone(); let captured_piece = self.make_move(mov); // King can not be in check after our own move if !self.is_king_in_check(color) { if depth == 1 { match mov.kind { MoveKind::Capture => { result.captures += 1; } MoveKind::EnPassant => { result.en_passants += 1; result.captures += 1; } MoveKind::Castle => { result.castles += 1; } _ => {} } if self.is_king_in_check(color.flip()) { result.checks += 1; } } if print { println!("{:?}", mov); self.print(); } let subtree_result = self.perft(depth - 1, print); result.leaf_nodes += subtree_result.leaf_nodes; result.captures += subtree_result.captures; result.checks += subtree_result.checks; result.castles += subtree_result.castles; result.en_passants += subtree_result.en_passants; } self.unmake_move(mov, captured_piece, ep_target_before, castling_rights_before, hash_before); } if print { println!("Found {} leaf nodes in this subtree (depth {})", result.leaf_nodes, depth); } result } /// Count player pieces' material, giving bonus for pieces standing well pub fn material(&self, color: Color) -> f32 { let mut material = 0f32; for (piece_index, bitboard) in self.pieces_by_color(color).iter().enumerate() { let piece_type = PieceType::from(piece_index); let bonus_table = match piece_type { PieceType::Pawn => PAWN_BONUS, PieceType::Knight => KNIGHT_BONUS, PieceType::Bishop => BISHOP_BONUS, PieceType::Rook => ROOK_BONUS, PieceType::King => KING_BONUS, PieceType::Queen => QUEEN_BONUS, _ => panic!("Unreachable") }; material += serialize_bitboard(*bitboard).iter().fold(0., |acc, square| { acc + piece_type.static_eval() + bonus_table[ match color { Color::White => square.mirror() as usize, Color::Black => *square as usize, } ] }); } material } /// Returns sum of the doubled, blocked and isolated pawns /// The greater result is, the worse is the pawn structure pub fn pawn_structure_penalty(&self, color: Color) -> f32 { let mut result = 0.0; let pawns = match color { Color::White => self.pieces[PieceType::Pawn as usize], Color::Black => self.pieces[PieceType::PawnBlack as usize], }; for file in 0..8 { let file_mask = A_FILE << file; let pawns_on_file = pop_count(pawns & file_mask) as f32; // Doubled pawns (-1 because one pawn on a file is ok) result += (pawns_on_file - 1.).max(0.0); // Isolated pawns (no pawns on neighbor files) if [ A_FILE << (file - 1).max(0), // File to the left (if any) A_FILE << (file + 1).min(7), // File to the right (if any) ].iter().all(|file| file & pawns == 0) { result += pawns_on_file; } } // Blocked pawns let blocked_mask = match color { Color::White => self.occupancy >> 8, Color::Black => self.occupancy << 8, }; result += pop_count(pawns & blocked_mask) as f32; result } /// Returns the weighted sum of distances from attacking pieces to a king /// The higher this value, the safer is the king pub fn king_tropism(&self, color: Color) -> f32 { let mut result = 0.0; let king_square = bitscan(match color { Color::White => self.pieces[PieceType::King as usize], Color::Black => self.pieces[PieceType::KingBlack as usize], }); for (piece_type, bitboard) in self.pieces_by_color(color.flip()).iter().enumerate() { if piece_type != PieceType::King as usize && piece_type != PieceType::Pawn as usize { for square in serialize_bitboard(*bitboard) { let distance = (king_square.rank() as f32 - square.rank() as f32).abs() + (king_square.file() as f32 - square.file() as f32).abs(); result += distance / PieceType::from(piece_type).static_eval(); } } } result } /// Evaluate a position relative to the current player pub fn evaluate(&self) -> f32 { let color = self.color(); let opponent_color = color.flip(); let mobility_advantage = self.mobility(color) - self.mobility(opponent_color); let opponent_material = self.material(opponent_color); let material_advantage = self.material(color) - opponent_material; let pawn_structure_penalty = self.pawn_structure_penalty(color) - self.pawn_structure_penalty(opponent_color); let king_tropism_penalty = self.king_tropism(color) - self.king_tropism(opponent_color); material_advantage + 0.15 * mobility_advantage - 0.3 * pawn_structure_penalty + king_tropism_penalty * (opponent_material / 40.0) * 0.07 } /// Evaluate move for move ordering, prioritizing efficient captures /// where low-value pieces capture high-value pieces fn eval_move(&self, m: Move) -> f32 { if m.is_tactical() { let [source_eval, target_eval] = [m.source, m.target] .map(|sq| self.piece_by_square(sq)) .map(|p| { match p { Some(p) => p.static_eval(), None => 0., } }); return 2. * target_eval - source_eval } 0.0 } /// Find current transposition in Transposition Table pub fn transposition(&self) -> Option { match self.transposition_table[(self.hash % TTABLE_SIZE) as usize] { Some(item) => { if item.hash == self.hash { return Some(item) } None } None => None } } pub fn order_moves(&self, moves: Vec, killer_moves: Vec) -> Vec { let mut moves_with_eval: Vec<(Move, f32)> = moves .iter() .map(|m| (*m, self.eval_move(*m))) .collect(); moves_with_eval.sort_unstable_by(|(a, a_eval), (b, b_eval)| { if *a_eval == 0.0 && *b_eval == 0.0 { // Prioritize equal captures over non-captures if a.is_tactical() && !b.is_tactical() { return Ordering::Less } if b.is_tactical() && !a.is_tactical() { return Ordering::Greater } } a_eval.total_cmp(b_eval).reverse() }); let mut ordered_moves: Vec = moves_with_eval.iter().map(|(m, _)| *m).collect(); // Insert killer moves after winning captures let equal_capture_index = match moves_with_eval.iter().position(|(m, eval)| m.is_tactical() && *eval == 0.0) { Some(x) => x, None => 0, }; for killer in killer_moves { match ordered_moves.iter().position(|m| *m == killer) { Some(index) => { let mov = ordered_moves.remove(index); ordered_moves.insert(equal_capture_index, mov); } None => {} }; } match self.transposition() { Some(transposition) => ordered_moves.insert(0, transposition.mov), None => {}, } ordered_moves } pub fn negamax_search(&mut self, mut alpha: f32, beta: f32, depth_left: u8, parent_killers: &mut Vec, deadline: Instant) -> (f32, Vec) { let mut principal_variation = Vec::new(); let mut killer_moves = Vec::new(); let color = self.color(); let is_in_check = self.is_king_in_check(color); match self.transposition() { Some(transposition) => { if transposition.depth == depth_left { match transposition.node_type { NodeType::PV => { // PV-nodes have exact score principal_variation.push(transposition.mov); return (transposition.score, principal_variation); } NodeType::Cut => { if transposition.score >= beta { principal_variation.push(transposition.mov); return (beta, principal_variation); } } NodeType::All => { if transposition.score <= alpha { principal_variation.push(transposition.mov); return (alpha, principal_variation); } } } } } None => {}, } let mut moves = self.generate_pseudolegal_moves(color); moves = self.order_moves(moves, parent_killers.to_vec()); if depth_left == 0 { return (self.quiscence(alpha, beta), principal_variation); } let mut should_pv_search = true; // = !should_apply_LMR let mut legal_move_found = false; for mov in moves { let ep_target_before = self.ep_target.clone(); let castling_rights_before = self.castling_rights.clone(); let hash_before = self.hash.clone(); let captured_piece = self.make_move(mov); if !self.is_king_in_check(color) { legal_move_found = true; let (mut score, mut subtree_pv) = if should_pv_search { // Assume PV-node is high in the list (if move ordering is good) self.negamax_search(-beta, -alpha, depth_left - 1, &mut killer_moves, deadline) } else { // Apply late move reductions let reduction = if depth_left > 2 && !is_in_check && !mov.is_tactical() { 1 } else { 0 }; // After we have PV-node (that raised alpha) all other nodes will be searched // with zero-window first to confirm this assumption. // Also (optionally) apply Late Move Reductions // TODO: changing 0.001 -> 0.0001 leads to a weird bug let result = self.negamax_search(-(alpha + 0.001), -alpha, depth_left - 1 - reduction, &mut killer_moves, deadline); // In case some of the other nodes raises alpha, then it's true PV node now, // let's research with full window to find its exact value if -result.0 > alpha { self.negamax_search(-beta, -alpha, depth_left - 1, &mut killer_moves, deadline) } else { result } }; score *= -1.; self.unmake_move(mov, captured_piece, ep_target_before, castling_rights_before, hash_before); if score >= beta { self.transposition_table[(self.hash % TTABLE_SIZE) as usize] = Some(TranspositionTableItem { hash: self.hash, mov, depth: depth_left, // TODO: should be actual depth searched node_type: NodeType::Cut, score, }); if mov.kind == MoveKind::Quiet { match parent_killers.iter().find(|m| **m == mov) { None => parent_killers.push(mov), Some(..) => {}, } } return (beta, principal_variation); } if score > alpha { alpha = score; should_pv_search = false; // Once we have PV-node we can start zero-window searching principal_variation = Vec::with_capacity(depth_left as usize); principal_variation.push(mov); principal_variation.append(&mut subtree_pv); self.transposition_table[(self.hash % TTABLE_SIZE) as usize] = Some(TranspositionTableItem { hash: self.hash, mov, depth: depth_left, // TODO: should be actual depth searched node_type: NodeType::PV, score, }); } else if self.transposition().is_none() { self.transposition_table[(self.hash % TTABLE_SIZE) as usize] = Some(TranspositionTableItem { hash: self.hash, mov, depth: depth_left, // TODO: should be actual depth searched node_type: NodeType::All, score, }); } } else { self.unmake_move(mov, captured_piece, ep_target_before, castling_rights_before, hash_before); } // Could not finish in time, return what we have so far if Instant::now() > deadline { break; } } if !legal_move_found { if is_in_check { return (-VALUE_WIN, principal_variation); } } (alpha, principal_variation) } pub fn quiscence(&mut self, mut alpha: f32, beta: f32) -> f32 { let color = self.color(); let mut moves = self.generate_pseudolegal_moves(color); moves = self.order_moves(moves, Vec::new()); if !self.is_king_in_check(color) { // If we are not in check, we can evaluate stand pat let stand_pat = self.evaluate(); if stand_pat >= beta { return beta; } if alpha < stand_pat { alpha = stand_pat; } // If we are not in check, we can only search tactical moves moves = moves .iter() .filter(|m| m.is_tactical()) .map(|m| *m) .collect() } let mut legal_move_found = false; for mov in moves { let ep_target_before = self.ep_target.clone(); let castling_rights_before = self.castling_rights.clone(); let hash_before = self.hash.clone(); let captured_piece = self.make_move(mov); if !self.is_king_in_check(color) { legal_move_found = true; let evaluation = -self.quiscence(-beta, -alpha); self.unmake_move(mov, captured_piece, ep_target_before, castling_rights_before, hash_before); if evaluation >= beta { return beta; // Fail-hard beta-cutoff } if evaluation > alpha { alpha = evaluation; } } else { self.unmake_move(mov, captured_piece, ep_target_before, castling_rights_before, hash_before); } } if !legal_move_found { if self.is_king_in_check(color) { return -VALUE_WIN } } alpha } pub fn iterative_deepening(&mut self, max_depth: u8, duration: Duration, print: bool) -> (f32, Vec) { let start = Instant::now(); let deadline = start + duration; let mut result = None; let mut depth = 1; let mut alpha = -INFINITY; let mut beta = INFINITY; let window_size = 0.25; let mut gradual_widening_counter = 0; let mut root_killers: Vec = Vec::new(); while depth <= max_depth { if print { println!("\nSearching depth({}) in the window {:?}", depth, (alpha, beta)); } let search_result = self.negamax_search(alpha, beta, depth, &mut root_killers, deadline); if search_result.0.abs() >= VALUE_WIN { return search_result } if Instant::now() > deadline { if print { println!("Aborting..."); } break; } if print { println!("Finished depth({}) {:?} [{:?} left]", depth, search_result, deadline - Instant::now()); } if search_result.0 <= alpha { // Alpha-cutoff if print { println!("Alpha cutoff {} <= {:?}", search_result.0, (alpha, beta)); } gradual_widening_counter += 1; beta = alpha + window_size * 0.1; alpha = search_result.0 - window_size * 2.0f32.powi(gradual_widening_counter); continue; } if search_result.0 >= beta { // Beta-cutoff if print { println!("Beta cutoff {:?} <= {}", (alpha, beta), search_result.0); } gradual_widening_counter += 1; alpha = beta - window_size * 0.1; beta = search_result.0 + window_size * 2.0f32.powi(gradual_widening_counter); continue; } if search_result.1.len() > 0 { depth += 1; gradual_widening_counter = 0; alpha = search_result.0 - window_size; beta = search_result.0 + window_size; } else { panic!("Why the fuck no moves?"); } result = Some(search_result); } match result { Some(r) => return r, None => panic!("Could not find a move in time"), } } } #[cfg(test)] mod tests { use std::time::Duration; use crate::{board::{Board, engine::{PerftResult, KING_BONUS}, Color}, square::Square, moves::{Move, MoveKind}}; use super::VALUE_WIN; #[test] fn perft() { let mut board = Board::new(); assert_eq!(board.perft(0, false), PerftResult { leaf_nodes: 1, captures: 0, en_passants: 0, castles: 0 , checks: 0 }); assert_eq!(board.perft(1, false), PerftResult { leaf_nodes: 20, captures: 0, en_passants: 0, castles: 0 , checks: 0 }); assert_eq!(board.perft(2, false), PerftResult { leaf_nodes: 400, captures: 0, en_passants: 0, castles: 0 , checks: 0 }); assert_eq!(board.perft(3, false), PerftResult { leaf_nodes: 8902, captures: 34, en_passants: 0, castles: 0 , checks: 12 }); assert_eq!(board.perft(4, false), PerftResult { leaf_nodes: 197281, captures: 1576, en_passants: 0, castles: 0 , checks: 469 }); // assert_eq!(board.perft(5, false), PerftResult { leaf_nodes: 4865609, captures: 82719, en_passants: 258, castles: 0, checks: 27351 }); } #[test] fn position_perft() { let fen = String::from("r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - "); let mut board = Board::from_FEN(fen); assert_eq!(board.perft(0, false), PerftResult { leaf_nodes: 1, captures: 0, en_passants: 0, castles: 0 , checks: 0 }); assert_eq!(board.perft(1, false), PerftResult { leaf_nodes: 48, captures: 8, en_passants: 0, castles: 2 , checks: 0 }); assert_eq!(board.perft(2, false), PerftResult { leaf_nodes: 2039, captures: 351, en_passants: 1, castles: 91 , checks: 3 }); assert_eq!(board.perft(3, false), PerftResult { leaf_nodes: 97862, captures: 17102, en_passants: 45, castles: 3162, checks: 993 }); // assert_eq!(board.perft(4, false), PerftResult { leaf_nodes: 4085603, captures: 757163, en_passants: 1929, castles: 128013, checks: 25523 }); } #[test] fn endgame_perft() { let fen = String::from("8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - "); let mut board = Board::from_FEN(fen); assert_eq!(board.perft(1, false), PerftResult { leaf_nodes: 14, captures: 1, en_passants: 0, castles: 0 , checks: 2 }); assert_eq!(board.perft(2, false), PerftResult { leaf_nodes: 191, captures: 14, en_passants: 0, castles: 0 , checks: 10 }); // assert_eq!(board.perft(3, false), PerftResult { leaf_nodes: 2812, captures: 209, en_passants: 2, castles: 0 , checks: 267 }); } #[test] fn material() { let board = Board::new(); assert_eq!(board.material(Color::Black), board.material(Color::White)); } #[test] fn checkmate() { let fen = String::from("2kr1b1r/pp1npppp/2p1bn2/7q/5B2/2NB1Q1P/PPP1N1P1/2KR3R w - - 0 1"); let mut board = Board::from_FEN(fen); let (score, pv) = board.iterative_deepening(8, Duration::from_secs(15), true); assert_eq!(score, VALUE_WIN); assert_eq!(pv, vec![ Move { source: Square::F3, target: Square::C6, kind: MoveKind::Capture }, Move { source: Square::B7, target: Square::C6, kind: MoveKind::Capture }, Move { source: Square::D3, target: Square::A6, kind: MoveKind::Quiet }, ]); } #[test] fn stupid_knight_sac() { let fen = String::from("r3k1r1/pp3ppp/1q6/2ppPn2/6P1/1PPP1P2/P1N3KP/R2QR3 b - - 0 18"); let mut board = Board::from_FEN(fen); board.ply += 1; // TODO: remove me when FEN parsing includes side to move let (_, pv) = board.iterative_deepening(6, Duration::from_secs(60), true); assert_eq!( pv[0], Move { source: Square::F5, target: Square::H4, kind: MoveKind::Quiet }, "You should save this poor knight from danger!" ); } #[test] fn weird_bishop_sac() { let fen = String::from("r1b1k1nr/p4pp1/1pp1p3/4n2p/1b1qP3/1B1P3N/PPPBQPPP/RN2K2R w KQkq - 7 10"); let mut board = Board::from_FEN(fen); let (_, pv) = board.iterative_deepening(5, Duration::from_secs(60), true); assert_eq!( pv[0], Move { source: Square::C2, target: Square::C3, kind: MoveKind::Quiet }, "You should fork this bastard!" ); } #[test] fn castle_bonus() { assert_eq!(KING_BONUS[Square::E1.mirror() as usize], 0.0); assert!(KING_BONUS[Square::G1.mirror() as usize] > 0.0); assert!(KING_BONUS[Square::C1.mirror() as usize] > 0.0); assert!(KING_BONUS[Square::D1.mirror() as usize] < 0.0); assert!(KING_BONUS[Square::E2.mirror() as usize] < 0.0); assert_eq!(KING_BONUS[Square::E8 as usize], 0.0); assert!(KING_BONUS[Square::G8 as usize] > 0.0); assert!(KING_BONUS[Square::C8 as usize] > 0.0); assert!(KING_BONUS[Square::D8 as usize] < 0.0); assert!(KING_BONUS[Square::E2 as usize] < 0.0); } mod evaluation { use crate::{moves::{Move, MoveKind}, square::Square}; use super::*; #[test] fn initial_eval() { let board = Board::new(); assert_eq!(board.evaluate(), 0.0); } #[test] fn king_tropism() { let mut board = Board::new(); board.make_move(Move { source: Square::D1, target: Square::F5, kind: MoveKind::Quiet }); let score = board.evaluate(); board.print(); println!("Score {}", score); assert!(score < 0.0); assert!(score > -1.0); } #[test] fn white_winning() { let fen = String::from("8/5pk1/6p1/R4b1p/3P4/1P2N3/P1r2PPP/R5K1 b - - 1 27"); let board = Board::from_FEN(fen); let score = board.evaluate(); board.print(); println!("Score {}", score); assert!(score > 7.0); } #[test] fn black_winning() { let fen = String::from("8/p7/1k4K1/8/4P3/8/PP5r/8 b - - 1 38"); let board = Board::from_FEN(fen); let score = board.evaluate(); board.print(); println!("Score {}", score); assert!(score < -3.0); } #[test] fn encourage_center_pawns() { let score1 = { let fen = String::from("rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2"); let board = Board::from_FEN(fen); let score = board.evaluate(); board.print(); println!("Score {}", score); score }; let score2 = { let fen = String::from("rnbqkbnr/pppp1ppp/8/4p3/2P5/8/PP1PPPPP/RNBQKBNR w KQkq - 0 2"); let board = Board::from_FEN(fen); let score = board.evaluate(); board.print(); println!("Score {}", score); score }; assert!(score1 > score2); } #[test] fn discourage_edge_knights() { let score1 = { let fen = String::from("r1bqkbnr/pppp1ppp/2n5/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 2 3"); let board = Board::from_FEN(fen); let score = board.evaluate(); board.print(); println!("Score {}", score); score }; let score2 = { let fen = String::from("r1bqkbnr/pppp1ppp/2n5/4p3/4P3/7N/PPPP1PPP/RNBQKB1R w KQkq - 2 3"); let board = Board::from_FEN(fen); let score = board.evaluate(); board.print(); println!("Score {}", score); score }; assert!(score1 > score2); } #[test] fn mirrored_evaluation() { let score1 = { let fen = String::from("r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1"); let board = Board::from_FEN(fen); let score = board.evaluate(); board.print(); println!("Score {}", score); score }; let score2 = { let fen = String::from("r2q1rk1/pP1p2pp/Q4n2/bbp1p3/Np6/1B3NBn/pPPP1PPP/R3K2R b KQ - 0 1 "); let mut board = Board::from_FEN(fen); board.ply += 1; // TODO: remove me when FEN parsing includes side to move let score = board.evaluate(); board.print(); println!("Score {}", score); score }; assert_eq!(score1.abs(), score2.abs()); } } }