From 8310e0c1e7eba4e06d7af8652cb489ae88a866df Mon Sep 17 00:00:00 2001 From: dece Date: Thu, 4 Jun 2020 18:58:00 +0200 Subject: [PATCH] engine: correctly manage some go subcommands --- src/board.rs | 6 +- src/cli.rs | 2 +- src/engine.rs | 198 ++++++++++++++++++++++++++++++++++++-------------- src/rules.rs | 11 ++- src/uci.rs | 55 ++++++++++++-- 5 files changed, 205 insertions(+), 67 deletions(-) diff --git a/src/board.rs b/src/board.rs index 29b2a65..8623744 100644 --- a/src/board.rs +++ b/src/board.rs @@ -196,7 +196,7 @@ pub fn apply_into(board: &mut Board, m: &Move) { clear_square(board, &m.0) } -pub fn draw(board: &Board) { +pub fn draw(board: &Board, f: &mut dyn std::io::Write) { for r in (0..8).rev() { let mut rank = String::with_capacity(8); for f in 0..8 { @@ -212,9 +212,9 @@ pub fn draw(board: &Board) { let piece = if is_color(s, SQ_WH) { piece.to_ascii_uppercase() } else { piece }; rank.push(piece); } - println!("{} {}", r + 1, rank); + writeln!(f, "{} {}", r + 1, rank).unwrap(); } - println!(" abcdefgh"); + writeln!(f, " abcdefgh").unwrap(); } #[cfg(test)] diff --git a/src/cli.rs b/src/cli.rs index 667ce17..6dd497f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -16,7 +16,7 @@ pub fn start_game(player_color: u8) { let mut b = board::new(); let mut turn = board::SQ_WH; loop { - board::draw(&b); + board::draw(&b, &mut io::stdout()); println!(""); let m = if turn == player_color { println!("Player turn."); diff --git a/src/engine.rs b/src/engine.rs index 3f04a71..ab1dcf6 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,6 +1,6 @@ //! Vatu engine. -use std::sync::mpsc; +use std::sync::{Arc, atomic, mpsc}; use std::thread; use std::time; @@ -11,35 +11,68 @@ use crate::notation; use crate::rules; use crate::uci; +/// Analysis engine. pub struct Engine { - board: board::Board, // Board to analyse. - color: u8, // Color to analyse. - castling: u8, // Castling state. - en_passant: Option, // En passant state. - halfmove: i32, // Current half moves. - fullmove: i32, // Current full moves. + /// Current game state, starting point of further analysis. + state: GameState, + /// Communication mode. mode: Mode, + /// If true, the engine is currently listening to incoming cmds. listening: bool, + /// Shared flag to notify workers if they should keep working. + working: Arc, } -pub enum Mode { - // No mode, sit here and do nothing. +/// Representation of a game state that can cloned to analysis workers. +/// +/// It does not include various parameters such as clocks so that they +/// can be passed separately using `WorkArgs`. +#[derive(Clone)] +struct GameState { + board: board::Board, + color: u8, + castling: u8, + en_passant: Option, + halfmove: i32, + fullmove: i32, +} + +/// Engine communication mode. +enum Mode { + /// No mode, sit here and do nothing. No, - // UCI mode: listen to Cmds, send Uci::Cmd::Engine commands. - Uci(mpsc::Sender, mpsc::Receiver), + /// UCI mode: listen to Cmds, send Uci::Cmd::Engine commands. + /// + /// First value is the Uci command sender to report results. + /// Second value is the receiver for all engine commands, whether + /// it's from the Uci controller or analysis workers. Third is the + /// sender that is passed to receive outer Uci and workers cmds. + Uci(mpsc::Sender, mpsc::Receiver, mpsc::Sender), } +/// Engine commands. #[derive(Debug)] pub enum Cmd { // Commands that can be received by the engine. UciChannel(mpsc::Sender), // Provide a sender to UCI to start receiving commands. UciPosition(Vec), // UCI "position" command. UciGo(Vec), // UCI "go" command. + Stop, // Stop working ASAP. + TmpBestMove(Option), // Send best move found by analysis worker (TEMPORARY). // Commands that can be sent by the engine. BestMove(Option), } +#[derive(Clone)] +struct WorkArgs { + move_time: i32, + white_time: i32, + black_time: i32, + white_inc: i32, + black_inc: i32, +} + pub const CASTLING_WH_K: u8 = 0b00000001; pub const CASTLING_WH_Q: u8 = 0b00000010; pub const CASTLING_BL_K: u8 = 0b00000100; @@ -50,14 +83,17 @@ pub const CASTLING_MASK: u8 = 0b00001111; impl Engine { pub fn new() -> Engine { Engine { - board: board::new_empty(), - color: board::SQ_WH, - castling: CASTLING_MASK, - en_passant: None, - halfmove: 0, - fullmove: 1, + state: GameState { + board: board::new_empty(), + color: board::SQ_WH, + castling: CASTLING_MASK, + en_passant: None, + halfmove: 0, + fullmove: 1, + }, mode: Mode::No, listening: false, + working: Arc::new(atomic::AtomicBool::new(false)), } } @@ -69,9 +105,9 @@ impl Engine { self.listening = true; while self.listening { match &self.mode { - Mode::Uci(_, rx) => { + Mode::Uci(_, rx, _) => { match rx.recv() { - Ok(c) => self.handle_uci_command(&c), + Ok(c) => self.handle_command(&c), Err(e) => eprintln!("Engine recv failure: {}", e), } } @@ -80,9 +116,22 @@ impl Engine { } } + /// Handle UCI commands passed as engine Cmds. + fn handle_command(&mut self, cmd: &Cmd) { + match cmd { + // UCI commands. + Cmd::UciPosition(args) => self.uci_position(&args), + Cmd::UciGo(args) => self.uci_go(&args), + Cmd::Stop => self.stop(), + // Workers commands. + Cmd::TmpBestMove(m) => self.reply(Cmd::BestMove(*m)), + _ => eprintln!("Not an engine input command: {:?}", cmd), + } + } + fn reply(&mut self, cmd: Cmd) { match &self.mode { - Mode::Uci(tx, _) => { + Mode::Uci(tx, _, _) => { tx.send(uci::Cmd::Engine(cmd)).unwrap(); } _ => {} @@ -93,6 +142,7 @@ impl Engine { /// /// For speed purposes, it assumes values are always valid. fn apply_fen(&mut self, fen: ¬ation::Fen) { + eprintln!("Applying FEN {:?}", fen); self.set_fen_placement(&fen.placement); self.set_fen_color(&fen.color); self.set_fen_castling(&fen.castling); @@ -102,13 +152,13 @@ impl Engine { } fn set_fen_placement(&mut self, placement: &str) { - self.board = board::new_from_fen(placement); + self.state.board = board::new_from_fen(placement); } fn set_fen_color(&mut self, color: &str) { match color.chars().next().unwrap() { - 'w' => self.color = board::SQ_WH, - 'b' => self.color = board::SQ_BL, + 'w' => self.state.color = board::SQ_WH, + 'b' => self.state.color = board::SQ_BL, _ => {} } } @@ -116,28 +166,28 @@ impl Engine { fn set_fen_castling(&mut self, castling: &str) { for c in castling.chars() { match c { - 'K' => self.castling |= CASTLING_WH_K, - 'Q' => self.castling |= CASTLING_WH_Q, - 'k' => self.castling |= CASTLING_BL_K, - 'q' => self.castling |= CASTLING_BL_Q, + 'K' => self.state.castling |= CASTLING_WH_K, + 'Q' => self.state.castling |= CASTLING_WH_Q, + 'k' => self.state.castling |= CASTLING_BL_K, + 'q' => self.state.castling |= CASTLING_BL_Q, _ => {} } } } fn set_fen_en_passant(&mut self, en_passant: &str) { - self.en_passant = match en_passant { + self.state.en_passant = match en_passant { "-" => None, p => Some(board::pos(p)), }; } fn set_fen_halfmove(&mut self, halfmove: &str) { - self.halfmove = halfmove.parse::().ok().unwrap(); + self.state.halfmove = halfmove.parse::().ok().unwrap(); } fn set_fen_fullmove(&mut self, fullmove: &str) { - self.fullmove = fullmove.parse::().ok().unwrap(); + self.state.fullmove = fullmove.parse::().ok().unwrap(); } fn apply_moves(&mut self, moves: &Vec) { @@ -145,20 +195,25 @@ impl Engine { } fn apply_move(&mut self, m: &board::Move) { - board::apply_into(&mut self.board, m); + board::apply_into(&mut self.state.board, m); } /// Start working on board, returning the best move found. /// /// Stop working after `movetime` ms, or go on forever if it's -1. - pub fn work(&mut self, movetime: i32) -> Option { - // Stupid engine! Return a random move. - let moves = rules::get_player_legal_moves(&self.board, self.color); - let mut rng = rand::thread_rng(); - let best_move = moves.iter().choose(&mut rng).and_then(|m| Some(*m)); - // board::draw(&self.board); - thread::sleep(time::Duration::from_millis(movetime as u64)); - best_move + fn work(&mut self, args: &WorkArgs) { + self.working.store(true, atomic::Ordering::Relaxed); + let state = self.state.clone(); + let args = args.clone(); + let working = self.working.clone(); + let tx = match &self.mode { Mode::Uci(_, _, tx) => tx.clone(), _ => return }; + thread::spawn(move || { + analyze(&state, &args, working, tx); + }); + } + + fn stop(&mut self) { + self.working.store(false, atomic::Ordering::SeqCst); } } @@ -168,20 +223,11 @@ impl Engine { pub fn setup_uci(&mut self, uci_s: mpsc::Sender) { // 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); + uci_s.send(uci::Cmd::Engine(Cmd::UciChannel(engine_s.clone()))).unwrap(); + self.mode = Mode::Uci(uci_s, engine_r, engine_s); self.listen(); } - /// Handle UCI commands passed as engine Cmds. - fn handle_uci_command(&mut self, cmd: &Cmd) { - match cmd { - Cmd::UciPosition(args) => self.uci_position(&args), - Cmd::UciGo(args) => self.uci_go(&args), - _ => eprintln!("Not an UCI command: {:?}", cmd), - } - } - /// Update board state from a "position" command's args. fn uci_position(&mut self, p_args: &Vec) { for arg in p_args { @@ -195,6 +241,11 @@ impl Engine { }, uci::PositionArgs::Moves(moves) => { self.apply_moves(&moves); + self.state.color = if moves.len() % 2 == 0 { + board::SQ_WH + } else { + board::SQ_BL + }; } } } @@ -202,15 +253,52 @@ impl Engine { /// Start working using parameters passed with a "go" command. fn uci_go(&mut self, g_args: &Vec) { - let mut movetime = -1; + let mut args = WorkArgs { + move_time: -1, + white_time: -1, + black_time: -1, + white_inc: -1, + black_inc: -1, + }; for arg in g_args { match arg { - uci::GoArgs::MoveTime(ms) => movetime = *ms, - uci::GoArgs::Infinite => movetime = -1, + uci::GoArgs::MoveTime(ms) => args.move_time = *ms, + uci::GoArgs::Infinite => {} + uci::GoArgs::WTime(ms) => args.white_time = *ms, + uci::GoArgs::BTime(ms) => args.black_time = *ms, + uci::GoArgs::WInc(ms) => args.white_inc = *ms, + uci::GoArgs::BInc(ms) => args.black_inc = *ms, + _ => {} } } - let best_move = self.work(movetime); - self.reply(Cmd::BestMove(best_move)); + self.work(&args); + } +} + +fn analyze( + state: &GameState, + _args: &WorkArgs, + wip: Arc, + tx: mpsc::Sender, +) { + if !wip.load(atomic::Ordering::Relaxed) { + return; } + // Stupid engine! Return a random move. + let moves = rules::get_player_legal_moves(&state.board, state.color); + let mut rng = rand::thread_rng(); + let best_move = moves.iter().choose(&mut rng).and_then(|m| Some(*m)); + thread::sleep(time::Duration::from_millis(1000u64)); + tx.send(Cmd::TmpBestMove(best_move)).unwrap(); + + // thread::sleep(time::Duration::from_secs(1)); + // for _ in 0..4 { + // let board = board.clone(); + // let wip = wip.clone(); + // thread::spawn(move || { + // analyze(&board, wip); + // }); + // } + } diff --git a/src/rules.rs b/src/rules.rs index a77fe12..b3da6c1 100644 --- a/src/rules.rs +++ b/src/rules.rs @@ -52,7 +52,8 @@ fn get_pawn_moves(board: &Board, at: &Pos, piece: u8) -> Vec { return moves } let forward: Pos = (f, forward_r); - if is_empty(board, &forward) { + // If forward square is empty (and we are not jumping over an occupied square), add it. + if is_empty(board, &forward) && (i == 1 || is_empty(board, &(f, forward_r - 1))) { moves.push((*at, forward)) } // Check diagonals for pieces to attack. @@ -240,14 +241,18 @@ mod tests { assert!(moves.contains( &(pos("e2"), pos("e4")) )); // Check that a pawn cannot move forward if a piece is blocking its path. - // 1. black pawn 2 square forward: + // 1. black pawn 2 square forward; only 1 square forward available from start pos. set_square(&mut b, &pos("e4"), SQ_BL_P); let moves = get_piece_moves(&b, &pos("e2")); assert!(moves.len() == 1 && moves.contains( &(pos("e2"), pos("e3")) )); - // 2. black pawn 1 square forward: + // 2. black pawn 1 square forward; no square available. set_square(&mut b, &pos("e3"), SQ_BL_P); let moves = get_piece_moves(&b, &pos("e2")); assert_eq!(moves.len(), 0); + // 3. remove the e4 black pawn; the white pawn should not be able to jump above e3 pawn. + clear_square(&mut b, &pos("e4")); + let moves = get_piece_moves(&b, &pos("e2")); + assert_eq!(moves.len(), 0); // Check that a pawn can take a piece diagonally. set_square(&mut b, &pos("f3"), SQ_BL_P); diff --git a/src/uci.rs b/src/uci.rs index b577029..8dddde7 100644 --- a/src/uci.rs +++ b/src/uci.rs @@ -63,6 +63,16 @@ pub enum PositionArgs { /// Arguments for the go remote commands. #[derive(Debug, Clone)] pub enum GoArgs { + SearchMoves(Vec), + Ponder, + WTime(i32), + BTime(i32), + WInc(i32), + BInc(i32), + MovesToGo(i32), + Depth(i32), + Nodes(i32), + Mate(i32), MoveTime(i32), Infinite, } @@ -165,7 +175,6 @@ impl Uci { }, UciCmd::IsReady => if self.state == State::Ready { self.send_ready() }, UciCmd::UciNewGame => if self.state == State::Ready { /* Nothing to do. */ }, - UciCmd::Stop => if self.state == State::Ready { /* Nothing to do. */ }, UciCmd::Position(args) => if self.state == State::Ready { let args = engine::Cmd::UciPosition(args.to_vec()); self.engine_in.as_ref().unwrap().send(args).unwrap(); @@ -173,7 +182,11 @@ impl Uci { UciCmd::Go(args) => if self.state == State::Ready { let args = engine::Cmd::UciGo(args.to_vec()); self.engine_in.as_ref().unwrap().send(args).unwrap(); + self.state = State::Working; } + UciCmd::Stop => if self.state == State::Working { + self.engine_in.as_ref().unwrap().send(engine::Cmd::Stop).unwrap(); + }, UciCmd::Quit => return false, UciCmd::Unknown(c) => { self.log(format!("Unknown command: {}", c)); } } @@ -188,6 +201,7 @@ impl Uci { self.engine_in = Some(s.to_owned()); } engine::Cmd::BestMove(m) => { + self.state = State::Ready; self.send_bestmove(m); } _ => {} @@ -287,13 +301,44 @@ fn parse_go_command(fields: &[&str]) -> UciCmd { let mut subcommands = vec!(); while i < num_fields { match fields[i] { + "infinite" => subcommands.push(GoArgs::Infinite), "movetime" => { i += 1; - let ms = fields[i].parse::().unwrap(); - subcommands.push(GoArgs::MoveTime(ms)); + subcommands.push(GoArgs::MoveTime(fields[i].parse::().unwrap())); } - "infinite" => subcommands.push(GoArgs::Infinite), - f => return UciCmd::Unknown(format!("Unknown go subcommand: {}", f)), + "wtime" => { + i += 1; + subcommands.push(GoArgs::WTime(fields[i].parse::().unwrap())); + }, + "btime" => { + i += 1; + subcommands.push(GoArgs::BTime(fields[i].parse::().unwrap())); + } + "winc" => { + i += 1; + subcommands.push(GoArgs::WInc(fields[i].parse::().unwrap())); + } + "binc" => { + i += 1; + subcommands.push(GoArgs::BInc(fields[i].parse::().unwrap())); + } + "movestogo" => { + i += 1; + subcommands.push(GoArgs::MovesToGo(fields[i].parse::().unwrap())); + } + "depth" => { + i += 1; + subcommands.push(GoArgs::Depth(fields[i].parse::().unwrap())); + } + "nodes" => { + i += 1; + subcommands.push(GoArgs::Nodes(fields[i].parse::().unwrap())); + } + "mate" => { + i += 1; + subcommands.push(GoArgs::Mate(fields[i].parse::().unwrap())); + } + f => eprintln!("Unknown go subcommand: {}", f), } i += 1; }