chess_uci/chess_uci/src/lib.rs

422 lines
13 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#![allow(dead_code)]
/// Manage UCI messages and link into chess component
pub mod uci_command;
use crate::uci_command::*;
use log::{info, warn};
use std::io::*;
use std::result::Result;
use std::time::Duration;
use chess::{Board, ChessMove, Game, Action};
const DEFAULT_TIME: (Option<Duration>, Option<Duration>) =
(Some(Duration::from_secs(90 * 60)), None);
/// Structure used to manage a chess game with a uci engine.
///
/// It needs a in / out communication channel to read / push line by line uci commands
///c
/// ```rust
/// use chess_uci::UciEngine;
/// use std::process::{Command, Stdio};
/// use std::io::BufReader;
///
/// let process = match Command::new("stockfish")
/// .stdin(Stdio::piped())
/// .stdout(Stdio::piped())
/// .spawn() {
/// Err(why) => panic!("couldn't spawn stockfish: {}", why),
/// Ok(process) => process,
/// };
/// let (sf_in,sf_out) = (process.stdin.expect("Program stdin"), process.stdout.expect("Program stdout"));
///
/// let mut uci = UciEngine::new(BufReader::new(sf_out), sf_in);
/// // .. uci.init(); ..
/// uci.push_raw("quit\n");
/// ```
pub struct UciEngine<Fi: Read, Fo: Write> {
source: Option<BufReader<Fi>>,
destination: Option<Fo>,
uciok: bool,
id: Id,
initial: Board,
game: Game,
/// white (total, incr), black (total, incr)
timer: [(Option<Duration>, Option<Duration>); 2],
initial_timer: [(Option<Duration>, Option<Duration>); 2],
/// white, black
player: [Player; 2],
}
pub enum UciOption {
Ponder { value: bool },
UCIElo { value: Option<u32> },
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Player {
Human { elo: Option<u32> },
Engine { elo: Option<u32> },
}
pub enum GameOption {
TotalTime { color: Color, value: Option<Duration> },
Increment { color: Color, value: Option<Duration> },
Player {color: Color, value: Player},
}
impl<Fi: Read, Fo: Write> UciEngine<Fi, Fo> {
/// Create new game manager
///
/// Requires line by line input and output streams to communicate with uci engine
pub fn new(source: Option<Fi>, destination: Option<Fo>) -> UciEngine<Fi, Fo> {
UciEngine::<Fi, Fo> {
source: source.map(|x| BufReader::new(x)),
destination,
id: Id::default(),
uciok: false,
initial: Board::default(),
game: Game::new(),
timer: [DEFAULT_TIME; 2],
initial_timer: [DEFAULT_TIME; 2],
player: [Player::Human { elo: None }; 2],
}
}
pub fn game_option(&mut self, option: GameOption) -> GameOption {
let old_value: GameOption;
match option {
GameOption::TotalTime { color, value } => {
old_value = GameOption::TotalTime {
color,
value: self.timer[color.to_index()].0,
};
self.timer[color.to_index()].0 = value;
self.initial_timer[color.to_index()].0 = value;
}
GameOption::Increment { color, value } => {
old_value = GameOption::Increment {
color,
value: self.timer[color.to_index()].1,
};
self.timer[color.to_index()].1 = value;
self.initial_timer[color.to_index()].1 = value;
}
GameOption::Player { color, value } => {
old_value = GameOption::Player {
color,
value: self.player[color.to_index()],
};
self.player[color.to_index()] = value;
}
}
old_value
}
pub fn get_player(&self, color: Color) -> Player {
self.player[color.to_index()]
}
/// Launch uci engine initialisation
///
/// Retrieve data from uci engine (until uciok command from engine)
pub fn init(&mut self) {
self.push(GuiCommand::Uci);
// Consume commands until uciok messages
if self.source.is_some() {
while !self.uciok {
self.pull();
}
}
}
/// Initialize a new game
///
/// Set a new board internally and tell the engine
pub fn new_game(&mut self) {
self.initial = Board::default();
self.timer = self.initial_timer;
self.game = Game::new_with_board(self.initial);
self.push(GuiCommand::Stop);
self.push(GuiCommand::UciNewGame);
self.push(GuiCommand::Position {
position: Some(self.initial),
moves: self.game.actions().to_vec(),
});
}
/// Play a move
///
/// Update internal game reference and tell the uci engine
///
/// ```rust
/// use chess::ChessMove;
/// use std::str::FromStr;
/// use chess_uci::*;
/// use std::io;
///
/// let mut uci = UciEngine::new(io::empty(), io::sink());
///
/// uci.make_move(ChessMove::from_str("e2e4").expect("error converting e2e4"));
/// ```
pub fn make_move(&mut self, chess_move: &ChessMove) -> bool {
if self.game.make_move(*chess_move) {
self.push(GuiCommand::Position {
position: Some(self.initial),
moves: self.game.actions().to_vec(),
});
true
} else {
false
}
}
pub fn back_move(&mut self) -> Option<ChessMove> {
let mut actions = self.game.actions().clone();
let mut last_move = None;
loop{
match actions.pop() {
Some(Action::MakeMove(last)) => {
last_move = Some(last);
break;
},
None => break,
_ => continue,
}
}
self.new_game();
for action in actions {
if let Action::MakeMove(chessmove) = action {
self.make_move(&chessmove);
}
}
last_move
}
fn terminate(&mut self, reason: &str) {
self.uciok = false;
info!("UCI termination: {}", reason);
}
/// Provides the name of the uci engine
pub fn name(&self) -> Option<String> {
self.id.name()
}
/// Provides the author sof the uci engine
pub fn author(&self) -> Option<String> {
self.id.author()
}
fn update(&mut self, id: &Id) {
if self.is_uciok() {
warn!("Engine info should not be updated now (uciok)");
}
self.id.update(id);
}
fn update_from_str(&mut self, name: Option<&str>, author: Option<&str>) {
if self.is_uciok() {
warn!("Engine info should not be updated now (uciok)");
}
self.id.update_from_str(name, author);
}
/// Execute a uci command
///
/// if `EngineCommand::Id`: update name or authorship of the engine
/// if `EngineCommand::UciOk`: end engine initialization phase
pub fn exec(&mut self, command: &str) -> Result<EngineCommand, &'static str> {
let uci_command = parse(&mut command.to_string());
match uci_command.clone() {
Ok(command) => self.exec_command(&command),
Err(_) => warn!("Not a command"),
};
uci_command
}
pub fn exec_command(&mut self, command: &EngineCommand) {
match command {
EngineCommand::Id { id } => self.update(&id),
EngineCommand::UciOk => self.uciok(),
EngineCommand::Opt { options: _ } => {}
EngineCommand::Info { infos: _ }
| EngineCommand::BestMove {
best_move: _,
ponder: _,
}
| EngineCommand::ReadyOk => {}
EngineCommand::Registration | EngineCommand::CopyProtection => {
unimplemented!("command not implemented")
}
}
}
/// Retrieve a line from the uci engine input stream and parse it
pub fn pull(&mut self) -> Option<EngineCommand> {
if let Some(source) = &mut self.source {
let mut command = String::new();
match source.read_line(&mut command) {
Ok(0) => {
self.terminate("Chess engine closed connection.");
None
}
Ok(_) => {
info!("← {}", command);
self.exec(&command).ok()
}
Err(reason) => {
warn!("Unable to read from engine: {reason}");
None
}
}
} else {
info!("❌ No connected uci engine");
None
}
}
/// Read a line from the uci engine input stream (do not parse)
pub fn pull_raw(&mut self) -> Option<String> {
if let Some(source) = &mut self.source {
let mut data = String::new();
match source.read_line(&mut data) {
Ok(0) => {
self.terminate("Chess engine closed connection.");
None
}
Ok(_) => {
info!("↜ {}", data);
Some(data.clone())
}
Err(reason) => {
warn!("Unable to read from engine: {reason}");
None
}
}
} else {
info!("❌ No connected uci engine");
None
}
}
/// Push a Uci Gui command to the engine
pub fn push(&mut self, command: GuiCommand) {
if let Some(destination) = &mut self.destination {
let command_str = command.to_string();
match destination.write(command_str.as_bytes()) {
Ok(n) if n == command_str.len() => info!("→ gui: {command_str}"),
Ok(n) => warn!("⚠ gui: {command_str} truncated at {n}"),
Err(reason) => warn!("Unable to send command {command_str}: {reason}"),
}
} else {
info!("❌ No connected uci engine");
}
}
/// Push string (not a game manager integrated command) to the Uci engine output stream
pub fn push_raw(&mut self, data: &str) {
if let Some(destination) = &mut self.destination {
match destination.write(data.as_bytes()) {
Ok(n) if n == data.len() => info!("↝ raw: {data}"),
Ok(n) => warn!("⚠ raw: {data} truncated at {n}"),
Err(reason) => warn!("Unable to send raw {data}: {reason}"),
}
} else {
info!("❌ No connected uci engine");
}
}
}
impl<Fi: Read, Fo: Write> UciEngine<Fi, Fo> {
fn uciok(&mut self) {
self.uciok = true;
}
/// Tell whether the uci engine has initialized
pub fn is_uciok(&self) -> bool {
self.uciok
}
pub fn human_play(&mut self, chess_move: &ChessMove) -> Result<ChessMove, &'static str> {
match self.player[self.side_to_move().to_index()] {
Player::Human { .. } => {
if self.make_move(chess_move) {
Ok(*chess_move)
} else {
Err("Invalid move for human player")
}
}
Player::Engine { .. } => Err("Not a human to play for current color."),
}
}
pub fn go(&mut self) {
match self.player[self.side_to_move().to_index()] {
Player::Human { .. } => warn!("Not a machine to play for current color."),
Player::Engine { elo } => {
if self.is_uciok() {
if let Some(elo) = elo {
self.push(GuiCommand::SetOption {
option: Opt::UCILimitStrength { value: true },
});
self.push(GuiCommand::SetOption {
option: Opt::UCIElo { value: elo },
});
}
let (wtime, wincr) = self.timer[Color::White.to_index()];
let (btime, bincr) = self.timer[Color::Black.to_index()];
self.push(GuiCommand::Go {
wtime,
wincr,
btime,
bincr,
});
} else {
warn!("UCI engine not ready")
}
}
}
}
pub fn stop(&mut self) {
self.push(GuiCommand::Stop);
}
}
use chess::{Color, Piece, Square};
impl<Fi: Read, Fo: Write> UciEngine<Fi, Fo> {
pub fn piece_on(&self, square: Square) -> Option<Piece> {
self.game.current_position().piece_on(square)
}
pub fn color_on(&self, square: Square) -> Option<Color> {
self.game.current_position().color_on(square)
}
pub fn current_position(&self) -> Board {
self.game.current_position()
}
pub fn side_to_move(&self) -> Color {
self.game.side_to_move()
}
pub fn clock(&self, color: Color) -> Option<Duration> {
self.timer[if color == Color::White {0} else {1}].0
}
pub fn update_clock(&mut self, elapsed: Duration) {
let index = if self.side_to_move() == Color::White {0} else {1};
self.timer[index] =
match self.timer[index] {
(Some(base), None) => (Some(base - elapsed), None),
(Some(base), Some(incr)) if base >= elapsed => (Some(base - elapsed + incr), Some(incr)),
_ => (None, None)
};
}
}
// LocalWords: uci