Compare commits
5 Commits
c2dcba81b8
...
39d5ebc2b3
Author | SHA1 | Date | |
---|---|---|---|
39d5ebc2b3 | |||
7d0d81905e | |||
5751215ffa | |||
3ebadf995f | |||
420e32fe86 |
@ -4,6 +4,8 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
|
|
||||||
|
default-run = "engine"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
133
src/bin/engine.rs
Normal file
133
src/bin/engine.rs
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
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>
|
||||||
|
*/
|
||||||
|
|
||||||
|
//! 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::Board;
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
/// UCI protocol says to ignore any unknown words.
|
||||||
|
///
|
||||||
|
/// This macro exists to avoid copy-pasting this explanation everywhere.
|
||||||
|
macro_rules! ignore {
|
||||||
|
() => {
|
||||||
|
continue
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// UCI engine metadata query.
|
||||||
|
fn cmd_uci() -> String {
|
||||||
|
let str = "id name chess_inator\n\
|
||||||
|
id author dogeystamp\n\
|
||||||
|
uciok";
|
||||||
|
str.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the `moves` after setting an initial position.
|
||||||
|
fn cmd_position_moves(mut tokens: std::str::SplitWhitespace<'_>, mut board: Board) -> Board {
|
||||||
|
while let Some(token) = tokens.next() {
|
||||||
|
match token {
|
||||||
|
"moves" => {
|
||||||
|
for mv in tokens.by_ref() {
|
||||||
|
let mv = Move::from_uci_algebraic(mv).unwrap();
|
||||||
|
let _ = mv.make(&mut board);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => ignore!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
board
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the position.
|
||||||
|
fn cmd_position(mut tokens: std::str::SplitWhitespace<'_>) -> Board {
|
||||||
|
while let Some(token) = tokens.next() {
|
||||||
|
match token {
|
||||||
|
"fen" => {
|
||||||
|
let mut fen = String::with_capacity(64);
|
||||||
|
// fen is 6 whitespace-delimited fields
|
||||||
|
for i in 0..6 {
|
||||||
|
fen.push_str(tokens.next().expect("FEN missing fields"));
|
||||||
|
if i < 5 {
|
||||||
|
fen.push(' ')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let board = Board::from_fen(&fen)
|
||||||
|
.unwrap_or_else(|e| panic!("failed to parse fen '{fen}': {e:?}"));
|
||||||
|
let board = cmd_position_moves(tokens, board);
|
||||||
|
|
||||||
|
return board;
|
||||||
|
}
|
||||||
|
"startpos" => {
|
||||||
|
let board = Board::starting_pos();
|
||||||
|
let board = cmd_position_moves(tokens, board);
|
||||||
|
|
||||||
|
return board;
|
||||||
|
}
|
||||||
|
_ => ignore!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
panic!("position command was empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Play the game.
|
||||||
|
fn cmd_go(mut _tokens: std::str::SplitWhitespace<'_>, board: &mut Board) {
|
||||||
|
let chosen = best_move(board);
|
||||||
|
match chosen {
|
||||||
|
Some(mv) => println!("bestmove {}", mv.to_uci_algebraic()),
|
||||||
|
None => println!("bestmove 0000"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let stdin = io::stdin();
|
||||||
|
|
||||||
|
let mut board = Board::starting_pos();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut line = String::new();
|
||||||
|
stdin.read_line(&mut line).unwrap();
|
||||||
|
let mut tokens = line.split_whitespace();
|
||||||
|
while let Some(token) = tokens.next() {
|
||||||
|
match token {
|
||||||
|
"uci" => {
|
||||||
|
println!("{}", cmd_uci());
|
||||||
|
}
|
||||||
|
"isready" => {
|
||||||
|
println!("readyok");
|
||||||
|
}
|
||||||
|
"ucinewgame" => {
|
||||||
|
board = Board::starting_pos();
|
||||||
|
}
|
||||||
|
"quit" => {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
"position" => {
|
||||||
|
board = cmd_position(tokens);
|
||||||
|
}
|
||||||
|
"go" => {
|
||||||
|
cmd_go(tokens, &mut board);
|
||||||
|
}
|
||||||
|
_ => ignore!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
72
src/eval.rs
Normal file
72
src/eval.rs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
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>
|
||||||
|
*/
|
||||||
|
|
||||||
|
//! Position evaluation.
|
||||||
|
|
||||||
|
use crate::{Board, Color, N_PIECES};
|
||||||
|
|
||||||
|
/// Signed centipawn type.
|
||||||
|
///
|
||||||
|
/// Positive is good for White, negative good for Black.
|
||||||
|
pub type EvalInt = i16;
|
||||||
|
|
||||||
|
pub trait Eval {
|
||||||
|
/// Evaluate a position and assign it a score.
|
||||||
|
fn eval(&self) -> EvalInt;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eval for Board {
|
||||||
|
fn eval(&self) -> EvalInt {
|
||||||
|
use crate::Piece::*;
|
||||||
|
let mut score: EvalInt = 0;
|
||||||
|
|
||||||
|
// scores in centipawns for each piece
|
||||||
|
let material_score: [EvalInt; N_PIECES] = [
|
||||||
|
500, // rook
|
||||||
|
300, // bishop
|
||||||
|
300, // knight
|
||||||
|
20000, // king
|
||||||
|
900, // queen
|
||||||
|
100, // pawn
|
||||||
|
];
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
score += material_score[pc as usize] * tally;
|
||||||
|
}
|
||||||
|
|
||||||
|
score
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::fen::FromFen;
|
||||||
|
|
||||||
|
/// Sanity check.
|
||||||
|
#[test]
|
||||||
|
fn test_eval() {
|
||||||
|
let board1 = Board::from_fen("4k3/8/8/8/8/8/PPPPPPPP/RNBQKBNR w KQ - 0 1").unwrap();
|
||||||
|
let eval1 = board1.eval();
|
||||||
|
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}");
|
||||||
|
}
|
||||||
|
}
|
@ -16,8 +16,10 @@ Copyright © 2024 dogeystamp <dogeystamp@disroot.org>
|
|||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
pub mod eval;
|
||||||
pub mod fen;
|
pub mod fen;
|
||||||
pub mod movegen;
|
pub mod movegen;
|
||||||
|
pub mod search;
|
||||||
|
|
||||||
use crate::fen::{FromFen, ToFen, START_POSITION};
|
use crate::fen::{FromFen, ToFen, START_POSITION};
|
||||||
|
|
||||||
|
@ -39,6 +39,29 @@ impl From<PromotePiece> for Piece {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<PromotePiece> for char {
|
||||||
|
fn from(value: PromotePiece) -> Self {
|
||||||
|
Piece::from(value).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NonPromotePiece;
|
||||||
|
|
||||||
|
impl TryFrom<Piece> for PromotePiece {
|
||||||
|
type Error = NonPromotePiece;
|
||||||
|
|
||||||
|
fn try_from(value: Piece) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
Piece::Rook => Ok(PromotePiece::Rook),
|
||||||
|
Piece::Bishop => Ok(PromotePiece::Bishop),
|
||||||
|
Piece::Knight => Ok(PromotePiece::Knight),
|
||||||
|
Piece::Queen => Ok(PromotePiece::Queen),
|
||||||
|
Piece::King => Err(NonPromotePiece),
|
||||||
|
Piece::Pawn => Err(NonPromotePiece),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
enum AntiMoveType {
|
enum AntiMoveType {
|
||||||
Normal,
|
Normal,
|
||||||
@ -73,7 +96,7 @@ pub struct AntiMove {
|
|||||||
|
|
||||||
impl AntiMove {
|
impl AntiMove {
|
||||||
/// Undo the move.
|
/// Undo the move.
|
||||||
fn unmake(self, pos: &mut Board) {
|
pub fn unmake(self, pos: &mut Board) {
|
||||||
pos.move_piece(self.dest, self.src);
|
pos.move_piece(self.dest, self.src);
|
||||||
pos.half_moves = self.half_moves;
|
pos.half_moves = self.half_moves;
|
||||||
pos.castle = self.castle;
|
pos.castle = self.castle;
|
||||||
@ -145,7 +168,7 @@ pub struct Move {
|
|||||||
|
|
||||||
impl Move {
|
impl Move {
|
||||||
/// Apply move to a position.
|
/// Apply move to a position.
|
||||||
fn make(self, pos: &mut Board) -> AntiMove {
|
pub fn make(self, pos: &mut Board) -> AntiMove {
|
||||||
let mut anti_move = AntiMove {
|
let mut anti_move = AntiMove {
|
||||||
dest: self.dest,
|
dest: self.dest,
|
||||||
src: self.src,
|
src: self.src,
|
||||||
@ -360,6 +383,8 @@ pub enum MoveAlgebraicError {
|
|||||||
InvalidLength(usize),
|
InvalidLength(usize),
|
||||||
/// Invalid character at given index.
|
/// Invalid character at given index.
|
||||||
InvalidCharacter(usize),
|
InvalidCharacter(usize),
|
||||||
|
/// Can't promote to a given piece (char at given index).
|
||||||
|
InvalidPromotePiece(usize),
|
||||||
/// Could not parse square string at a certain index.
|
/// Could not parse square string at a certain index.
|
||||||
SquareError(usize, SquareError),
|
SquareError(usize, SquareError),
|
||||||
}
|
}
|
||||||
@ -391,13 +416,12 @@ impl FromUCIAlgebraic for Move {
|
|||||||
|
|
||||||
if value_len == 5 {
|
if value_len == 5 {
|
||||||
let promote_char = value.as_bytes()[4] as char;
|
let promote_char = value.as_bytes()[4] as char;
|
||||||
match promote_char {
|
|
||||||
'q' => move_type = MoveType::Promotion(PromotePiece::Queen),
|
let err = Err(MoveAlgebraicError::InvalidCharacter(4));
|
||||||
'b' => move_type = MoveType::Promotion(PromotePiece::Bishop),
|
let pc = Piece::try_from(promote_char).or(err)?;
|
||||||
'n' => move_type = MoveType::Promotion(PromotePiece::Knight),
|
|
||||||
'r' => move_type = MoveType::Promotion(PromotePiece::Rook),
|
let err = Err(MoveAlgebraicError::InvalidPromotePiece(4));
|
||||||
_ => return Err(MoveAlgebraicError::InvalidCharacter(4)),
|
move_type = MoveType::Promotion(PromotePiece::try_from(pc).or(err)?);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Move {
|
Ok(Move {
|
||||||
@ -408,6 +432,17 @@ impl FromUCIAlgebraic for Move {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ToUCIAlgebraic for Move {
|
||||||
|
fn to_uci_algebraic(&self) -> String {
|
||||||
|
let prom_str = match self.move_type {
|
||||||
|
MoveType::Promotion(promote_piece) => char::from(promote_piece).to_string(),
|
||||||
|
_ => "".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
format!("{}{}{}", self.src, self.dest, prom_str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum MoveGenType {
|
pub enum MoveGenType {
|
||||||
/// Legal move generation.
|
/// Legal move generation.
|
||||||
@ -1350,6 +1385,15 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_uci_move_fmt() {
|
||||||
|
let test_cases = ["a1e5", "e7e8q", "e7e8r", "e7e8b", "e7e8n"];
|
||||||
|
for tc in test_cases {
|
||||||
|
let mv = Move::from_uci_algebraic(tc).unwrap();
|
||||||
|
assert_eq!(mv.to_uci_algebraic(), tc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The standard movegen test.
|
/// The standard movegen test.
|
||||||
///
|
///
|
||||||
/// See https://www.chessprogramming.org/Perft
|
/// See https://www.chessprogramming.org/Perft
|
||||||
|
81
src/search.rs
Normal file
81
src/search.rs
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
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>
|
||||||
|
*/
|
||||||
|
|
||||||
|
//! Game-tree search.
|
||||||
|
|
||||||
|
use crate::eval::{Eval, EvalInt};
|
||||||
|
use crate::movegen::{Move, MoveGen, MoveGenType, ToUCIAlgebraic};
|
||||||
|
use crate::Board;
|
||||||
|
use std::cmp::max;
|
||||||
|
|
||||||
|
// min can't be represented as positive
|
||||||
|
const EVAL_WORST: EvalInt = -(EvalInt::MAX);
|
||||||
|
|
||||||
|
/// Search the game tree to find the absolute (positive good) eval for the current player.
|
||||||
|
fn minmax(board: &mut Board, depth: usize) -> EvalInt {
|
||||||
|
if depth == 0 {
|
||||||
|
let eval = board.eval();
|
||||||
|
match board.turn {
|
||||||
|
crate::Color::White => return eval,
|
||||||
|
crate::Color::Black => return -eval,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mvs: Vec<_> = board.gen_moves(MoveGenType::Legal).into_iter().collect();
|
||||||
|
|
||||||
|
let mut abs_best = EVAL_WORST;
|
||||||
|
|
||||||
|
if mvs.is_empty() {
|
||||||
|
if board.is_check(board.turn) {
|
||||||
|
return EVAL_WORST;
|
||||||
|
} else {
|
||||||
|
// stalemate
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for mv in mvs {
|
||||||
|
let anti_mv = mv.make(board);
|
||||||
|
abs_best = max(abs_best, -minmax(board, depth - 1));
|
||||||
|
anti_mv.unmake(board);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
if abs_eval >= best_eval {
|
||||||
|
best_eval = abs_eval;
|
||||||
|
best_mv = Some(mv);
|
||||||
|
}
|
||||||
|
anti_mv.unmake(board);
|
||||||
|
}
|
||||||
|
|
||||||
|
best_mv
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the best move.
|
||||||
|
pub fn best_move(board: &mut Board) -> Option<Move> {
|
||||||
|
search(board)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user