analysis: use negamax with alpha-beta
This commit is contained in:
parent
56e7450c7e
commit
685039d98a
|
@ -60,9 +60,8 @@ impl Analyzer {
|
|||
self.log(format!("Legal moves: {}", notation::move_list_to_string(&moves)));
|
||||
}
|
||||
|
||||
self.max_depth = 2;
|
||||
let color_factor = if board::is_white(self.node.game_state.color) { 1 } else { -1 } as f32;
|
||||
let (max_score, best_move) = self.negamax(&self.node, 0, color_factor);
|
||||
self.max_depth = 3;
|
||||
let (max_score, best_move) = self.negamax(&self.node, MIN_F32, MAX_F32, 0);
|
||||
|
||||
if best_move.is_some() {
|
||||
let log_str = format!(
|
||||
|
@ -83,24 +82,37 @@ impl Analyzer {
|
|||
fn negamax(
|
||||
&self,
|
||||
node: &Node,
|
||||
alpha: f32,
|
||||
beta: f32,
|
||||
depth: u32,
|
||||
color_f: f32,
|
||||
) -> (f32, Option<Move>) {
|
||||
if depth == self.max_depth {
|
||||
let stats = node.compute_stats();
|
||||
return (color_f * evaluate(&stats), None)
|
||||
let ev = evaluate(&stats);
|
||||
return (ev, None)
|
||||
}
|
||||
let moves = node.get_player_moves(true);
|
||||
let mut alpha = alpha;
|
||||
let mut best_score = MIN_F32;
|
||||
let mut best_move = None;
|
||||
for m in moves {
|
||||
self.log(format!("negamax: depth {} move {}...", depth, notation::move_to_string(&m)));
|
||||
let mut sub_node = node.clone();
|
||||
sub_node.apply_move(&m);
|
||||
let (score, _) = self.negamax(&mut sub_node, depth + 1, -color_f);
|
||||
let score = -score;
|
||||
if score >= best_score {
|
||||
let result = self.negamax(&sub_node, -beta, -alpha, depth + 1);
|
||||
let score = -result.0;
|
||||
if score > best_score {
|
||||
best_score = score;
|
||||
best_move = Some(m);
|
||||
self.log(format!("negamax: depth {} new best score {}.", depth, best_score));
|
||||
}
|
||||
if best_score > alpha {
|
||||
alpha = best_score;
|
||||
self.log(format!("negamax: depth {} new alpha {}.", depth, alpha));
|
||||
}
|
||||
if alpha >= beta {
|
||||
self.log(format!("negamax: depth {} alpha above beta {}, cut.", depth, beta));
|
||||
break
|
||||
}
|
||||
}
|
||||
(best_score, best_move)
|
||||
|
@ -128,32 +140,3 @@ fn evaluate(stats: &(stats::BoardStats, stats::BoardStats)) -> f32 {
|
|||
) as f32
|
||||
+ 0.1 * (player_stats.mobility - opponent_stats.mobility) as f32
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_minimax() {
|
||||
// FIXME
|
||||
// let mut node = Node::new();
|
||||
// node.game_state.castling = 0;
|
||||
|
||||
// // White mates in 1 move, queen to d7.
|
||||
// board::set_square(&mut node.board, &pos("a1"), board::SQ_WH_K);
|
||||
// board::set_square(&mut node.board, &pos("c6"), board::SQ_WH_P);
|
||||
// board::set_square(&mut node.board, &pos("h7"), board::SQ_WH_Q);
|
||||
// board::set_square(&mut node.board, &pos("d8"), board::SQ_BL_K);
|
||||
// let (_, m) = minimax(&mut node, 0, 2, true);
|
||||
// assert_eq!(m.unwrap(), notation::parse_move("h7d7"));
|
||||
|
||||
// // Check that it works for black as well.
|
||||
// board::set_square(&mut node.board, &pos("a1"), board::SQ_BL_K);
|
||||
// board::set_square(&mut node.board, &pos("c6"), board::SQ_BL_P);
|
||||
// board::set_square(&mut node.board, &pos("h7"), board::SQ_BL_Q);
|
||||
// board::set_square(&mut node.board, &pos("d8"), board::SQ_WH_K);
|
||||
// node.game_state.color = board::SQ_BL;
|
||||
// let (_, m) = minimax(&mut node, 0, 2, true);
|
||||
// assert_eq!(m.unwrap(), notation::parse_move("h7d7"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -104,6 +104,11 @@ impl Engine {
|
|||
}
|
||||
}
|
||||
|
||||
/// Enable debug output.
|
||||
pub fn enable_debug(&mut self) {
|
||||
self.debug = true;
|
||||
}
|
||||
|
||||
/// Handle UCI commands passed as engine Cmds.
|
||||
fn handle_command(&mut self, cmd: &Cmd) {
|
||||
match cmd {
|
||||
|
|
|
@ -18,6 +18,9 @@ fn main() {
|
|||
.setting(AppSettings::ArgRequiredElseHelp)
|
||||
.subcommand(SubCommand::with_name("uci")
|
||||
.about("Start engine in UCI mode")
|
||||
.arg(Arg::with_name("debug")
|
||||
.help("Enable debug mode")
|
||||
.short("d").long("debug").takes_value(false).required(false))
|
||||
.arg(Arg::with_name("log_file")
|
||||
.help("Log file path (default is stderr)")
|
||||
.long("log-file").takes_value(true).required(false)))
|
||||
|
@ -30,7 +33,8 @@ fn main() {
|
|||
}
|
||||
|
||||
fn cmd_uci(args: &ArgMatches) -> i32 {
|
||||
let debug = args.is_present("debug");
|
||||
let output = args.value_of("log_file");
|
||||
uci::Uci::start(output);
|
||||
uci::Uci::start(debug, output);
|
||||
0
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ pub fn apply_move_to(
|
|||
|
||||
// Update board and game state.
|
||||
apply_move_to_board(board, m);
|
||||
apply_move_to_state(game_state, m);
|
||||
game_state.color = opposite(game_state.color);
|
||||
|
||||
// If the move is a castle, remove it from castling options.
|
||||
if let Some(castle) = get_castle(m) {
|
||||
|
@ -129,14 +129,6 @@ pub fn apply_move_to_board(board: &mut Board, m: &Move) {
|
|||
}
|
||||
}
|
||||
|
||||
/// Update `game_state` with the move `m`.
|
||||
///
|
||||
/// This only updates the player turn. Castling should be updated in a
|
||||
/// context where the corresponding board is available.
|
||||
pub fn apply_move_to_state(game_state: &mut rules::GameState, _m: &Move) {
|
||||
game_state.color = opposite(game_state.color);
|
||||
}
|
||||
|
||||
/// Get the corresponding castling flag for this move.
|
||||
pub fn get_castle(m: &Move) -> Option<u8> {
|
||||
if m.0 == pos("e1") {
|
||||
|
|
21
src/uci.rs
21
src/uci.rs
|
@ -18,10 +18,16 @@ const VATU_AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
|
|||
/// UCI manager with means to send/receive commands and communicate
|
||||
/// with the engine.
|
||||
pub struct Uci {
|
||||
state: State, // Local UCI state for consistency.
|
||||
cmd_channel: (mpsc::Sender<Cmd>, mpsc::Receiver<Cmd>), // Channel of Cmd, handled by Uci.
|
||||
engine_in: Option<mpsc::Sender<engine::Cmd>>, // Sender for engine comms.
|
||||
logfile: Option<fs::File>, // If some, write logs to it.
|
||||
/// Local UCI state for consistency.
|
||||
state: State,
|
||||
/// Channel of Cmd, handled by Uci.
|
||||
cmd_channel: (mpsc::Sender<Cmd>, mpsc::Receiver<Cmd>),
|
||||
/// Sender for engine comms.
|
||||
engine_in: Option<mpsc::Sender<engine::Cmd>>,
|
||||
/// Debug mode, if true it will override debug mode settings for the engine.
|
||||
debug: bool,
|
||||
/// If some, write logs to it.
|
||||
logfile: Option<fs::File>,
|
||||
}
|
||||
|
||||
/// Internal UCI state.
|
||||
|
@ -80,7 +86,7 @@ pub enum GoArgs {
|
|||
|
||||
impl Uci {
|
||||
/// Start a new UCI listening for standard input.
|
||||
pub fn start(output: Option<&str>) {
|
||||
pub fn start(debug: bool, output: Option<&str>) {
|
||||
// Create the UCI queue, both for standard IO and for engine communication.
|
||||
let (uci_s, uci_r): (mpsc::Sender<Cmd>, mpsc::Receiver<Cmd>) = mpsc::channel();
|
||||
let stdin_tx = uci_s.clone();
|
||||
|
@ -92,6 +98,7 @@ impl Uci {
|
|||
state: State::Init,
|
||||
cmd_channel: (uci_s, uci_r),
|
||||
engine_in: None,
|
||||
debug,
|
||||
logfile: None,
|
||||
};
|
||||
// Configure log output, either a file or stderr.
|
||||
|
@ -225,9 +232,13 @@ impl Uci {
|
|||
|
||||
/// Setup engine for UCI.
|
||||
fn setup_engine(&mut self) {
|
||||
let debug = self.debug;
|
||||
let uci_s = self.cmd_channel.0.clone();
|
||||
thread::spawn(move || {
|
||||
let mut engine = engine::Engine::new();
|
||||
if debug {
|
||||
engine.enable_debug();
|
||||
}
|
||||
engine.setup_uci(uci_s);
|
||||
});
|
||||
self.state = State::Ready;
|
||||
|
|
Reference in a new issue