Compare commits
10 Commits
8d5ea3de31
...
1c4c158053
Author | SHA1 | Date | |
---|---|---|---|
1c4c158053 | |||
4b245e0421 | |||
b51dc87b2c | |||
7d7a2531ad | |||
da6b3f20f9 | |||
96b4816f84 | |||
b7b3c6c5b8 | |||
b36faba3ef | |||
9b447ca039 | |||
60d084886f |
@ -1,7 +1,6 @@
|
||||
/*
|
||||
|
||||
This file is part of chess_inator.
|
||||
|
||||
chess_inator is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
chess_inator is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
@ -14,8 +13,8 @@ Copyright © 2024 dogeystamp <dogeystamp@disroot.org>
|
||||
//! Main UCI engine binary.
|
||||
|
||||
use chess_inator::fen::FromFen;
|
||||
use chess_inator::movegen::{FromUCIAlgebraic, Move, MoveGen, MoveGenType, ToUCIAlgebraic};
|
||||
use chess_inator::search::best_move;
|
||||
use chess_inator::movegen::{FromUCIAlgebraic, Move, ToUCIAlgebraic};
|
||||
use chess_inator::search::{best_line, SearchEval};
|
||||
use chess_inator::Board;
|
||||
use std::io;
|
||||
|
||||
@ -88,7 +87,21 @@ fn cmd_position(mut tokens: std::str::SplitWhitespace<'_>) -> Board {
|
||||
|
||||
/// Play the game.
|
||||
fn cmd_go(mut _tokens: std::str::SplitWhitespace<'_>, board: &mut Board) {
|
||||
let chosen = best_move(board);
|
||||
let (line, eval) = best_line(board, None);
|
||||
let chosen = line.last().copied();
|
||||
println!(
|
||||
"info pv{}",
|
||||
line.iter()
|
||||
.rev()
|
||||
.map(|mv| mv.to_uci_algebraic())
|
||||
.fold(String::new(), |a, b| a + " " + &b)
|
||||
);
|
||||
match eval {
|
||||
SearchEval::Checkmate(n) => println!("info score mate {}", n / 2),
|
||||
SearchEval::Centipawns(eval) => {
|
||||
println!("info score cp {}", eval,)
|
||||
}
|
||||
}
|
||||
match chosen {
|
||||
Some(mv) => println!("bestmove {}", mv.to_uci_algebraic()),
|
||||
None => println!("bestmove 0000"),
|
||||
|
@ -1,14 +1,14 @@
|
||||
//! Generates moves from the FEN in the argv.
|
||||
|
||||
use chess_inator::fen::FromFen;
|
||||
use chess_inator::movegen::{MoveGen, MoveGenType};
|
||||
use chess_inator::movegen::MoveGen;
|
||||
use chess_inator::Board;
|
||||
use std::env;
|
||||
|
||||
fn main() {
|
||||
let fen = env::args().nth(1).unwrap();
|
||||
let mut board = Board::from_fen(&fen).unwrap();
|
||||
let mvs = board.gen_moves(MoveGenType::Legal);
|
||||
let mvs = board.gen_moves();
|
||||
for mv in mvs.into_iter() {
|
||||
println!("{mv:?}")
|
||||
}
|
||||
|
350
src/eval.rs
350
src/eval.rs
@ -13,7 +13,9 @@ Copyright © 2024 dogeystamp <dogeystamp@disroot.org>
|
||||
|
||||
//! Position evaluation.
|
||||
|
||||
use crate::{Board, Color, N_PIECES};
|
||||
use crate::{Board, Color, Piece, Square, N_COLORS, N_PIECES, N_SQUARES};
|
||||
use core::cmp::max;
|
||||
use core::ops::Index;
|
||||
|
||||
/// Signed centipawn type.
|
||||
///
|
||||
@ -22,34 +24,336 @@ pub type EvalInt = i16;
|
||||
|
||||
pub trait Eval {
|
||||
/// Evaluate a position and assign it a score.
|
||||
///
|
||||
/// Negative for Black advantage and positive for White.
|
||||
fn eval(&self) -> EvalInt;
|
||||
}
|
||||
|
||||
impl Eval for Board {
|
||||
fn eval(&self) -> EvalInt {
|
||||
use crate::Piece::*;
|
||||
let mut score: EvalInt = 0;
|
||||
pub(crate) mod eval_score {
|
||||
//! Opaque "score" counters to be used in the board.
|
||||
|
||||
// scores in centipawns for each piece
|
||||
let material_score: [EvalInt; N_PIECES] = [
|
||||
500, // rook
|
||||
300, // bishop
|
||||
300, // knight
|
||||
20000, // king
|
||||
900, // queen
|
||||
100, // pawn
|
||||
];
|
||||
use super::{EvalInt, PST_ENDGAME, PST_MIDGAME};
|
||||
use crate::{ColPiece, Square};
|
||||
|
||||
for pc in [Rook, Queen, Pawn, Knight, Bishop, King] {
|
||||
let tally_white = self.pl(Color::White).board(pc).0.count_ones();
|
||||
let tally_black = self.pl(Color::Black).board(pc).0.count_ones();
|
||||
let tally =
|
||||
EvalInt::try_from(tally_white).unwrap() - EvalInt::try_from(tally_black).unwrap();
|
||||
/// Internal score-keeping for a board.
|
||||
///
|
||||
/// This is kept in order to efficiently update evaluation with moves.
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Default, Debug)]
|
||||
pub struct EvalScores {
|
||||
/// Middle-game perspective evaluation of this board.
|
||||
pub midgame: EvalScore,
|
||||
/// End-game perspective evaluation of this board.
|
||||
pub endgame: EvalScore,
|
||||
/// Non-pawn/king piece count, used to determine when the endgame has begun.
|
||||
pub min_maj_pieces: u8,
|
||||
}
|
||||
|
||||
score += material_score[pc as usize] * tally;
|
||||
impl EvalScores {
|
||||
/// Add/remove the value of a piece based on the PST.
|
||||
///
|
||||
/// Use +1 as sign to add, -1 to delete.
|
||||
fn change_piece(&mut self, pc: &ColPiece, sq: &Square, sign: i8) {
|
||||
assert!(sign == 1 || sign == -1);
|
||||
let tables = [
|
||||
(&mut self.midgame, &PST_MIDGAME),
|
||||
(&mut self.endgame, &PST_ENDGAME),
|
||||
];
|
||||
for (phase, pst) in tables {
|
||||
phase.score += pst[pc.pc][pc.col][*sq] * EvalInt::from(pc.col.sign() * sign);
|
||||
}
|
||||
|
||||
use crate::Piece::*;
|
||||
if matches!(pc.pc, Rook | Queen | Knight | Bishop) {
|
||||
match sign {
|
||||
-1 => self.min_maj_pieces -= 1,
|
||||
1 => self.min_maj_pieces += 1,
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
score
|
||||
/// Remove the value of a piece on a square.
|
||||
pub fn del_piece(&mut self, pc: &ColPiece, sq: &Square) {
|
||||
self.change_piece(pc, sq, -1);
|
||||
}
|
||||
|
||||
/// Add the value of a piece on a square.
|
||||
pub fn add_piece(&mut self, pc: &ColPiece, sq: &Square) {
|
||||
self.change_piece(pc, sq, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// Score from a given perspective (e.g. midgame, endgame).
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Default, Debug)]
|
||||
pub struct EvalScore {
|
||||
/// Signed score.
|
||||
pub score: EvalInt,
|
||||
}
|
||||
}
|
||||
|
||||
/// The main piece-square-table (PST) type that assigns scores to pieces on given squares.
|
||||
///
|
||||
/// This is the main source of positional knowledge, as well as the ability to count material.
|
||||
pub struct Pst([PstPiece; N_PIECES]);
|
||||
/// A PST for a specific piece.
|
||||
type PstPiece = [PstSide; N_COLORS];
|
||||
/// A PST for a given piece, of a given color.
|
||||
type PstSide = [EvalInt; N_SQUARES];
|
||||
|
||||
impl Index<Piece> for Pst {
|
||||
type Output = PstPiece;
|
||||
|
||||
fn index(&self, index: Piece) -> &Self::Output {
|
||||
&self.0[index as usize]
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<Color> for PstPiece {
|
||||
type Output = PstSide;
|
||||
|
||||
fn index(&self, index: Color) -> &Self::Output {
|
||||
&self[index as usize]
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<Square> for PstSide {
|
||||
type Output = EvalInt;
|
||||
|
||||
fn index(&self, index: Square) -> &Self::Output {
|
||||
&self[usize::from(index)]
|
||||
}
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
const PERSPECTIVE_WHITE: [usize; N_SQUARES] = [
|
||||
56, 57, 58, 59, 60, 61, 62, 63,
|
||||
48, 49, 50, 51, 52, 53, 54, 55,
|
||||
40, 41, 42, 43, 44, 45, 46, 47,
|
||||
32, 33, 34, 35, 36, 37, 38, 39,
|
||||
24, 25, 26, 27, 28, 29, 30, 31,
|
||||
16, 17, 18, 19, 20, 21, 22, 23,
|
||||
8, 9, 10, 11, 12, 13, 14, 15,
|
||||
0, 1, 2, 3, 4, 5, 6, 7,
|
||||
];
|
||||
|
||||
/// This perspective is also horizontally reversed so the king is on the right side.
|
||||
#[rustfmt::skip]
|
||||
const PERSPECTIVE_BLACK: [usize; N_SQUARES] = [
|
||||
0, 1, 2, 3, 4, 5, 6, 7,
|
||||
8, 9, 10, 11, 12, 13, 14, 15,
|
||||
16, 17, 18, 19, 20, 21, 22, 23,
|
||||
24, 25, 26, 27, 28, 29, 30, 31,
|
||||
32, 33, 34, 35, 36, 37, 38, 39,
|
||||
40, 41, 42, 43, 44, 45, 46, 47,
|
||||
48, 49, 50, 51, 52, 53, 54, 55,
|
||||
56, 57, 58, 59, 60, 61, 62, 63,
|
||||
];
|
||||
|
||||
/// Helper to have the right board perspective in the source code.
|
||||
///
|
||||
/// In the source code, a1 will be at the bottom left, while h8 will be at the top right,
|
||||
/// corresponding to how humans usually see the board. This means that a8 is index 0, and h1 is
|
||||
/// index 63. This function shifts it so that a1 is 0, and h8 is 63, as in our implementation.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * pst: Square values in centipawns.
|
||||
/// * base_val: The base value of the piece, which is added to every square.
|
||||
const fn pst_perspective(
|
||||
pst: PstSide,
|
||||
base_val: EvalInt,
|
||||
perspective: [usize; N_SQUARES],
|
||||
) -> PstSide {
|
||||
let mut ret = pst;
|
||||
let mut i = 0;
|
||||
while i < N_SQUARES {
|
||||
let j = perspective[i];
|
||||
ret[i] = pst[j] + base_val;
|
||||
i += 1;
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
/// Construct PSTs for a single piece, from white's perspective.
|
||||
const fn make_pst(val: PstSide, base_val: EvalInt) -> PstPiece {
|
||||
[
|
||||
pst_perspective(val, base_val, PERSPECTIVE_WHITE),
|
||||
pst_perspective(val, base_val, PERSPECTIVE_BLACK),
|
||||
]
|
||||
}
|
||||
|
||||
/// Middle-game PSTs.
|
||||
#[rustfmt::skip]
|
||||
pub const PST_MIDGAME: Pst = Pst([
|
||||
// rook
|
||||
make_pst([
|
||||
1, 3, 2, 1, 4, 3, 2, 1, // 8
|
||||
20, 20, 20, 20, 20, 20, 20, 20, // 7
|
||||
1, 2, 3, 1, 2, 1, 2, 1, // 6
|
||||
-1, -2, 1, 2, 1, -1, 1, -1, // 5
|
||||
-1, -1, 2, -1, 1, -1, 2, 1, // 4
|
||||
2, 1, 1, 0, 0, 0, 0, 0, // 3
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 2
|
||||
0, 0, 0, 10, 10, 5, 0, 0, // 1
|
||||
// a b c d e f g h
|
||||
], 500),
|
||||
|
||||
// bishop
|
||||
make_pst([
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 8
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 7
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 6
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 5
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 4
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 3
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 2
|
||||
0, 0, -10, 0, 0, -10, 0, 0, // 1
|
||||
// a b c d e f g h
|
||||
], 300),
|
||||
|
||||
// knight
|
||||
make_pst([
|
||||
-5, -5, -5, -5, -5, -5, -5, -5, // 8
|
||||
-5, 0, 0, 0, 0, 0, 0, -5, // 7
|
||||
-5, 1, 0, 0, 0, 0, 0, -5, // 6
|
||||
-5, 2, 0, 10, 10, 0, 0, -5, // 5
|
||||
-5, 0, 1, 10, 10, 0, 0, -5, // 4
|
||||
-5, 2, 10, 0, 0, 10, 0, -5, // 3
|
||||
-5, 1, 0, 0, 0, 0, 0, -5, // 2
|
||||
-5, -5, -5, -5, -5, -5, -5, -5, // 1
|
||||
// a b c d e f g h
|
||||
], 300),
|
||||
|
||||
// king
|
||||
make_pst([
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 8
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 7
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 6
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 5
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 4
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 3
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 2
|
||||
0, 0, 10, 0, 0, 0, 20, 0, // 1
|
||||
// a b c d e f g h
|
||||
], 20_000),
|
||||
|
||||
// queen
|
||||
make_pst([
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 8
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 7
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 6
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 5
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 4
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 3
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 2
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 1
|
||||
// a b c d e f g h
|
||||
], 900),
|
||||
|
||||
// pawn
|
||||
make_pst([
|
||||
10, 10, 10, 10, 10, 10, 10, 10, // 8
|
||||
9, 9, 9, 9, 9, 9, 9, 9, // 7
|
||||
8, 8, 8, 8, 8, 8, 8, 8, // 6
|
||||
7, 7, 7, 8, 8, 7, 7, 7, // 5
|
||||
6, 6, 6, 6, 6, 6, 6, 6, // 4
|
||||
2, 2, 2, 4, 4, 0, 2, 0, // 3
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 2
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 1
|
||||
// a b c d e f g h
|
||||
], 100),
|
||||
]);
|
||||
|
||||
#[rustfmt::skip]
|
||||
pub const PST_ENDGAME: Pst = Pst([
|
||||
// rook
|
||||
make_pst([
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 8
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 7
|
||||
1, 2, 3, 1, 2, 1, 2, 1, // 6
|
||||
1, 2, 1, 2, 1, 1, 1, 1, // 5
|
||||
1, 1, 2, 1, 1, 1, 2, 1, // 4
|
||||
2, 1, 1, 0, 0, 0, 0, 0, // 3
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 2
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 1
|
||||
// a b c d e f g h
|
||||
], 500),
|
||||
|
||||
// bishop
|
||||
make_pst([
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 8
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 7
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 6
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 5
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 4
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 3
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 2
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 1
|
||||
// a b c d e f g h
|
||||
], 300),
|
||||
|
||||
// knight
|
||||
make_pst([
|
||||
-5, -5, -5, -5, -5, -5, -5, -5, // 8
|
||||
-5, 0, 0, 0, 0, 0, 0, -5, // 7
|
||||
-5, 0, 0, 0, 0, 0, 0, -5, // 6
|
||||
-5, 0, 0, 0, 0, 0, 0, -5, // 5
|
||||
-5, 0, 0, 0, 0, 0, 0, -5, // 4
|
||||
-5, 0, 0, 0, 0, 0, 0, -5, // 3
|
||||
-5, 0, 0, 0, 0, 0, 0, -5, // 2
|
||||
-5, -5, -5, -5, -5, -5, -5, -5, // 1
|
||||
// a b c d e f g h
|
||||
], 300),
|
||||
|
||||
// king
|
||||
make_pst([
|
||||
-50, -50, -50, -50, -50, -50, -50, -50, // 8
|
||||
-50, -10, -10, -10, -10, -10, -10, -50, // 7
|
||||
-50, -10, 0, 0, 0, 0, -10, -50, // 6
|
||||
-50, -10, 0, 4, 4, 0, -10, -50, // 5
|
||||
-50, -10, 0, 4, 4, 0, -10, -50, // 4
|
||||
-50, -10, 0, 0, 0, 0, -10, -50, // 3
|
||||
-50, -10, -10, -10, -10, -10, -10, -50, // 2
|
||||
-50, -50, -50, -50, -50, -50, -50, -50, // 1
|
||||
// a b c d e f g h
|
||||
], 20_000),
|
||||
|
||||
// queen
|
||||
make_pst([
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 8
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 7
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 6
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 5
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 4
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 3
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 2
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 1
|
||||
// a b c d e f g h
|
||||
], 900),
|
||||
|
||||
// pawn
|
||||
make_pst([
|
||||
10, 10, 10, 10, 10, 10, 10, 10, // 8
|
||||
139, 139, 139, 139, 139, 139, 139, 139, // 7
|
||||
138, 138, 138, 138, 138, 138, 138, 108, // 6
|
||||
67, 67, 67, 68, 68, 67, 67, 67, // 5
|
||||
36, 36, 36, 36, 36, 36, 36, 36, // 4
|
||||
32, 32, 32, 34, 34, 30, 32, 30, // 3
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 2
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 1
|
||||
// a b c d e f g h
|
||||
], 100),
|
||||
]);
|
||||
|
||||
impl Eval for Board {
|
||||
fn eval(&self) -> EvalInt {
|
||||
// we'll define endgame as the moment when there are 7 non pawn/king pieces left on the
|
||||
// board in total.
|
||||
//
|
||||
// `phase` will range from 7 (game start) to 0 (endgame).
|
||||
let phase = i32::from(self.eval.min_maj_pieces.saturating_sub(7));
|
||||
let eval = i32::from(self.eval.midgame.score) * phase / 7
|
||||
+ i32::from(self.eval.endgame.score) * max(7 - phase, 0) / 7;
|
||||
eval.try_into().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,7 +370,7 @@ mod tests {
|
||||
let board2 = Board::from_fen("rnbqkbnr/pppppppp/8/8/8/8/8/4K3 w kq - 0 1").unwrap();
|
||||
let eval2 = board2.eval();
|
||||
|
||||
assert!(eval1 > 0, "got eval {eval1}");
|
||||
assert!(eval2 < 0, "got eval {eval2}");
|
||||
assert!(eval1 > 0, "got eval {eval1} ({:?})", board1.eval);
|
||||
assert!(eval2 < 0, "got eval {eval2} ({:?})", board2.eval);
|
||||
}
|
||||
}
|
||||
|
131
src/lib.rs
131
src/lib.rs
@ -14,6 +14,7 @@ Copyright © 2024 dogeystamp <dogeystamp@disroot.org>
|
||||
#![deny(rust_2018_idioms)]
|
||||
|
||||
use std::fmt::Display;
|
||||
use std::ops::{Index, IndexMut};
|
||||
use std::str::FromStr;
|
||||
|
||||
pub mod eval;
|
||||
@ -22,6 +23,7 @@ pub mod movegen;
|
||||
pub mod search;
|
||||
|
||||
use crate::fen::{FromFen, ToFen, START_POSITION};
|
||||
use eval::eval_score::EvalScores;
|
||||
|
||||
const BOARD_WIDTH: usize = 8;
|
||||
const BOARD_HEIGHT: usize = 8;
|
||||
@ -43,6 +45,12 @@ impl Color {
|
||||
Color::Black => Color::White,
|
||||
}
|
||||
}
|
||||
pub fn sign(&self) -> i8 {
|
||||
match self {
|
||||
Color::White => 1,
|
||||
Color::Black => -1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for char {
|
||||
@ -156,6 +164,7 @@ macro_rules! sq_try_from {
|
||||
type Error = SquareError;
|
||||
|
||||
fn try_from(value: $T) -> Result<Self, Self::Error> {
|
||||
#[allow(irrefutable_let_patterns)]
|
||||
if let Ok(upper_bound) = <$T>::try_from(N_SQUARES) {
|
||||
if (0..upper_bound).contains(&value) {
|
||||
return Ok(Square(value as SquareIdx));
|
||||
@ -372,23 +381,11 @@ impl Mailbox {
|
||||
///
|
||||
/// Default is all empty.
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
struct Player {
|
||||
pub struct PlayerBoards {
|
||||
/// Bitboards for individual pieces. Piece -> locations.
|
||||
bit: [Bitboard; N_PIECES],
|
||||
}
|
||||
|
||||
impl Player {
|
||||
/// Get board (non-mutable) for a specific piece.
|
||||
fn board(&self, pc: Piece) -> &Bitboard {
|
||||
&self.bit[pc as usize]
|
||||
}
|
||||
|
||||
/// Get board (mutable) for a specific piece.
|
||||
fn board_mut(&mut self, pc: Piece) -> &mut Bitboard {
|
||||
&mut self.bit[pc as usize]
|
||||
}
|
||||
}
|
||||
|
||||
/// Castling rights for one player
|
||||
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
|
||||
pub struct CastlePlayer {
|
||||
@ -402,9 +399,9 @@ pub struct CastlePlayer {
|
||||
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
|
||||
pub struct CastleRights([CastlePlayer; N_COLORS]);
|
||||
|
||||
impl ToString for CastleRights {
|
||||
impl Display for CastleRights {
|
||||
/// Convert to FEN castling rights format.
|
||||
fn to_string(&self) -> String {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut ret = String::with_capacity(4);
|
||||
for (val, ch) in [
|
||||
(self.0[Color::White as usize].k, 'K'),
|
||||
@ -419,7 +416,7 @@ impl ToString for CastleRights {
|
||||
if ret.is_empty() {
|
||||
ret.push('-')
|
||||
}
|
||||
ret
|
||||
write!(f, "{}", ret)
|
||||
}
|
||||
}
|
||||
|
||||
@ -429,7 +426,7 @@ impl ToString for CastleRights {
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Board {
|
||||
/// Player bitboards
|
||||
players: [Player; N_COLORS],
|
||||
players: [PlayerBoards; N_COLORS],
|
||||
|
||||
/// Mailbox (array) board. Location -> piece.
|
||||
mail: Mailbox,
|
||||
@ -450,6 +447,9 @@ pub struct Board {
|
||||
|
||||
/// Whose turn it is
|
||||
turn: Color,
|
||||
|
||||
/// Counters for evaluation.
|
||||
eval: EvalScores,
|
||||
}
|
||||
|
||||
impl Board {
|
||||
@ -458,37 +458,18 @@ impl Board {
|
||||
Board::from_fen(START_POSITION).unwrap()
|
||||
}
|
||||
|
||||
/// Get mutable reference to a player.
|
||||
fn pl_mut(&mut self, col: Color) -> &mut Player {
|
||||
&mut self.players[col as usize]
|
||||
}
|
||||
|
||||
/// Get immutable reference to a player.
|
||||
fn pl(&self, col: Color) -> &Player {
|
||||
&self.players[col as usize]
|
||||
}
|
||||
|
||||
/// Get immutable reference to castling rights.
|
||||
pub fn pl_castle(&self, col: Color) -> &CastlePlayer {
|
||||
&self.castle.0[col as usize]
|
||||
}
|
||||
|
||||
/// Get mutable reference to castling rights.
|
||||
fn pl_castle_mut(&mut self, col: Color) -> &mut CastlePlayer {
|
||||
&mut self.castle.0[col as usize]
|
||||
}
|
||||
|
||||
/// Get iterator over all squares.
|
||||
pub fn squares() -> impl Iterator<Item = Square> {
|
||||
(0..N_SQUARES).map(Square::try_from).map(|x| x.unwrap())
|
||||
}
|
||||
|
||||
/// Create a new piece in a location, and pop any existing piece in the destination.
|
||||
pub fn set_piece(&mut self, idx: Square, pc: ColPiece) -> Option<ColPiece> {
|
||||
let dest_pc = self.del_piece(idx);
|
||||
let pl = self.pl_mut(pc.col);
|
||||
pl.board_mut(pc.into()).on_sq(idx);
|
||||
*self.mail.sq_mut(idx) = Some(pc);
|
||||
pub fn set_piece(&mut self, sq: Square, pc: ColPiece) -> Option<ColPiece> {
|
||||
let dest_pc = self.del_piece(sq);
|
||||
let pl = &mut self[pc.col];
|
||||
pl[pc.into()].on_sq(sq);
|
||||
*self.mail.sq_mut(sq) = Some(pc);
|
||||
self.eval.add_piece(&pc, &sq);
|
||||
dest_pc
|
||||
}
|
||||
|
||||
@ -501,11 +482,12 @@ impl Board {
|
||||
}
|
||||
|
||||
/// Delete the piece in a location, and return ("pop") that piece.
|
||||
pub fn del_piece(&mut self, idx: Square) -> Option<ColPiece> {
|
||||
if let Some(pc) = *self.mail.sq_mut(idx) {
|
||||
let pl = self.pl_mut(pc.col);
|
||||
pl.board_mut(pc.into()).off_sq(idx);
|
||||
*self.mail.sq_mut(idx) = None;
|
||||
pub fn del_piece(&mut self, sq: Square) -> Option<ColPiece> {
|
||||
if let Some(pc) = *self.mail.sq_mut(sq) {
|
||||
let pl = &mut self[pc.col];
|
||||
pl[pc.into()].off_sq(sq);
|
||||
*self.mail.sq_mut(sq) = None;
|
||||
self.eval.del_piece(&pc, &sq);
|
||||
Some(pc)
|
||||
} else {
|
||||
None
|
||||
@ -539,6 +521,7 @@ impl Board {
|
||||
mail: Default::default(),
|
||||
ep_square: self.ep_square.map(|sq| sq.mirror_vert()),
|
||||
castle: CastleRights(self.castle.0),
|
||||
eval: Default::default(),
|
||||
};
|
||||
|
||||
new_board.castle.0.reverse();
|
||||
@ -555,7 +538,7 @@ impl Board {
|
||||
|
||||
/// Is a given player in check?
|
||||
pub fn is_check(&self, pl: Color) -> bool {
|
||||
for src in self.pl(pl).board(Piece::King).into_iter() {
|
||||
for src in self[pl][Piece::King] {
|
||||
macro_rules! detect_checker {
|
||||
($dirs: ident, $pc: pat, $keep_going: expr) => {
|
||||
for dir in $dirs.into_iter() {
|
||||
@ -602,10 +585,57 @@ impl Board {
|
||||
false
|
||||
}
|
||||
|
||||
/// Get the current player to move.
|
||||
pub fn get_turn(&self) -> Color {
|
||||
self.turn
|
||||
}
|
||||
|
||||
/// Maximum amount of moves in the counter to parse before giving up
|
||||
const MAX_MOVES: usize = 9_999;
|
||||
}
|
||||
|
||||
impl Index<Color> for Board {
|
||||
type Output = PlayerBoards;
|
||||
|
||||
fn index(&self, col: Color) -> &Self::Output {
|
||||
&self.players[col as usize]
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexMut<Color> for Board {
|
||||
fn index_mut(&mut self, col: Color) -> &mut Self::Output {
|
||||
&mut self.players[col as usize]
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<Color> for CastleRights {
|
||||
type Output = CastlePlayer;
|
||||
|
||||
fn index(&self, col: Color) -> &Self::Output {
|
||||
&self.0[col as usize]
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexMut<Color> for CastleRights {
|
||||
fn index_mut(&mut self, col: Color) -> &mut Self::Output {
|
||||
&mut self.0[col as usize]
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<Piece> for PlayerBoards {
|
||||
type Output = Bitboard;
|
||||
|
||||
fn index(&self, pc: Piece) -> &Self::Output {
|
||||
&self.bit[pc as usize]
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexMut<Piece> for PlayerBoards {
|
||||
fn index_mut(&mut self, pc: Piece) -> &mut Self::Output {
|
||||
&mut self.bit[pc as usize]
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Board {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut str = String::with_capacity(N_SQUARES + BOARD_HEIGHT);
|
||||
@ -633,6 +663,7 @@ mod tests {
|
||||
for tc in fail_cases {
|
||||
macro_rules! try_type {
|
||||
($T: ty) => {
|
||||
#[allow(irrefutable_let_patterns)]
|
||||
if let Ok(conv) = <$T>::try_from(tc) {
|
||||
assert!(matches!(
|
||||
Square::try_from(conv),
|
||||
@ -693,9 +724,7 @@ mod tests {
|
||||
}
|
||||
|
||||
let board = Board::from_fen("8/4p3/1q1Q1p2/4p3/1p1r4/8/8/8 w - - 0 1").unwrap();
|
||||
let white_queens = board
|
||||
.pl(Color::White)
|
||||
.board(Piece::Queen)
|
||||
let white_queens = board[Color::White][Piece::Queen]
|
||||
.into_iter()
|
||||
.collect::<Vec<Square>>();
|
||||
assert_eq!(white_queens, vec![Square::from_str("d6").unwrap()])
|
||||
|
108
src/movegen.rs
108
src/movegen.rs
@ -296,7 +296,7 @@ impl Move {
|
||||
pos.half_moves = 0;
|
||||
}
|
||||
|
||||
let castle = &mut pos.pl_castle_mut(pc_src.col);
|
||||
let castle = &mut pos.castle[pc_src.col];
|
||||
if matches!(pc_src.pc, Piece::King) {
|
||||
// forfeit castling rights
|
||||
castle.k = false;
|
||||
@ -444,15 +444,27 @@ impl ToUCIAlgebraic for Move {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum MoveGenType {
|
||||
enum MoveGenType {
|
||||
/// Legal move generation.
|
||||
Legal,
|
||||
/// Allow capturing friendly pieces, moving into check, but not castling through check.
|
||||
Pseudo,
|
||||
_Pseudo,
|
||||
}
|
||||
|
||||
/// Internal, slightly more general movegen interface
|
||||
trait MoveGenInternal {
|
||||
fn gen_moves_general(&mut self, gen_type: MoveGenType) -> impl IntoIterator<Item = Move>;
|
||||
}
|
||||
|
||||
pub trait MoveGen {
|
||||
fn gen_moves(&mut self, gen_type: MoveGenType) -> impl IntoIterator<Item = Move>;
|
||||
/// Legal move generation.
|
||||
fn gen_moves(&mut self) -> impl IntoIterator<Item = Move>;
|
||||
}
|
||||
|
||||
impl<T: MoveGenInternal> MoveGen for T {
|
||||
fn gen_moves(&mut self) -> impl IntoIterator<Item = Move> {
|
||||
self.gen_moves_general(MoveGenType::Legal)
|
||||
}
|
||||
}
|
||||
|
||||
pub const DIRS_STRAIGHT: [(isize, isize); 4] = [(0, 1), (1, 0), (-1, 0), (0, -1)];
|
||||
@ -561,13 +573,13 @@ fn is_legal(board: &mut Board, mv: Move) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
impl MoveGen for Board {
|
||||
fn gen_moves(&mut self, gen_type: MoveGenType) -> impl IntoIterator<Item = Move> {
|
||||
impl MoveGenInternal for Board {
|
||||
fn gen_moves_general(&mut self, gen_type: MoveGenType) -> impl IntoIterator<Item = Move> {
|
||||
let mut ret = Vec::new();
|
||||
let pl = self.pl(self.turn);
|
||||
let pl = self[self.turn];
|
||||
macro_rules! squares {
|
||||
($pc: ident) => {
|
||||
pl.board(Piece::$pc).into_iter()
|
||||
pl[Piece::$pc].into_iter()
|
||||
};
|
||||
}
|
||||
|
||||
@ -583,7 +595,7 @@ impl MoveGen for Board {
|
||||
for src in squares!(King) {
|
||||
move_slider(self, src, &mut ret, SliderDirection::Star, false);
|
||||
let (r, c) = src.to_row_col_signed();
|
||||
let rights = self.pl_castle(self.turn);
|
||||
let rights = self.castle[self.turn];
|
||||
let castle_sides = [(rights.k, 2, BOARD_WIDTH as isize - 1), (rights.q, -2, 0)];
|
||||
for (is_allowed, move_offset, endpoint) in castle_sides {
|
||||
if !is_allowed {
|
||||
@ -660,11 +672,7 @@ impl MoveGen for Board {
|
||||
Color::Black => 0,
|
||||
};
|
||||
|
||||
let nr = (r)
|
||||
+ match self.turn {
|
||||
Color::White => 1,
|
||||
Color::Black => -1,
|
||||
};
|
||||
let nr = r + isize::from(self.turn.sign());
|
||||
let is_promotion = nr == last_row;
|
||||
|
||||
macro_rules! push_moves {
|
||||
@ -746,7 +754,7 @@ impl MoveGen for Board {
|
||||
}
|
||||
ret.into_iter().filter(move |mv| match gen_type {
|
||||
MoveGenType::Legal => is_legal(self, *mv),
|
||||
MoveGenType::Pseudo => true,
|
||||
MoveGenType::_Pseudo => true,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -759,7 +767,7 @@ pub fn perft(depth: usize, pos: &mut Board) -> usize {
|
||||
|
||||
let mut ans = 0;
|
||||
|
||||
let moves: Vec<Move> = pos.gen_moves(MoveGenType::Legal).into_iter().collect();
|
||||
let moves: Vec<Move> = pos.gen_moves().into_iter().collect();
|
||||
for mv in moves {
|
||||
let anti_move = mv.make(pos);
|
||||
ans += perft(depth - 1, pos);
|
||||
@ -784,8 +792,8 @@ mod tests {
|
||||
use std::collections::hash_set::HashSet;
|
||||
use Piece::*;
|
||||
for pc in [Rook, Bishop, Knight, Queen, King, Pawn] {
|
||||
let white: HashSet<_> = pos.pl(Color::White).board(pc).into_iter().collect();
|
||||
let black: HashSet<_> = pos.pl(Color::Black).board(pc).into_iter().collect();
|
||||
let white: HashSet<_> = pos[Color::White][pc].into_iter().collect();
|
||||
let black: HashSet<_> = pos[Color::Black][pc].into_iter().collect();
|
||||
let intersect = white.intersection(&black).collect::<Vec<_>>();
|
||||
assert!(
|
||||
intersect.is_empty(),
|
||||
@ -1081,7 +1089,10 @@ mod tests {
|
||||
let all_cases = [augmented_test_cases, test_cases].concat();
|
||||
|
||||
for (mut board, expected_moves) in all_cases {
|
||||
let mut moves: Vec<Move> = board.gen_moves(MoveGenType::Pseudo).into_iter().collect();
|
||||
let mut moves: Vec<Move> = board
|
||||
.gen_moves_general(MoveGenType::_Pseudo)
|
||||
.into_iter()
|
||||
.collect();
|
||||
moves.sort_unstable();
|
||||
let moves = moves;
|
||||
|
||||
@ -1236,7 +1247,7 @@ mod tests {
|
||||
expected_moves.sort_unstable();
|
||||
let expected_moves = expected_moves;
|
||||
|
||||
let mut moves: Vec<Move> = board.gen_moves(MoveGenType::Legal).into_iter().collect();
|
||||
let mut moves: Vec<Move> = board.gen_moves().into_iter().collect();
|
||||
moves.sort_unstable();
|
||||
let moves = moves;
|
||||
|
||||
@ -1393,61 +1404,4 @@ mod tests {
|
||||
assert_eq!(mv.to_uci_algebraic(), tc);
|
||||
}
|
||||
}
|
||||
|
||||
/// The standard movegen test.
|
||||
///
|
||||
/// See https://www.chessprogramming.org/Perft
|
||||
#[test]
|
||||
fn test_perft() {
|
||||
// https://www.chessprogramming.org/Perft_Results
|
||||
let test_cases = [
|
||||
(
|
||||
// fen
|
||||
START_POSITION,
|
||||
// expected perft values
|
||||
vec![1, 20, 400, 8_902, 197_281, 4_865_609, 119_060_324],
|
||||
// limit depth when not under `cargo test --release` (unoptimized build too slow)
|
||||
4,
|
||||
),
|
||||
(
|
||||
"r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1",
|
||||
vec![1, 48, 2_039, 97_862, 4_085_603],
|
||||
3,
|
||||
),
|
||||
(
|
||||
"8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1",
|
||||
vec![1, 14, 191, 2_812, 43_238, 674_624, 11_030_083],
|
||||
4,
|
||||
),
|
||||
(
|
||||
"r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1",
|
||||
vec![1, 6, 264, 9467, 422_333, 15_833_292],
|
||||
3,
|
||||
),
|
||||
(
|
||||
"rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8",
|
||||
vec![1, 44, 1_486, 62_379, 2_103_487, 89_941_194],
|
||||
3,
|
||||
),
|
||||
(
|
||||
"r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10",
|
||||
vec![1, 46, 2_079, 89_890, 3_894_594],
|
||||
3,
|
||||
),
|
||||
];
|
||||
for (fen, expected_values, _debug_limit_depth) in test_cases {
|
||||
let mut pos = Board::from_fen(fen).unwrap();
|
||||
|
||||
for (depth, expected) in expected_values.iter().enumerate() {
|
||||
eprintln!("running perft depth {depth} on position '{fen}'");
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
if depth > _debug_limit_depth {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert_eq!(perft(depth, &mut pos), *expected,);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
210
src/search.rs
210
src/search.rs
@ -14,7 +14,7 @@ Copyright © 2024 dogeystamp <dogeystamp@disroot.org>
|
||||
//! Game-tree search.
|
||||
|
||||
use crate::eval::{Eval, EvalInt};
|
||||
use crate::movegen::{Move, MoveGen, MoveGenType, ToUCIAlgebraic};
|
||||
use crate::movegen::{Move, MoveGen};
|
||||
use crate::Board;
|
||||
use std::cmp::max;
|
||||
|
||||
@ -34,7 +34,82 @@ mod test_eval_int {
|
||||
}
|
||||
}
|
||||
|
||||
/// Search the game tree to find the absolute (positive good) eval for the current player.
|
||||
/// Eval in the context of search.
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
|
||||
pub enum SearchEval {
|
||||
/// Mate in |n| - 1 half moves, negative for own mate.
|
||||
Checkmate(i8),
|
||||
/// Centipawn score.
|
||||
Centipawns(EvalInt),
|
||||
}
|
||||
|
||||
impl SearchEval {
|
||||
/// Flip side, and increment the "mate in n" counter.
|
||||
fn increment(self) -> Self {
|
||||
match self {
|
||||
SearchEval::Checkmate(n) => {
|
||||
debug_assert_ne!(n, 0);
|
||||
if n < 0 {
|
||||
Self::Checkmate(-(n - 1))
|
||||
} else {
|
||||
Self::Checkmate(-(n + 1))
|
||||
}
|
||||
}
|
||||
SearchEval::Centipawns(eval) => Self::Centipawns(-eval),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SearchEval> for EvalInt {
|
||||
fn from(value: SearchEval) -> Self {
|
||||
match value {
|
||||
SearchEval::Checkmate(n) => {
|
||||
debug_assert_ne!(n, 0);
|
||||
if n < 0 {
|
||||
EVAL_WORST - EvalInt::from(n)
|
||||
} else {
|
||||
EVAL_BEST - EvalInt::from(n)
|
||||
}
|
||||
}
|
||||
SearchEval::Centipawns(eval) => eval,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for SearchEval {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
let e1 = EvalInt::from(*self);
|
||||
let e2 = EvalInt::from(*other);
|
||||
e1.cmp(&e2)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for SearchEval {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for the gametree search.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct SearchConfig {
|
||||
/// Enable alpha-beta pruning.
|
||||
alpha_beta_on: bool,
|
||||
/// Limit search depth (will probably change as quiescence search is implemented)
|
||||
depth: usize,
|
||||
}
|
||||
|
||||
impl Default for SearchConfig {
|
||||
fn default() -> Self {
|
||||
SearchConfig {
|
||||
alpha_beta_on: true,
|
||||
depth: 5,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Search the game tree to find the absolute (positive good) move and corresponding eval for the
|
||||
/// current player.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
@ -42,7 +117,17 @@ mod test_eval_int {
|
||||
/// * depth: how deep to analyze the game tree.
|
||||
/// * alpha: best score (absolute, from current player perspective) guaranteed for current player.
|
||||
/// * beta: best score (absolute, from current player perspective) guaranteed for other player.
|
||||
fn minmax(board: &mut Board, depth: usize, alpha: Option<EvalInt>, beta: Option<EvalInt>) -> EvalInt {
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The best line (in reverse move order), and its corresponding absolute eval for the current player.
|
||||
fn minmax(
|
||||
board: &mut Board,
|
||||
config: &SearchConfig,
|
||||
depth: usize,
|
||||
alpha: Option<EvalInt>,
|
||||
beta: Option<EvalInt>,
|
||||
) -> (Vec<Move>, SearchEval) {
|
||||
// default to worst, then gradually improve
|
||||
let mut alpha = alpha.unwrap_or(EVAL_WORST);
|
||||
// our best is their worst
|
||||
@ -50,68 +135,115 @@ fn minmax(board: &mut Board, depth: usize, alpha: Option<EvalInt>, beta: Option<
|
||||
|
||||
if depth == 0 {
|
||||
let eval = board.eval();
|
||||
match board.turn {
|
||||
crate::Color::White => return eval,
|
||||
crate::Color::Black => return -eval,
|
||||
}
|
||||
return (
|
||||
Vec::new(),
|
||||
SearchEval::Centipawns(eval * EvalInt::from(board.turn.sign())),
|
||||
);
|
||||
}
|
||||
|
||||
let mvs: Vec<_> = board.gen_moves(MoveGenType::Legal).into_iter().collect();
|
||||
let mvs: Vec<_> = board.gen_moves().into_iter().collect();
|
||||
|
||||
let mut abs_best = EVAL_WORST;
|
||||
let mut abs_best = SearchEval::Centipawns(EVAL_WORST);
|
||||
let mut best_move: Option<Move> = None;
|
||||
let mut best_continuation: Vec<Move> = Vec::new();
|
||||
|
||||
if mvs.is_empty() {
|
||||
if board.is_check(board.turn) {
|
||||
return EVAL_WORST;
|
||||
return (Vec::new(), SearchEval::Checkmate(-1));
|
||||
} else {
|
||||
// stalemate
|
||||
return 0;
|
||||
return (Vec::new(), SearchEval::Centipawns(0));
|
||||
}
|
||||
}
|
||||
|
||||
for mv in mvs {
|
||||
let anti_mv = mv.make(board);
|
||||
let abs_score = -minmax(board, depth - 1, Some(-beta), Some(-alpha));
|
||||
abs_best = max(abs_best, abs_score);
|
||||
alpha = max(alpha, abs_best);
|
||||
let (continuation, score) = minmax(board, config, depth - 1, Some(-beta), Some(-alpha));
|
||||
let abs_score = score.increment();
|
||||
if abs_score > abs_best {
|
||||
abs_best = abs_score;
|
||||
best_move = Some(mv);
|
||||
best_continuation = continuation;
|
||||
}
|
||||
alpha = max(alpha, abs_best.into());
|
||||
anti_mv.unmake(board);
|
||||
if alpha >= beta {
|
||||
if alpha >= beta && config.alpha_beta_on {
|
||||
// alpha-beta prune.
|
||||
//
|
||||
// Beta represents the best eval that the other player can get in sibling branches
|
||||
// (different moves in the parent node). Alpha >= beta means the eval here is _worse_
|
||||
// (different moves in the parent node). Alpha > beta means the eval here is _worse_
|
||||
// for the other player, so they will never make the move that leads into this branch.
|
||||
// Therefore, we stop evaluating this branch at all.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
abs_best
|
||||
}
|
||||
|
||||
/// Find the best move for a position (internal interface).
|
||||
fn search(board: &mut Board) -> Option<Move> {
|
||||
const DEPTH: usize = 4;
|
||||
let mvs: Vec<_> = board.gen_moves(MoveGenType::Legal).into_iter().collect();
|
||||
|
||||
// absolute eval value
|
||||
let mut best_eval = EVAL_WORST;
|
||||
let mut best_mv: Option<Move> = None;
|
||||
|
||||
for mv in mvs {
|
||||
let anti_mv = mv.make(board);
|
||||
let abs_eval = -minmax(board, DEPTH, None, None);
|
||||
if abs_eval >= best_eval {
|
||||
best_eval = abs_eval;
|
||||
best_mv = Some(mv);
|
||||
}
|
||||
anti_mv.unmake(board);
|
||||
if let Some(mv) = best_move {
|
||||
best_continuation.push(mv);
|
||||
}
|
||||
|
||||
best_mv
|
||||
(best_continuation, abs_best)
|
||||
}
|
||||
|
||||
/// Find the best line (in reverse order) and its evaluation.
|
||||
pub fn best_line(board: &mut Board, config: Option<SearchConfig>) -> (Vec<Move>, SearchEval) {
|
||||
let config = config.unwrap_or_default();
|
||||
let (line, eval) = minmax(board, &config, config.depth, None, None);
|
||||
(line, eval)
|
||||
}
|
||||
|
||||
/// Find the best move.
|
||||
pub fn best_move(board: &mut Board) -> Option<Move> {
|
||||
search(board)
|
||||
pub fn best_move(board: &mut Board, config: Option<SearchConfig>) -> Option<Move> {
|
||||
let (line, _eval) = best_line(board, Some(config.unwrap_or_default()));
|
||||
line.last().copied()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::fen::{FromFen, ToFen};
|
||||
use crate::movegen::ToUCIAlgebraic;
|
||||
|
||||
/// Theoretically, alpha-beta pruning should not affect the result of minmax.
|
||||
#[test]
|
||||
fn alpha_beta_same_result() {
|
||||
let test_cases = [
|
||||
// in these cases the engines really likes to sacrifice its pieces for no gain...
|
||||
"r2q1rk1/1bp1pp1p/p2p2p1/1p1P2P1/2n1P3/3Q1P2/PbPBN2P/3RKB1R b K - 5 15",
|
||||
"r1b1k2r/p1qpppbp/1p4pn/2B3N1/1PP1P3/2P5/P4PPP/RN1QR1K1 w kq - 0 14",
|
||||
];
|
||||
for fen in test_cases {
|
||||
let mut board = Board::from_fen(fen).unwrap();
|
||||
let mv_no_prune = best_move(
|
||||
&mut board,
|
||||
Some(SearchConfig {
|
||||
alpha_beta_on: false,
|
||||
depth: 3,
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(board.to_fen(), fen);
|
||||
|
||||
let mv_with_prune = best_move(
|
||||
&mut board,
|
||||
Some(SearchConfig {
|
||||
alpha_beta_on: true,
|
||||
depth: 3,
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(board.to_fen(), fen);
|
||||
|
||||
println!(
|
||||
"without ab prune got {}, otherwise {}, fen {}",
|
||||
mv_no_prune.to_uci_algebraic(),
|
||||
mv_with_prune.to_uci_algebraic(),
|
||||
fen
|
||||
);
|
||||
|
||||
assert_eq!(mv_no_prune, mv_with_prune);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
74
tests/perft.rs
Normal file
74
tests/perft.rs
Normal file
@ -0,0 +1,74 @@
|
||||
/*
|
||||
|
||||
This file is part of chess_inator.
|
||||
|
||||
chess_inator is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
chess_inator is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with chess_inator. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
Copyright © 2024 dogeystamp <dogeystamp@disroot.org>
|
||||
*/
|
||||
|
||||
//! Perft verification on known positions.
|
||||
//!
|
||||
//! See https://www.chessprogramming.org/Perft
|
||||
|
||||
use chess_inator::fen::{FromFen, START_POSITION};
|
||||
use chess_inator::movegen::perft;
|
||||
use chess_inator::Board;
|
||||
|
||||
#[test]
|
||||
fn test_perft() {
|
||||
// https://www.chessprogramming.org/Perft_Results
|
||||
let test_cases = [
|
||||
(
|
||||
// fen
|
||||
START_POSITION,
|
||||
// expected perft values
|
||||
vec![1, 20, 400, 8_902, 197_281, 4_865_609, 119_060_324],
|
||||
// limit depth when not under `cargo test --release` (unoptimized build too slow)
|
||||
4,
|
||||
),
|
||||
(
|
||||
"r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1",
|
||||
vec![1, 48, 2_039, 97_862, 4_085_603],
|
||||
3,
|
||||
),
|
||||
(
|
||||
"8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1",
|
||||
vec![1, 14, 191, 2_812, 43_238, 674_624, 11_030_083],
|
||||
4,
|
||||
),
|
||||
(
|
||||
"r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1",
|
||||
vec![1, 6, 264, 9467, 422_333, 15_833_292],
|
||||
3,
|
||||
),
|
||||
(
|
||||
"rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8",
|
||||
vec![1, 44, 1_486, 62_379, 2_103_487, 89_941_194],
|
||||
3,
|
||||
),
|
||||
(
|
||||
"r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10",
|
||||
vec![1, 46, 2_079, 89_890, 3_894_594],
|
||||
3,
|
||||
),
|
||||
];
|
||||
for (fen, expected_values, _debug_limit_depth) in test_cases {
|
||||
let mut pos = Board::from_fen(fen).unwrap();
|
||||
|
||||
for (depth, expected) in expected_values.iter().enumerate() {
|
||||
eprintln!("running perft depth {depth} on position '{fen}'");
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
if depth > _debug_limit_depth {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert_eq!(perft(depth, &mut pos), *expected,);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user