Compare commits

...

5 Commits

Author SHA1 Message Date
39d5ebc2b3 fix: not escaping checkmates 2024-10-26 21:05:51 -04:00
7d0d81905e feat: basic basic search 2024-10-26 19:53:20 -04:00
5751215ffa feat: uci engine interface 2024-10-26 18:25:07 -04:00
3ebadf995f feat: move conversion to uci algebraic 2024-10-26 16:48:51 -04:00
420e32fe86 feat: basic basic eval 2024-10-26 16:43:06 -04:00
6 changed files with 343 additions and 9 deletions

View File

@ -4,6 +4,8 @@ version = "0.1.0"
edition = "2021"
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
[dependencies]

133
src/bin/engine.rs Normal file
View 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
View 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}");
}
}

View File

@ -16,8 +16,10 @@ Copyright © 2024 dogeystamp <dogeystamp@disroot.org>
use std::fmt::Display;
use std::str::FromStr;
pub mod eval;
pub mod fen;
pub mod movegen;
pub mod search;
use crate::fen::{FromFen, ToFen, START_POSITION};

View File

@ -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)]
enum AntiMoveType {
Normal,
@ -73,7 +96,7 @@ pub struct AntiMove {
impl AntiMove {
/// Undo the move.
fn unmake(self, pos: &mut Board) {
pub fn unmake(self, pos: &mut Board) {
pos.move_piece(self.dest, self.src);
pos.half_moves = self.half_moves;
pos.castle = self.castle;
@ -145,7 +168,7 @@ pub struct Move {
impl Move {
/// 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 {
dest: self.dest,
src: self.src,
@ -360,6 +383,8 @@ pub enum MoveAlgebraicError {
InvalidLength(usize),
/// Invalid character at given index.
InvalidCharacter(usize),
/// Can't promote to a given piece (char at given index).
InvalidPromotePiece(usize),
/// Could not parse square string at a certain index.
SquareError(usize, SquareError),
}
@ -391,13 +416,12 @@ impl FromUCIAlgebraic for Move {
if value_len == 5 {
let promote_char = value.as_bytes()[4] as char;
match promote_char {
'q' => move_type = MoveType::Promotion(PromotePiece::Queen),
'b' => move_type = MoveType::Promotion(PromotePiece::Bishop),
'n' => move_type = MoveType::Promotion(PromotePiece::Knight),
'r' => move_type = MoveType::Promotion(PromotePiece::Rook),
_ => return Err(MoveAlgebraicError::InvalidCharacter(4)),
}
let err = Err(MoveAlgebraicError::InvalidCharacter(4));
let pc = Piece::try_from(promote_char).or(err)?;
let err = Err(MoveAlgebraicError::InvalidPromotePiece(4));
move_type = MoveType::Promotion(PromotePiece::try_from(pc).or(err)?);
}
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)]
pub enum MoveGenType {
/// 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.
///
/// See https://www.chessprogramming.org/Perft

81
src/search.rs Normal file
View 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)
}