Skip to content

Commit

Permalink
chore: Store all the progress so far
Browse files Browse the repository at this point in the history
  • Loading branch information
kirillbobyrev committed Jun 2, 2024
1 parent afdba2d commit 6f873b5
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 124 deletions.
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion src/chess/position.rs
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,8 @@ impl Position {
// most usecases (e.g. for search) would clone the position and then mutate
// it anyway. This would prevent (im)mutability reference problems.
pub fn make_move(&mut self, next_move: &Move) {

Check warning on line 412 in src/chess/position.rs

View workflow job for this annotation

GitHub Actions / Build docs

missing documentation for a method

Check warning on line 412 in src/chess/position.rs

View workflow job for this annotation

GitHub Actions / Test Suite (ubuntu-latest, stable, false)

missing documentation for a method

Check warning on line 412 in src/chess/position.rs

View workflow job for this annotation

GitHub Actions / Test Suite (ubuntu-latest, nightly, true)

missing documentation for a method

Check warning on line 412 in src/chess/position.rs

View workflow job for this annotation

GitHub Actions / Test Suite (ubuntu-latest, 1.78.0, false)

missing documentation for a method

Check warning on line 412 in src/chess/position.rs

View workflow job for this annotation

GitHub Actions / Test Suite (macos-latest, stable, true)

missing documentation for a method

Check warning on line 412 in src/chess/position.rs

View workflow job for this annotation

GitHub Actions / Test Suite (macos-latest, nightly, true)

missing documentation for a method

Check warning on line 412 in src/chess/position.rs

View workflow job for this annotation

GitHub Actions / Test Suite (windows-latest, nightly, true)

missing documentation for a method

Check warning on line 412 in src/chess/position.rs

View workflow job for this annotation

GitHub Actions / Test Suite (windows-latest, stable, true)

missing documentation for a method
// debug_assert!(self.is_legal());
debug_assert!(self.is_legal());
// TODO: debug_assert!(self.is_legal_move(move));
let (us, they) = (self.us(), self.they());
let our_backrank = Rank::backrank(us);
let (our_pieces, their_pieces) = match self.us() {
Expand Down
130 changes: 74 additions & 56 deletions src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,26 @@
use core::panic;
use std::io::{BufRead, Write};

use crate::chess::position::Position;
use crate::{
chess::{core::Move, position::Position},
search::SearchState,
};

pub struct Engine {
/// The Engine manages all resources, keeps track of the time and handles
/// commands sent by UCI server.
pub struct Engine<'a, R: BufRead, W: Write> {
position: Position,
search_state: crate::search::SearchState,
input: &'a mut R,
output: &'a mut W,
}

impl Default for Engine {
fn default() -> Self {
Self::new()
}
}

impl Engine {
impl<'a, R: BufRead, W: Write> Engine<'a, R, W> {
#[must_use]
pub fn new() -> Self {
pub fn new(input: &'a mut R, output: &'a mut W) -> Self {

Check warning on line 26 in src/engine.rs

View workflow job for this annotation

GitHub Actions / Build docs

missing documentation for an associated function

Check warning on line 26 in src/engine.rs

View workflow job for this annotation

GitHub Actions / Test Suite (ubuntu-latest, stable, false)

missing documentation for an associated function

Check warning on line 26 in src/engine.rs

View workflow job for this annotation

GitHub Actions / Test Suite (ubuntu-latest, nightly, true)

missing documentation for an associated function

Check warning on line 26 in src/engine.rs

View workflow job for this annotation

GitHub Actions / Test Suite (ubuntu-latest, 1.78.0, false)

missing documentation for an associated function

Check warning on line 26 in src/engine.rs

View workflow job for this annotation

GitHub Actions / Test Suite (macos-latest, stable, true)

missing documentation for an associated function

Check warning on line 26 in src/engine.rs

View workflow job for this annotation

GitHub Actions / Test Suite (macos-latest, nightly, true)

missing documentation for an associated function

Check warning on line 26 in src/engine.rs

View workflow job for this annotation

GitHub Actions / Test Suite (windows-latest, nightly, true)

missing documentation for an associated function

Check warning on line 26 in src/engine.rs

View workflow job for this annotation

GitHub Actions / Test Suite (windows-latest, stable, true)

missing documentation for an associated function
Self {
position: Position::starting(),
search_state: crate::search::SearchState::new(),
input,
output,
}
}

Expand All @@ -41,66 +42,75 @@ impl Engine {
/// Reads UCI commands from the input stream and executes them accordingly
/// while writing the responses to the output stream.
///
/// The minimal set of supported commands is:
/// The minimal set of supported commands should be:
///
/// - uci
/// - isready
/// - go
/// - go wtime btime winc binc
/// - go wtime btime winc binc movetime infinite
/// - quit
/// - ucinewgame
/// - setoption
// TODO: Document the expected behavior.
/// - stop (?)
///
/// NOTE: The assumption is that the UCI input stream is **correct**. It is
/// tournament manager's responsibility to send uncorrupted input and make
/// sure that the commands are in valid format. The engine won't spend too
/// much time and effort on error recovery. If a command is not valid or
/// unsupported yet, it will just be skipped.
///
/// For example, if the UCI server sends a corrupted position or illegal
/// moves to the engine, the behavior is undefined.
// > The engine must always be able to process input from stdin, even while
// > thinking.
pub fn uci_loop(
&mut self,
input: &mut impl BufRead,
output: &mut impl Write,
) -> anyhow::Result<()> {
pub fn uci_loop(&mut self) -> anyhow::Result<()> {
loop {
let mut line = String::new();

match input.read_line(&mut line) {
match self.input.read_line(&mut line) {
// EOF reached.
Ok(0) => break,
Ok(_) => {},
Err(e) => panic!("Error reading from input: {}", e),
}

let tokens: Vec<&str> = line.split_whitespace().collect();
let mut stream = tokens.iter();

match tokens.first() {
match stream.next() {
// `uci` is the first command sent to the engine. The response
// should be `id` and `uciok` followed by all supported options.
Some(&"uci") => {
writeln!(
output,
self.output,
"id name {} {}",
env!("CARGO_PKG_NAME"),
crate::get_version()
)?;
writeln!(output, "id author {}", env!("CARGO_PKG_AUTHORS"))?;
writeln!(output, "uciok")?;
writeln!(self.output, "id author {}", env!("CARGO_PKG_AUTHORS"))?;
writeln!(self.output, "uciok")?;

// These options don't mean anything for now.
writeln!(
output,
self.output,
"option name Threads type spin default 1 min 1 max 1"
)?;
writeln!(output, "option name Hash type spin default 1 min 1 max 1")?;
writeln!(
self.output,
"option name Hash type spin default 1 min 1 max 1"
)?;
},
// This is a "health check" command. It is usually used to wait
// for the engine to load necessary files (tablebase, eval
// weights) or to check that the engine is responsive.
Some(&"isready") => {
println!("readyok");
writeln!(self.output, "readyok")?;
},
// Sets the engine parameter.
// TODO: Add support for threads, hash size, Syzygy tablebase
// path.
Some(&"setoption") => {
writeln!(
output,
write!(
self.output,
"info string `setoption` is no-op for now: received command {line}"
)?;
},
Expand All @@ -109,59 +119,67 @@ impl Engine {
// same as `stop`.
Some(&"ucinewgame") => {
// TODO: Stop search, reset the board, etc.
todo!();
},
// Sets up the position search will start from.
Some(&"position") => {
if tokens.len() < 2 {
writeln!(output, "info string Missing position specification")?;
continue;
}
// Set the position.
match tokens[1] {
"startpos" => {
match stream.next() {
Some(&"startpos") => {
self.position = Position::starting();
},
"fen" => {
Some(&"fen") => {
const FEN_SIZE: usize = 6;
if tokens.len() < 2 + FEN_SIZE {
writeln!(
output,
self.output,
"info string FEN consists of 6 pieces, got {}",
tokens.len() - 2
)?;
}
},
_ => {
writeln!(
output,
"info string Expected position [fen <fenstring> | startpos] moves
<move1> ... <move_i>, got: {line}"
write!(
self.output,
"info string Expected `position [fen <fenstring> | startpos] moves
<move1> ... <move_i>`, got: {line}"
)?;
},
}
if tokens.len() > 2 && tokens[2] == "moves" {
// Handle moves
for token in tokens.iter().skip(3) {
// Process the move
todo!();
}
match stream.next() {
Some(&"moves") => {
for next_move in stream {
match Move::from_uci(next_move) {
Ok(next_move) => self.position.make_move(&next_move),
Err(e) => writeln!(
self.output,
"info string Unexpected UCI move: {e}"
)?,
}
}
},
_ => continue,
}
},
//
Some(&"go") => {
todo!();
// TODO: Handle the time and at least save it for now.
let mut state = SearchState::new();
let MAX_DEPTH = 3;
state.reset(&self.position);
let search_result = crate::search::minimax::negamax(
&mut state,
MAX_DEPTH,
&crate::evaluation::material::material_advantage,
);
writeln!(self.output, "bestmove {}", search_result.best_move.unwrap())?;
},
// TODO: Stop calculating as soon as possible.
Some(&"stop") => {
todo!();
},
Some(&"stop") => {},
Some(&"quit") => {
// TODO: Stop the search.
break;
},
Some(&command) => {
writeln!(output, "info string Unsupported command: {command}")?;
writeln!(self.output, "info string Unsupported command: {command}")?;
},
None => {},
}
Expand Down
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,14 @@ pub(crate) fn get_version() -> String {
)
}

/// Prints main information about the engine to standard output.
pub fn print_engine_info() {
println!("Pabi Chess Engine");
println!("Version {}", get_version());
println!("https://github.com/kirillbobyrev/pabi");
}

/// Prints information about the binary to the standard output. This includes
/// the version, build type and what features are enabled.
/// Prints information about how the binary was built to the standard output.
pub fn print_binary_info() {
println!("Debug: {}", shadow_rs::is_debug());
println!("Features: {FEATURES}");
Expand Down
6 changes: 4 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ fn main() -> anyhow::Result<()> {
pabi::print_engine_info();
pabi::print_binary_info();

let mut engine = pabi::Engine::new();
engine.uci_loop(&mut std::io::stdin().lock(), &mut std::io::stdout())
let mut input = std::io::stdin().lock();
let mut output = std::io::stdout().lock();
let mut engine = pabi::Engine::new(&mut input, &mut output);
engine.uci_loop()
}
84 changes: 62 additions & 22 deletions src/search/minimax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,53 +8,93 @@
// TODO: Implement alpha-beta pruning.
// TODO: Implement move ordering.

use std::num::NonZeroI16;

use crate::chess::position::Position;
use crate::evaluation::Value;
use crate::search::Score;
use crate::search::SearchResult;

// TODO: Document.
pub(crate) fn negamax(
state: &mut crate::search::SearchState,
depth: u8,
static_evaluator: &dyn Fn(&Position) -> Value,
) -> Score {
assert!(!state.stack.is_empty());
) -> SearchResult {
debug_assert!(!state.stack.is_empty());
let position = state.stack.last_mut().unwrap();
if depth == 0 {
let value = static_evaluator(position);
let _ = state.stack.pop();
return Score::Relative(value);
}
if position.is_checkmate() {
//
let moves = NonZeroI16::new(depth as i16 / 2 + 1).unwrap();
// Players alternate turns every ply and the root node is player's turn.
let moves = (depth + 1) as i16 / 2;
let win = depth % 2 == 1;
let value = if win {
Score::Checkmate(moves)
} else {
Score::Checkmate(-moves)

let _ = state.stack.pop();
return SearchResult {
score: Score::Checkmate(if win { moves } else { -moves }),
best_move: None,
};
}
if position.is_stalemate() {
let _ = state.stack.pop();
// TODO: Maybe handle stalemate differently since it's a "precise"
// evaluation.
return SearchResult {
score: Score::Relative(0),
best_move: None,
};
}
if depth == 0 {
let value = static_evaluator(position);
let _ = state.stack.pop();
return value;
return SearchResult {
score: Score::Relative(value),
best_move: None,
};
}
// TODO: Check if the position is terminal (checkmate or stalemate).
// TODO: Check transposition table for existing evaluation.
// TODO: Check tablebase for existing evaluation.
let mut best_value = Score::Relative(Value::MIN);
let mut best_result = SearchResult {
score: Score::Relative(Value::MIN),
best_move: None,
};
// TODO: Do not copy here, figure out how to beat the borrow checker.
let current_position = state.stack.last().unwrap().clone();
for next_move in current_position.generate_moves() {
let mut new_position = current_position.clone();
new_position.make_move(&next_move);
state.stack.push(new_position);
let value = -negamax(state, depth - 1, static_evaluator);
if value > best_value {
best_value = value;

let mut search_result = negamax(state, depth - 1, static_evaluator);
search_result.score = -search_result.score;

// Update the best score and move that achieves it if the explored move
// leads to the best result so far.
if search_result.score > best_result.score {
best_result.score = search_result.score;
best_result.best_move = Some(next_move);
}
}
let _ = state.stack.pop();
best_value
best_result
}

#[cfg(test)]
mod test {
use crate::{evaluation::material::material_advantage, search::SearchState};

use super::*;

#[test]
fn starting_position() {
let mut state = SearchState::new();
state.stack.push(Position::starting());
assert_eq!(
negamax(&mut state, 0, &material_advantage),
SearchResult {
score: Score::Relative(0),
best_move: None
}
);
}

#[test]
fn losing_position() {}
}
Loading

0 comments on commit 6f873b5

Please sign in to comment.