use std::path::PathBuf;
use std::sync::{Arc, Mutex, MutexGuard};
use std::time::{Duration, Instant};
use pyo3::create_exception;
use pyo3::exceptions::{PyException, PyOSError, PyStopIteration, PyValueError};
use pyo3::prelude::*;
use pyo3::types::PyDict;
use crate::game::Game;
use crate::uci::{self, Info, Limits, Message, OptionKind, OptionSpec, OptionValue, Progress};
use super::board::{PyGame, PyMove};
use super::convert::castling_output_from;
create_exception!(
esca.uci,
UciError,
PyException,
"The base of every error talking to an engine raises."
);
create_exception!(
esca.uci,
EngineTimeout,
UciError,
"The engine did not say what was awaited in time."
);
create_exception!(
esca.uci,
EngineDied,
UciError,
"The engine closed its output or exited."
);
create_exception!(
esca.uci,
ProtocolError,
UciError,
"The engine broke the order of the conversation."
);
fn to_py_error(error: uci::Error) -> PyErr {
let message = error.to_string();
match error {
uci::Error::Timeout { .. } => EngineTimeout::new_err(message),
uci::Error::Died { .. } => EngineDied::new_err(message),
uci::Error::Protocol(_) | uci::Error::NotIdentified => ProtocolError::new_err(message),
uci::Error::NoSuchOption(_) | uci::Error::BadValue { .. } => PyValueError::new_err(message),
uci::Error::Io(_) => PyOSError::new_err(message),
}
}
fn seconds(value: f64, what: &str) -> PyResult<Duration> {
if !value.is_finite() || value < 0.0 {
return Err(PyValueError::new_err(format!(
"{what} is a count of seconds, not {value}"
)));
}
Ok(Duration::from_secs_f64(value))
}
type Shared = Arc<Mutex<Option<uci::Engine>>>;
fn locked(shared: &Shared) -> MutexGuard<'_, Option<uci::Engine>> {
shared.lock().unwrap_or_else(|held| held.into_inner())
}
fn on_engine<T: Send>(
py: Python<'_>,
shared: &Shared,
work: impl FnOnce(&mut uci::Engine) -> Result<T, uci::Error> + Send,
) -> PyResult<T> {
py.detach(|| {
let mut guard = locked(shared);
let engine = guard
.as_mut()
.ok_or_else(|| EngineDied::new_err("the engine has been closed"))?;
work(engine).map_err(to_py_error)
})
}
#[pyclass(frozen, from_py_object, module = "esca.uci", name = "Limits")]
#[derive(Clone)]
pub struct PyLimits {
pub(crate) inner: Limits,
}
#[pymethods]
impl PyLimits {
#[new]
#[pyo3(signature = (
*,
depth = None,
nodes = None,
movetime = None,
mate = None,
infinite = false,
ponder = false,
white_time = None,
black_time = None,
white_increment = None,
black_increment = None,
moves_to_go = None,
search_moves = None,
))]
#[allow(clippy::too_many_arguments)]
fn py_new(
depth: Option<u32>,
nodes: Option<u64>,
movetime: Option<f64>,
mate: Option<u32>,
infinite: bool,
ponder: bool,
white_time: Option<f64>,
black_time: Option<f64>,
white_increment: Option<f64>,
black_increment: Option<f64>,
moves_to_go: Option<u32>,
search_moves: Option<Vec<String>>,
) -> PyResult<PyLimits> {
let time = |value: Option<f64>, what: &str| -> PyResult<Option<Duration>> {
value.map(|value| seconds(value, what)).transpose()
};
Ok(PyLimits {
inner: Limits {
search_moves: search_moves.unwrap_or_default(),
ponder,
white_time: time(white_time, "white_time")?,
black_time: time(black_time, "black_time")?,
white_increment: time(white_increment, "white_increment")?,
black_increment: time(black_increment, "black_increment")?,
moves_to_go,
depth,
nodes,
mate,
movetime: time(movetime, "movetime")?,
infinite,
},
})
}
#[getter]
fn depth(&self) -> Option<u32> {
self.inner.depth
}
#[getter]
fn nodes(&self) -> Option<u64> {
self.inner.nodes
}
#[getter]
fn movetime(&self) -> Option<f64> {
self.inner.movetime.map(|time| time.as_secs_f64())
}
#[getter]
fn mate(&self) -> Option<u32> {
self.inner.mate
}
#[getter]
fn infinite(&self) -> bool {
self.inner.infinite
}
#[getter]
fn ponder(&self) -> bool {
self.inner.ponder
}
#[getter]
fn white_time(&self) -> Option<f64> {
self.inner.white_time.map(|time| time.as_secs_f64())
}
#[getter]
fn black_time(&self) -> Option<f64> {
self.inner.black_time.map(|time| time.as_secs_f64())
}
#[getter]
fn white_increment(&self) -> Option<f64> {
self.inner.white_increment.map(|time| time.as_secs_f64())
}
#[getter]
fn black_increment(&self) -> Option<f64> {
self.inner.black_increment.map(|time| time.as_secs_f64())
}
#[getter]
fn moves_to_go(&self) -> Option<u32> {
self.inner.moves_to_go
}
#[getter]
fn search_moves(&self) -> Vec<String> {
self.inner.search_moves.clone()
}
fn __repr__(&self) -> String {
format!("<Limits {}>", uci::Command::Go(self.inner.clone()))
}
}
#[pyclass(frozen, skip_from_py_object, module = "esca.uci", name = "Option")]
#[derive(Clone)]
pub struct PyOption {
inner: OptionSpec,
}
#[pymethods]
impl PyOption {
#[getter]
fn name(&self) -> String {
self.inner.name.clone()
}
#[getter]
fn r#type(&self) -> &'static str {
self.inner.kind.type_name()
}
#[getter]
fn default(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
Ok(match &self.inner.kind {
OptionKind::Check { default } => default.into_pyobject(py)?.into_any().unbind(),
OptionKind::Spin { default, .. } => default.into_pyobject(py)?.into_any().unbind(),
OptionKind::Combo { default, .. } | OptionKind::String { default } => {
default.clone().into_pyobject(py)?.into_any().unbind()
}
OptionKind::Button => py.None(),
})
}
#[getter]
fn min(&self) -> Option<i64> {
match self.inner.kind {
OptionKind::Spin { min, .. } => min,
_ => None,
}
}
#[getter]
fn max(&self) -> Option<i64> {
match self.inner.kind {
OptionKind::Spin { max, .. } => max,
_ => None,
}
}
#[getter]
fn vars(&self) -> Vec<String> {
match &self.inner.kind {
OptionKind::Combo { vars, .. } => vars.clone(),
_ => Vec::new(),
}
}
#[pyo3(signature = (value = None))]
fn value_text(&self, value: Option<&Bound<'_, PyAny>>) -> PyResult<Option<String>> {
let read = read_value(&self.inner.kind, &self.inner.name, value)?;
self.inner.accepts(&read).map_err(|reason| {
PyValueError::new_err(format!("option {:?}: {reason}", self.inner.name))
})?;
Ok(read.to_text())
}
fn __repr__(&self) -> String {
format!("<Option {} type {}>", self.inner.name, self.r#type())
}
}
#[pyclass(frozen, skip_from_py_object, module = "esca.uci", name = "Info")]
#[derive(Clone)]
pub struct PyInfo {
depth: Option<u32>,
seldepth: Option<u32>,
time: Option<f64>,
nodes: Option<u64>,
nps: Option<u64>,
pv: Vec<PyMove>,
multipv: Option<u32>,
cp: Option<i32>,
mate: Option<i32>,
bound: Option<&'static str>,
wdl: Option<(u32, u32, u32)>,
currmove: Option<PyMove>,
currmovenumber: Option<u32>,
hashfull: Option<u32>,
tbhits: Option<u64>,
sbhits: Option<u64>,
cpuload: Option<u32>,
refutation: Vec<PyMove>,
currline: Vec<PyMove>,
currline_cpu: Option<u32>,
string: Option<String>,
unknown: Vec<String>,
}
impl PyInfo {
fn of(info: &Info, game: &Game) -> PyInfo {
let moves = |line: &[String]| {
uci::moves_of_line(game, line)
.into_iter()
.map(PyMove::new)
.collect()
};
PyInfo {
depth: info.depth,
seldepth: info.seldepth,
time: info.time.map(|time| time.as_secs_f64()),
nodes: info.nodes,
nps: info.nps,
pv: moves(&info.pv),
multipv: info.multipv,
cp: match info.score {
Some(crate::position::Score::Cp(cp)) => Some(cp),
_ => None,
},
mate: match info.score {
Some(crate::position::Score::Mate(mate)) => Some(mate),
_ => None,
},
bound: info.bound.map(|bound| match bound {
uci::Bound::Lower => "lowerbound",
uci::Bound::Upper => "upperbound",
}),
wdl: info.wdl.map(|wdl| (wdl.win, wdl.draw, wdl.loss)),
currmove: info.current_move(game).map(PyMove::new),
currmovenumber: info.currmovenumber,
hashfull: info.hashfull,
tbhits: info.tbhits,
sbhits: info.sbhits,
cpuload: info.cpuload,
refutation: moves(&info.refutation),
currline: info
.currline
.as_ref()
.map(|line| moves(&line.moves))
.unwrap_or_default(),
currline_cpu: info.currline.as_ref().and_then(|line| line.cpu),
string: info.string.clone(),
unknown: info.unknown.clone(),
}
}
}
#[pymethods]
impl PyInfo {
#[getter]
fn depth(&self) -> Option<u32> {
self.depth
}
#[getter]
fn seldepth(&self) -> Option<u32> {
self.seldepth
}
#[getter]
fn time(&self) -> Option<f64> {
self.time
}
#[getter]
fn nodes(&self) -> Option<u64> {
self.nodes
}
#[getter]
fn nps(&self) -> Option<u64> {
self.nps
}
#[getter]
fn pv(&self) -> Vec<PyMove> {
self.pv.clone()
}
#[getter]
fn multipv(&self) -> Option<u32> {
self.multipv
}
#[getter]
fn cp(&self) -> Option<i32> {
self.cp
}
#[getter]
fn mate(&self) -> Option<i32> {
self.mate
}
#[getter]
fn bound(&self) -> Option<&'static str> {
self.bound
}
#[getter]
fn wdl(&self) -> Option<(u32, u32, u32)> {
self.wdl
}
#[getter]
fn currmove(&self) -> Option<PyMove> {
self.currmove
}
#[getter]
fn currmovenumber(&self) -> Option<u32> {
self.currmovenumber
}
#[getter]
fn hashfull(&self) -> Option<u32> {
self.hashfull
}
#[getter]
fn tbhits(&self) -> Option<u64> {
self.tbhits
}
#[getter]
fn sbhits(&self) -> Option<u64> {
self.sbhits
}
#[getter]
fn cpuload(&self) -> Option<u32> {
self.cpuload
}
#[getter]
fn refutation(&self) -> Vec<PyMove> {
self.refutation.clone()
}
#[getter]
fn currline(&self) -> Vec<PyMove> {
self.currline.clone()
}
#[getter]
fn currline_cpu(&self) -> Option<u32> {
self.currline_cpu
}
#[getter]
fn string(&self) -> Option<String> {
self.string.clone()
}
#[getter]
fn unknown(&self) -> Vec<String> {
self.unknown.clone()
}
fn __repr__(&self) -> String {
let score = match (self.cp, self.mate) {
(Some(cp), _) => format!(" cp {cp}"),
(_, Some(mate)) => format!(" mate {mate}"),
_ => String::new(),
};
format!(
"<Info depth {}{score} pv {}>",
self.depth.map_or("-".to_owned(), |depth| depth.to_string()),
self.pv
.iter()
.map(|mv| mv.inner.to_string())
.collect::<Vec<_>>()
.join(" ")
)
}
}
#[pyclass(frozen, skip_from_py_object, module = "esca.uci", name = "Answer")]
#[derive(Clone, Copy)]
pub struct PyAnswer {
best: Option<PyMove>,
ponder: Option<PyMove>,
}
#[pymethods]
impl PyAnswer {
#[getter]
fn best(&self) -> Option<PyMove> {
self.best
}
#[getter]
fn ponder(&self) -> Option<PyMove> {
self.ponder
}
fn __repr__(&self) -> String {
match self.best {
None => "<Answer (none)>".to_owned(),
Some(best) => format!("<Answer {}>", best.inner),
}
}
}
impl PyAnswer {
fn of(answer: uci::Answer) -> PyAnswer {
PyAnswer {
best: answer.best.map(PyMove::new),
ponder: answer.ponder.map(PyMove::new),
}
}
}
#[pyclass(
frozen,
skip_from_py_object,
module = "esca.uci.protocol",
name = "Command"
)]
#[derive(Clone)]
pub struct PyCommand {
inner: uci::Command,
}
#[pymethods]
impl PyCommand {
#[staticmethod]
fn uci() -> PyCommand {
PyCommand {
inner: uci::Command::Uci,
}
}
#[staticmethod]
fn debug(on: bool) -> PyCommand {
PyCommand {
inner: uci::Command::Debug(on),
}
}
#[staticmethod]
fn isready() -> PyCommand {
PyCommand {
inner: uci::Command::IsReady,
}
}
#[staticmethod]
#[pyo3(signature = (name, value = None))]
fn setoption(name: String, value: Option<String>) -> PyCommand {
PyCommand {
inner: uci::Command::SetOption { name, value },
}
}
#[staticmethod]
fn ucinewgame() -> PyCommand {
PyCommand {
inner: uci::Command::NewGame,
}
}
#[staticmethod]
#[pyo3(signature = (game, castling = None))]
fn position(game: &PyGame, castling: Option<&str>) -> PyResult<PyCommand> {
let played = game.played();
let style = match castling {
Some(name) => castling_output_from(name)?,
None => played.castling_output(),
};
Ok(PyCommand {
inner: uci::Command::Position(uci::Setup::of_game(played, style)),
})
}
#[staticmethod]
#[pyo3(signature = (limits = None))]
fn go(limits: Option<PyLimits>) -> PyCommand {
PyCommand {
inner: uci::Command::Go(limits.map(|limits| limits.inner).unwrap_or_default()),
}
}
#[staticmethod]
fn stop() -> PyCommand {
PyCommand {
inner: uci::Command::Stop,
}
}
#[staticmethod]
fn ponderhit() -> PyCommand {
PyCommand {
inner: uci::Command::PonderHit,
}
}
#[staticmethod]
fn quit() -> PyCommand {
PyCommand {
inner: uci::Command::Quit,
}
}
fn to_line(&self) -> String {
self.inner.to_line()
}
#[getter]
fn keyword(&self) -> &'static str {
self.inner.keyword()
}
fn __repr__(&self) -> String {
format!("<Command {}>", self.inner.to_line())
}
}
#[pyclass(
frozen,
skip_from_py_object,
module = "esca.uci.protocol",
name = "Message"
)]
#[derive(Clone)]
pub struct PyMessage {
inner: Message,
line: String,
option: Option<PyOption>,
info: Option<PyInfo>,
answer: Option<PyAnswer>,
}
impl PyMessage {
fn of(message: Message, line: &str, game: &Game) -> PyMessage {
PyMessage {
option: match &message {
Message::Option(spec) => Some(PyOption {
inner: spec.clone(),
}),
_ => None,
},
info: match &message {
Message::Info(info) => Some(PyInfo::of(info, game)),
_ => None,
},
answer: match &message {
Message::BestMove(best) => Some(PyAnswer {
best: best.best_move(game).map(PyMove::new),
ponder: best.ponder_move(game).map(PyMove::new),
}),
_ => None,
},
line: line.to_owned(),
inner: message,
}
}
}
#[pymethods]
impl PyMessage {
#[getter]
fn kind(&self) -> &'static str {
match self.inner {
Message::Id { .. } => "id",
Message::UciOk => "uciok",
Message::ReadyOk => "readyok",
Message::Option(_) => "option",
Message::Info(_) => "info",
Message::BestMove(_) => "bestmove",
Message::Registration(_) => "registration",
Message::CopyProtection(_) => "copyprotection",
Message::Raw(_) => "raw",
}
}
#[getter]
fn line(&self) -> String {
self.line.clone()
}
#[getter]
fn key(&self) -> Option<String> {
match &self.inner {
Message::Id { key, .. } => Some(key.clone()),
_ => None,
}
}
#[getter]
fn value(&self) -> Option<String> {
match &self.inner {
Message::Id { value, .. } => Some(value.clone()),
_ => None,
}
}
#[getter]
fn status(&self) -> Option<&'static str> {
let status = match self.inner {
Message::Registration(status) | Message::CopyProtection(status) => status,
_ => return None,
};
Some(match status {
uci::Status::Checking => "checking",
uci::Status::Ok => "ok",
uci::Status::Error => "error",
})
}
#[getter]
fn option(&self) -> Option<PyOption> {
self.option.clone()
}
#[getter]
fn info(&self) -> Option<PyInfo> {
self.info.clone()
}
#[getter]
fn answer(&self) -> Option<PyAnswer> {
self.answer
}
fn __repr__(&self) -> String {
format!("<Message {} {:?}>", self.kind(), self.line)
}
}
#[pyfunction]
#[pyo3(signature = (line, game = None))]
fn uci_parse(line: &str, game: Option<&PyGame>) -> PyMessage {
let played = game
.map(|game| game.played().clone())
.unwrap_or_else(|| Game::new(crate::variant::classic()));
PyMessage::of(uci::parse(line), line, &played)
}
#[pyclass(module = "esca.uci.protocol", name = "Session")]
pub struct PySession {
inner: uci::Session,
}
#[pymethods]
impl PySession {
#[new]
fn py_new() -> PySession {
PySession {
inner: uci::Session::new(),
}
}
#[getter]
fn state(&self) -> &'static str {
self.inner.state().name()
}
#[getter]
fn pending_ready(&self) -> u32 {
self.inner.pending_ready()
}
fn sent(&mut self, command: &PyCommand) -> PyResult<()> {
self.inner
.sent(&command.inner)
.map_err(|error| ProtocolError::new_err(error.to_string()))
}
fn received(&mut self, message: &PyMessage) -> PyResult<()> {
self.inner
.received(&message.inner)
.map_err(|error| ProtocolError::new_err(error.to_string()))
}
fn __repr__(&self) -> String {
format!("<Session {}>", self.state())
}
}
#[pyclass(frozen, module = "esca.uci", name = "Engine")]
pub struct PyEngine {
shared: Shared,
}
#[pymethods]
impl PyEngine {
#[new]
#[pyo3(signature = (command, args = Vec::new(), *, cwd = None, timeout = 10.0))]
fn py_new(
command: PathBuf,
args: Vec<String>,
cwd: Option<PathBuf>,
timeout: f64,
) -> PyResult<PyEngine> {
let mut launch = uci::Launch::new(command)
.args(args)
.timeout(seconds(timeout, "timeout")?);
if let Some(cwd) = cwd {
launch = launch.current_dir(cwd);
}
let engine = launch.spawn().map_err(to_py_error)?;
Ok(PyEngine {
shared: Arc::new(Mutex::new(Some(engine))),
})
}
#[getter]
fn timeout(&self) -> f64 {
locked(&self.shared)
.as_ref()
.map_or(0.0, |engine| engine.timeout().as_secs_f64())
}
#[setter]
fn set_timeout(&self, timeout: f64) -> PyResult<()> {
let timeout = seconds(timeout, "timeout")?;
if let Some(engine) = locked(&self.shared).as_mut() {
engine.set_timeout(timeout);
}
Ok(())
}
fn handshake(&self, py: Python<'_>) -> PyResult<()> {
on_engine(py, &self.shared, |engine| engine.handshake().map(|_| ()))
}
#[getter]
fn name(&self) -> Option<String> {
locked(&self.shared)
.as_ref()
.and_then(|engine| engine.identity().name.clone())
}
#[getter]
fn author(&self) -> Option<String> {
locked(&self.shared)
.as_ref()
.and_then(|engine| engine.identity().author.clone())
}
#[getter]
fn options<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
let options = PyDict::new(py);
if let Some(engine) = locked(&self.shared).as_ref() {
for spec in engine.options() {
options.set_item(
spec.name.clone(),
PyOption {
inner: spec.clone(),
},
)?;
}
}
Ok(options)
}
fn option(&self, name: &str) -> Option<PyOption> {
locked(&self.shared)
.as_ref()
.and_then(|engine| engine.option(name))
.map(|spec| PyOption {
inner: spec.clone(),
})
}
#[pyo3(signature = (name, value = None))]
fn set_option(
&self,
py: Python<'_>,
name: &str,
value: Option<&Bound<'_, PyAny>>,
) -> PyResult<()> {
let kind = self
.option(name)
.ok_or_else(|| PyValueError::new_err(format!("the engine offers no option {name:?}")))?
.inner
.kind;
let value = read_value(&kind, name, value)?;
on_engine(py, &self.shared, move |engine| {
engine.set_option(name, value)
})
}
fn debug(&self, py: Python<'_>, on: bool) -> PyResult<()> {
on_engine(py, &self.shared, |engine| engine.set_debug(on))
}
fn new_game(&self, py: Python<'_>) -> PyResult<()> {
on_engine(py, &self.shared, |engine| engine.new_game())
}
fn is_ready(&self, py: Python<'_>) -> PyResult<()> {
on_engine(py, &self.shared, |engine| engine.is_ready())
}
fn set_position(&self, py: Python<'_>, game: &PyGame) -> PyResult<()> {
let game = game.played().clone();
on_engine(py, &self.shared, move |engine| engine.set_position(&game))
}
#[pyo3(signature = (limits = None, *, timeout = None))]
fn go(
&self,
py: Python<'_>,
limits: Option<PyLimits>,
timeout: Option<f64>,
) -> PyResult<PySearch> {
let limits = limits.map(|limits| limits.inner).unwrap_or_default();
let budget = self.budget(timeout)?;
let game = on_engine(py, &self.shared, move |engine| {
engine.start_search(&limits)?;
Ok(engine.game().cloned())
})?;
let now = Instant::now();
Ok(PySearch {
shared: Arc::clone(&self.shared),
game: game.unwrap_or_else(|| Game::new(crate::variant::classic())),
until: now
.checked_add(budget)
.unwrap_or_else(|| now + Duration::from_secs(60 * 60 * 24 * 365)),
answer: Mutex::new(None),
})
}
#[pyo3(signature = (game, limits = None, *, timeout = None))]
fn play(
&self,
py: Python<'_>,
game: &PyGame,
limits: Option<PyLimits>,
timeout: Option<f64>,
) -> PyResult<PyAnswer> {
let limits = limits.map(|limits| limits.inner).unwrap_or_default();
let budget = self.budget(timeout)?;
let game = game.played().clone();
let answer = on_engine(py, &self.shared, move |engine| {
engine.play(&game, &limits, budget)
})?;
Ok(PyAnswer::of(answer))
}
#[pyo3(signature = (game, limits = None, *, multipv = None, timeout = None))]
fn analyse(
&self,
py: Python<'_>,
game: &PyGame,
limits: Option<PyLimits>,
multipv: Option<i64>,
timeout: Option<f64>,
) -> PyResult<Vec<PyInfo>> {
if let Some(lines) = multipv {
self.set_option(
py,
"MultiPV",
Some(&pyo3::types::PyInt::new(py, lines).into_any()),
)?;
}
let limits = limits.map(|limits| limits.inner).unwrap_or_default();
let budget = self.budget(timeout)?;
let searched = game.played().clone();
let reports = on_engine(py, &self.shared, move |engine| {
engine.set_position(&searched)?;
let mut search = engine.go(&limits, budget)?;
let mut reports: Vec<Info> = Vec::new();
while let Some(info) = search.next_info()? {
if info.score.is_none() {
continue;
}
let line = info.multipv.unwrap_or(1);
match reports
.iter_mut()
.find(|kept| kept.multipv.unwrap_or(1) == line)
{
Some(kept) => *kept = info,
None => reports.push(info),
}
}
Ok(reports)
})?;
let game = game.played();
let mut reports: Vec<PyInfo> = reports.iter().map(|info| PyInfo::of(info, game)).collect();
reports.sort_by_key(|info| info.multipv.unwrap_or(1));
Ok(reports)
}
fn stop(&self, py: Python<'_>) -> PyResult<()> {
on_engine(py, &self.shared, |engine| engine.stop())
}
fn ponderhit(&self, py: Python<'_>) -> PyResult<()> {
on_engine(py, &self.shared, |engine| engine.ponderhit())
}
fn send_line(&self, py: Python<'_>, text: &str) -> PyResult<()> {
on_engine(py, &self.shared, |engine| engine.send_line(text))
}
#[pyo3(signature = (timeout = None))]
fn next_line(&self, py: Python<'_>, timeout: Option<f64>) -> PyResult<Option<String>> {
let budget = self.budget(timeout)?;
on_engine(py, &self.shared, move |engine| engine.next_line(budget))
}
#[getter]
fn state(&self) -> &'static str {
locked(&self.shared)
.as_ref()
.map_or("quitting", |engine| engine.state().name())
}
#[getter]
fn is_alive(&self) -> bool {
locked(&self.shared)
.as_mut()
.is_some_and(|engine| engine.is_alive())
}
fn quit(&self, py: Python<'_>) -> PyResult<Option<i32>> {
py.detach(|| {
let mut guard = locked(&self.shared);
match guard.take() {
None => Ok(None),
Some(mut engine) => engine.quit().map_err(to_py_error),
}
})
}
fn kill(&self, py: Python<'_>) {
py.detach(|| {
if let Some(mut engine) = locked(&self.shared).take() {
engine.kill();
}
});
}
fn __enter__(slf: Py<Self>) -> Py<Self> {
slf
}
#[pyo3(signature = (*_args))]
fn __exit__(&self, py: Python<'_>, _args: &Bound<'_, PyAny>) -> PyResult<()> {
self.quit(py).map(|_| ())
}
fn __repr__(&self) -> String {
match self.name() {
Some(name) => format!("<Engine {name} {}>", self.state()),
None => format!("<Engine {}>", self.state()),
}
}
}
impl PyEngine {
fn budget(&self, timeout: Option<f64>) -> PyResult<Duration> {
match timeout {
Some(timeout) => seconds(timeout, "timeout"),
None => Ok(locked(&self.shared)
.as_ref()
.map_or(uci::DEFAULT_TIMEOUT, |engine| engine.timeout())),
}
}
}
fn read_value(
kind: &OptionKind,
name: &str,
value: Option<&Bound<'_, PyAny>>,
) -> PyResult<OptionValue> {
let wrong = |wanted: &str| {
PyValueError::new_err(format!(
"option {name:?} is a {} option, which takes {wanted}",
kind.type_name()
))
};
let Some(value) = value.filter(|value| !value.is_none()) else {
return match kind {
OptionKind::Button => Ok(OptionValue::Button),
_ => Err(wrong("a value")),
};
};
match kind {
OptionKind::Check { .. } => value
.extract::<bool>()
.map(OptionValue::Check)
.map_err(|_| wrong("True or False")),
OptionKind::Spin { .. } if value.is_instance_of::<pyo3::types::PyBool>() => {
Err(wrong("an integer"))
}
OptionKind::Spin { .. } => value
.extract::<i64>()
.map(OptionValue::Spin)
.map_err(|_| wrong("an integer")),
OptionKind::Combo { .. } => value
.extract::<String>()
.map(OptionValue::Combo)
.map_err(|_| wrong("one of its values")),
OptionKind::String { .. } => value
.extract::<String>()
.map(OptionValue::String)
.map_err(|_| wrong("text")),
OptionKind::Button => Err(wrong("no value")),
}
}
#[pyclass(frozen, module = "esca.uci", name = "Search")]
pub struct PySearch {
shared: Shared,
game: Game,
until: Instant,
answer: Mutex<Option<PyAnswer>>,
}
#[pymethods]
impl PySearch {
#[getter]
fn done(&self) -> bool {
self.answered().is_some()
}
fn answer(&self, py: Python<'_>) -> PyResult<PyAnswer> {
loop {
if let Some(answer) = self.answered() {
return Ok(answer);
}
self.progress(py)?;
}
}
fn stop(&self, py: Python<'_>) -> PyResult<()> {
on_engine(py, &self.shared, |engine| engine.stop())
}
fn ponderhit(&self, py: Python<'_>) -> PyResult<()> {
on_engine(py, &self.shared, |engine| engine.ponderhit())
}
fn __iter__(slf: Py<Self>) -> Py<Self> {
slf
}
fn __next__(&self, py: Python<'_>) -> PyResult<PyInfo> {
if self.done() {
return Err(PyStopIteration::new_err(()));
}
match self.progress(py)? {
Some(info) => Ok(info),
None => Err(PyStopIteration::new_err(())),
}
}
fn __enter__(slf: Py<Self>) -> Py<Self> {
slf
}
#[pyo3(signature = (*_args))]
fn __exit__(&self, py: Python<'_>, _args: &Bound<'_, PyAny>) -> PyResult<()> {
if self.done() {
return Ok(());
}
self.stop(py)?;
self.answer(py).map(|_| ())
}
fn __repr__(&self) -> String {
format!(
"<Search {}>",
if self.done() { "answered" } else { "running" }
)
}
}
impl PySearch {
fn answered(&self) -> Option<PyAnswer> {
*self.answer.lock().unwrap_or_else(|held| held.into_inner())
}
fn progress(&self, py: Python<'_>) -> PyResult<Option<PyInfo>> {
let left = self.until.saturating_duration_since(Instant::now());
if left.is_zero() {
return Err(EngineTimeout::new_err("no bestmove within the timeout"));
}
let progress = on_engine(py, &self.shared, move |engine| engine.next_progress(left))?;
match progress {
Progress::Info(info) => Ok(Some(PyInfo::of(&info, &self.game))),
Progress::Done(answer) => {
*self.answer.lock().unwrap_or_else(|held| held.into_inner()) =
Some(PyAnswer::of(answer));
Ok(None)
}
}
}
}
pub(super) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> {
let py = module.py();
module.add_class::<PyLimits>()?;
module.add_class::<PyOption>()?;
module.add_class::<PyInfo>()?;
module.add_class::<PyAnswer>()?;
module.add_class::<PyEngine>()?;
module.add_class::<PySearch>()?;
module.add_class::<PyCommand>()?;
module.add_class::<PyMessage>()?;
module.add_class::<PySession>()?;
module.add_function(wrap_pyfunction!(uci_parse, module)?)?;
module.add("UciError", py.get_type::<UciError>())?;
module.add("EngineTimeout", py.get_type::<EngineTimeout>())?;
module.add("EngineDied", py.get_type::<EngineDied>())?;
module.add("ProtocolError", py.get_type::<ProtocolError>())?;
Ok(())
}