Compare commits

...

10 Commits

Author SHA1 Message Date
1c4c158053 perf: borrow in the evalscore piece changes 2024-11-03 11:42:19 -05:00
4b245e0421 chore: silence warnings
and also move perft tests into its own file
2024-11-03 11:32:13 -05:00
b51dc87b2c fix: make promoting pawns more important 2024-11-02 22:23:41 -04:00
7d7a2531ad feat: tapered eval for endgame 2024-11-02 22:16:21 -04:00
da6b3f20f9 test: alpha-beta results are the same as without 2024-11-02 21:53:54 -04:00
96b4816f84 feat: some extra uci info about the best line, etc 2024-11-02 19:33:29 -04:00
b7b3c6c5b8 fix: not going for checkmate
engine will now prioritize closer mates rather than continuously going
"ah i'll checkmate next move" then never checkkmating
2024-11-02 16:05:04 -04:00
b36faba3ef feat: piece square table
no tapered eval yet
2024-11-02 14:45:57 -04:00
9b447ca039 refactor: simplify move gen public interface 2024-11-01 21:58:38 -04:00
60d084886f refactor: use Index/IndexMut instead of helper functions 2024-11-01 21:46:53 -04:00
7 changed files with 702 additions and 196 deletions

View File

@ -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"),

View File

@ -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:?}")
}

View File

@ -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);
}
}

View File

@ -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()])

View File

@ -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,);
}
}
}
}

View File

@ -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
View 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,);
}
}
}