uci: threaded setup

Properly (I hope so) thread standard input and engine communications.
This commit is contained in:
dece 2020-06-03 09:55:41 +02:00
parent f6851f6ef7
commit ee06bfef0b
3 changed files with 69 additions and 62 deletions

View file

@ -23,17 +23,18 @@ pub enum Mode {
// No mode, sit here and do nothing.
No,
// UCI mode: listen to Cmds, send Uci::Cmd::Engine commands.
Uci(mpsc::Receiver<Cmd>, mpsc::Sender<uci::Cmd>),
Uci(mpsc::Sender<uci::Cmd>, mpsc::Receiver<Cmd>),
}
#[derive(Debug)]
pub enum Cmd {
// Commands that can be received by the engine.
Ping(String), // Test if the engine is responding.
UciChannel(mpsc::Sender<Cmd>), // Provide a sender to UCI to start receiving commands.
UciPosition(Vec<uci::PositionArgs>), // UCI "position" command.
// Commands that can be sent by the engine.
Pong(String), // Answer a Ping command with the same payload.
}
pub const CASTLING_WH_K: u8 = 0b00000001;
@ -43,7 +44,7 @@ pub const CASTLING_BL_Q: u8 = 0b00001000;
pub const CASTLING_MASK: u8 = 0b00001111;
impl Engine {
pub fn new(mode: Mode) -> Engine {
pub fn new() -> Engine {
Engine {
board: board::new_empty(),
color: board::SQ_WH,
@ -51,20 +52,32 @@ impl Engine {
en_passant: None,
halfmove: 0,
fullmove: 1,
mode,
mode: Mode::No,
}
}
/// Setup engine for UCI communication.
pub fn setup_uci(&mut self, uci_s: mpsc::Sender<uci::Cmd>) {
// Create a channel to receive commands from Uci.
let (engine_s, engine_r) = mpsc::channel();
uci_s.send(uci::Cmd::Engine(Cmd::UciChannel(engine_s))).unwrap();
self.mode = Mode::Uci(uci_s, engine_r);
self.listen();
}
/// Listen for incoming commands.
///
/// In UCI mode, read incoming Cmds over the MPSC channel.
/// In no modes, stop listening immediately.
pub fn listen(&mut self) {
loop {
match self.mode {
match &self.mode {
Mode::No => break,
Mode::Uci(rx, tx) => {
self.recv_uci(rx, tx);
Mode::Uci(tx, rx) => {
match rx.recv() {
Ok(c) => eprintln!("Unhandled command: {:?}", c),
Err(e) => eprintln!("Engine recv failure: {}", e),
}
}
}
}
@ -131,13 +144,4 @@ impl Engine {
let best_move = moves.iter().choose(&mut rng).unwrap();
*best_move
}
/// Receive a command from Uci.
pub fn recv_uci(&mut self, rx: mpsc::Receiver<Cmd>, tx: mpsc::Sender<uci::Cmd>) {
match rx.recv() {
Ok(Cmd::Ping(s)) => tx.send(uci::Cmd::Engine(Cmd::Pong(s))).unwrap(),
Ok(c) => eprintln!("Unhandled command: {:?}", c),
Err(e) => eprintln!("Engine recv failure: {}", e),
}
}
}

View file

@ -20,9 +20,9 @@ fn main() {
.possible_values(&["w", "white", "b", "black"])))
.subcommand(SubCommand::with_name("uci")
.about("Start engine in UCI mode")
.arg(Arg::with_name("output")
.help("Log file path")
.short("o").long("output").takes_value(true).required(false)))
.arg(Arg::with_name("log_file")
.help("Log file path (default is stderr)")
.long("log-file").takes_value(true).required(false)))
.get_matches();
process::exit(match matches.subcommand() {

View file

@ -66,14 +66,15 @@ impl Uci {
/// Start a new UCI listening for standard input.
pub fn start(output: Option<&str>) {
// Create the UCI queue, both for standard IO and for engine communication.
let (uci_tx, uci_rx): (mpsc::Sender<Cmd>, mpsc::Receiver<Cmd>) = mpsc::channel();
let (uci_s, uci_r): (mpsc::Sender<Cmd>, mpsc::Receiver<Cmd>) = mpsc::channel();
let stdin_tx = uci_s.clone();
thread::spawn(move || {
read_stdin(uci_tx);
Uci::read_stdin(stdin_tx);
});
let mut uci = Uci {
state: State::Init,
cmd_channel: (uci_tx, uci_rx),
cmd_channel: (uci_s, uci_r),
engine_in: None,
logfile: None,
};
@ -93,12 +94,13 @@ impl Uci {
loop {
match self.cmd_channel.1.recv() {
Ok(Cmd::Stdin(cmd)) => {
self.log(format!("UCI >>> {}", cmd));
if !self.handle_command(&parse_command(&cmd)) {
break
}
}
Ok(Cmd::Engine(cmd)) => {
// TODO
self.handle_engine_command(&cmd);
}
Err(e) => self.log(format!("Can't read commands: {}", e))
}
@ -118,12 +120,29 @@ impl Uci {
}
}
/// Read a command from the interface.
fn receive(&mut self) -> Option<String> {
/// Read lines over stdin, notifying over an MPSC channel.
///
/// As it is not trivial to add a timeout, or overly complicated
/// to break the loop with a second channel, simply stop listening
/// when the UCI "quit" command is received.
///
/// This is not an Uci method as it does not need to act on the
/// instance itself.
pub fn read_stdin(tx: mpsc::Sender<Cmd>) {
let mut s = String::new();
loop {
match io::stdin().read_line(&mut s) {
Ok(_) => { self.log(format!(">>> {}", s.trim_end())); Some(s.trim().to_string()) }
Err(e) => { self.log(format!("Failed to read input: {:?}", e)); None }
Ok(_) => {
let s = s.trim();
tx.send(Cmd::Stdin(s.to_string())).unwrap();
if s == "quit" {
break;
}
}
Err(e) => {
eprintln!("Failed to read input: {:?}", e);
}
}
}
}
@ -133,7 +152,7 @@ impl Uci {
println!("{}", s);
}
/// Handle an UCI command, return false if engine should stop listening.
/// Handle an UCI command, return false if it should stop listening.
fn handle_command(&mut self, cmd: &UciCmd) -> bool {
match cmd {
UciCmd::Uci => if self.state == State::Init {
@ -150,11 +169,23 @@ impl Uci {
UciCmd::Go(g) => if self.state == State::Ready { self.go(g) }
UciCmd::Quit => return false,
UciCmd::Unknown(c) => { self.log(format!("Unknown command: {}", c)); }
_ => {}
}
true
}
/// Handle an engine command.
fn handle_engine_command(&mut self, cmd: &engine::Cmd) {
match cmd {
engine::Cmd::UciChannel(s) => {
self.engine_in = Some(s.to_owned());
// Send a ping to the engine to ensure communication.
let ping = engine::Cmd::Ping("test".to_string());
self.engine_in.as_ref().unwrap().send(ping).unwrap();
}
_ => {}
}
}
/// Send IDs to interface.
fn send_identities(&mut self) {
self.send(&format!("id name {}", VATU_NAME));
@ -163,17 +194,11 @@ impl Uci {
}
fn setup_engine(&mut self) {
// Create the channel to send commands to the engine.
let (uci_out, engine_in): (mpsc::Sender<engine::Cmd>, mpsc::Receiver<engine::Cmd>) =
mpsc::channel();
self.engine_in = Some(uci_out);
// Pass the general Uci receiver to the engine as well.
let engine_out = self.cmd_channel.0;
let uci_s = self.cmd_channel.0.clone();
thread::spawn(move || {
let mut engine = engine::Engine::new(engine::Mode::Uci(engine_in, engine_out));
engine.listen();
let mut engine = engine::Engine::new();
engine.setup_uci(uci_s);
});
self.engine_in.as_ref().unwrap().send(engine::Cmd::Ping("test".to_string()));
self.state = State::Ready;
}
@ -226,28 +251,6 @@ impl Uci {
}
}
/// Read lines over stdin, notifying over an MPSC channel.
///
/// As it is not trivial to add a timeout, or overly complicated to
/// break the loop with a second channel, simply stop listening when
/// the UCI "quit" command is received.
pub fn read_stdin(tx: mpsc::Sender<Cmd>) {
let mut s = String::new();
loop {
match io::stdin().read_line(&mut s) {
Ok(_) => {
let s = s.trim();
tx.send(Cmd::Stdin(s.to_string()));
if s == "quit" {
break;
}
}
Err(e) => {
eprintln!("Failed to read input: {:?}", e);
}
}
}
}
/// Parse an UCI command.
fn parse_command(s: &str) -> UciCmd {