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.log(format!("Legal moves: {}", notation::move_list_to_string(&moves)));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.max_depth = 2;
|
self.max_depth = 3;
|
||||||
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, MIN_F32, MAX_F32, 0);
|
||||||
let (max_score, best_move) = self.negamax(&self.node, 0, color_factor);
|
|
||||||
|
|
||||||
if best_move.is_some() {
|
if best_move.is_some() {
|
||||||
let log_str = format!(
|
let log_str = format!(
|
||||||
|
@ -83,24 +82,37 @@ impl Analyzer {
|
||||||
fn negamax(
|
fn negamax(
|
||||||
&self,
|
&self,
|
||||||
node: &Node,
|
node: &Node,
|
||||||
|
alpha: f32,
|
||||||
|
beta: f32,
|
||||||
depth: u32,
|
depth: u32,
|
||||||
color_f: f32,
|
|
||||||
) -> (f32, Option<Move>) {
|
) -> (f32, Option<Move>) {
|
||||||
if depth == self.max_depth {
|
if depth == self.max_depth {
|
||||||
let stats = node.compute_stats();
|
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 moves = node.get_player_moves(true);
|
||||||
|
let mut alpha = alpha;
|
||||||
let mut best_score = MIN_F32;
|
let mut best_score = MIN_F32;
|
||||||
let mut best_move = None;
|
let mut best_move = None;
|
||||||
for m in moves {
|
for m in moves {
|
||||||
|
self.log(format!("negamax: depth {} move {}...", depth, notation::move_to_string(&m)));
|
||||||
let mut sub_node = node.clone();
|
let mut sub_node = node.clone();
|
||||||
sub_node.apply_move(&m);
|
sub_node.apply_move(&m);
|
||||||
let (score, _) = self.negamax(&mut sub_node, depth + 1, -color_f);
|
let result = self.negamax(&sub_node, -beta, -alpha, depth + 1);
|
||||||
let score = -score;
|
let score = -result.0;
|
||||||
if score >= best_score {
|
if score > best_score {
|
||||||
best_score = score;
|
best_score = score;
|
||||||
best_move = Some(m);
|
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)
|
(best_score, best_move)
|
||||||
|
@ -116,44 +128,15 @@ fn evaluate(stats: &(stats::BoardStats, stats::BoardStats)) -> f32 {
|
||||||
let (player_stats, opponent_stats) = stats;
|
let (player_stats, opponent_stats) = stats;
|
||||||
|
|
||||||
200.0 * (player_stats.num_kings - opponent_stats.num_kings) as f32
|
200.0 * (player_stats.num_kings - opponent_stats.num_kings) as f32
|
||||||
+ 9.0 * (player_stats.num_queens - opponent_stats.num_queens) as f32
|
+ 9.0 * (player_stats.num_queens - opponent_stats.num_queens) as f32
|
||||||
+ 5.0 * (player_stats.num_rooks - opponent_stats.num_rooks) as f32
|
+ 5.0 * (player_stats.num_rooks - opponent_stats.num_rooks) as f32
|
||||||
+ 3.0 * (player_stats.num_bishops - opponent_stats.num_bishops) as f32
|
+ 3.0 * (player_stats.num_bishops - opponent_stats.num_bishops) as f32
|
||||||
+ 3.0 * (player_stats.num_knights - opponent_stats.num_knights) as f32
|
+ 3.0 * (player_stats.num_knights - opponent_stats.num_knights) as f32
|
||||||
+ (player_stats.num_pawns - opponent_stats.num_pawns) as f32
|
+ (player_stats.num_pawns - opponent_stats.num_pawns) as f32
|
||||||
- 0.5 * (
|
- 0.5 * (
|
||||||
player_stats.num_doubled_pawns - opponent_stats.num_doubled_pawns +
|
player_stats.num_doubled_pawns - opponent_stats.num_doubled_pawns +
|
||||||
player_stats.num_isolated_pawns - opponent_stats.num_isolated_pawns +
|
player_stats.num_isolated_pawns - opponent_stats.num_isolated_pawns +
|
||||||
player_stats.num_backward_pawns - opponent_stats.num_backward_pawns
|
player_stats.num_backward_pawns - opponent_stats.num_backward_pawns
|
||||||
) as f32
|
) as f32
|
||||||
+ 0.1 * (player_stats.mobility - opponent_stats.mobility) 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.
|
/// Handle UCI commands passed as engine Cmds.
|
||||||
fn handle_command(&mut self, cmd: &Cmd) {
|
fn handle_command(&mut self, cmd: &Cmd) {
|
||||||
match cmd {
|
match cmd {
|
||||||
|
|
|
@ -18,6 +18,9 @@ fn main() {
|
||||||
.setting(AppSettings::ArgRequiredElseHelp)
|
.setting(AppSettings::ArgRequiredElseHelp)
|
||||||
.subcommand(SubCommand::with_name("uci")
|
.subcommand(SubCommand::with_name("uci")
|
||||||
.about("Start engine in UCI mode")
|
.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")
|
.arg(Arg::with_name("log_file")
|
||||||
.help("Log file path (default is stderr)")
|
.help("Log file path (default is stderr)")
|
||||||
.long("log-file").takes_value(true).required(false)))
|
.long("log-file").takes_value(true).required(false)))
|
||||||
|
@ -30,7 +33,8 @@ fn main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cmd_uci(args: &ArgMatches) -> i32 {
|
fn cmd_uci(args: &ArgMatches) -> i32 {
|
||||||
|
let debug = args.is_present("debug");
|
||||||
let output = args.value_of("log_file");
|
let output = args.value_of("log_file");
|
||||||
uci::Uci::start(output);
|
uci::Uci::start(debug, output);
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,7 +49,7 @@ pub fn apply_move_to(
|
||||||
|
|
||||||
// Update board and game state.
|
// Update board and game state.
|
||||||
apply_move_to_board(board, m);
|
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 the move is a castle, remove it from castling options.
|
||||||
if let Some(castle) = get_castle(m) {
|
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.
|
/// Get the corresponding castling flag for this move.
|
||||||
pub fn get_castle(m: &Move) -> Option<u8> {
|
pub fn get_castle(m: &Move) -> Option<u8> {
|
||||||
if m.0 == pos("e1") {
|
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
|
/// UCI manager with means to send/receive commands and communicate
|
||||||
/// with the engine.
|
/// with the engine.
|
||||||
pub struct Uci {
|
pub struct Uci {
|
||||||
state: State, // Local UCI state for consistency.
|
/// Local UCI state for consistency.
|
||||||
cmd_channel: (mpsc::Sender<Cmd>, mpsc::Receiver<Cmd>), // Channel of Cmd, handled by Uci.
|
state: State,
|
||||||
engine_in: Option<mpsc::Sender<engine::Cmd>>, // Sender for engine comms.
|
/// Channel of Cmd, handled by Uci.
|
||||||
logfile: Option<fs::File>, // If some, write logs to it.
|
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.
|
/// Internal UCI state.
|
||||||
|
@ -80,7 +86,7 @@ pub enum GoArgs {
|
||||||
|
|
||||||
impl Uci {
|
impl Uci {
|
||||||
/// Start a new UCI listening for standard input.
|
/// 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.
|
// 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 (uci_s, uci_r): (mpsc::Sender<Cmd>, mpsc::Receiver<Cmd>) = mpsc::channel();
|
||||||
let stdin_tx = uci_s.clone();
|
let stdin_tx = uci_s.clone();
|
||||||
|
@ -92,6 +98,7 @@ impl Uci {
|
||||||
state: State::Init,
|
state: State::Init,
|
||||||
cmd_channel: (uci_s, uci_r),
|
cmd_channel: (uci_s, uci_r),
|
||||||
engine_in: None,
|
engine_in: None,
|
||||||
|
debug,
|
||||||
logfile: None,
|
logfile: None,
|
||||||
};
|
};
|
||||||
// Configure log output, either a file or stderr.
|
// Configure log output, either a file or stderr.
|
||||||
|
@ -225,9 +232,13 @@ impl Uci {
|
||||||
|
|
||||||
/// Setup engine for UCI.
|
/// Setup engine for UCI.
|
||||||
fn setup_engine(&mut self) {
|
fn setup_engine(&mut self) {
|
||||||
|
let debug = self.debug;
|
||||||
let uci_s = self.cmd_channel.0.clone();
|
let uci_s = self.cmd_channel.0.clone();
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let mut engine = engine::Engine::new();
|
let mut engine = engine::Engine::new();
|
||||||
|
if debug {
|
||||||
|
engine.enable_debug();
|
||||||
|
}
|
||||||
engine.setup_uci(uci_s);
|
engine.setup_uci(uci_s);
|
||||||
});
|
});
|
||||||
self.state = State::Ready;
|
self.state = State::Ready;
|
||||||
|
|
Reference in a new issue