analysis: use negamax with alpha-beta

This commit is contained in:
dece 2020-06-15 01:27:31 +02:00
parent 56e7450c7e
commit 685039d98a
5 changed files with 58 additions and 63 deletions

View file

@ -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)
@ -116,44 +128,15 @@ fn evaluate(stats: &(stats::BoardStats, stats::BoardStats)) -> f32 {
let (player_stats, opponent_stats) = stats;
200.0 * (player_stats.num_kings - opponent_stats.num_kings) 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
+ 3.0 * (player_stats.num_bishops - opponent_stats.num_bishops) as f32
+ 3.0 * (player_stats.num_knights - opponent_stats.num_knights) as f32
+ (player_stats.num_pawns - opponent_stats.num_pawns) as f32
- 0.5 * (
player_stats.num_doubled_pawns - opponent_stats.num_doubled_pawns +
player_stats.num_isolated_pawns - opponent_stats.num_isolated_pawns +
player_stats.num_backward_pawns - opponent_stats.num_backward_pawns
) 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"));
}
+ 9.0 * (player_stats.num_queens - opponent_stats.num_queens) 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_knights - opponent_stats.num_knights) as f32
+ (player_stats.num_pawns - opponent_stats.num_pawns) as f32
- 0.5 * (
player_stats.num_doubled_pawns - opponent_stats.num_doubled_pawns +
player_stats.num_isolated_pawns - opponent_stats.num_isolated_pawns +
player_stats.num_backward_pawns - opponent_stats.num_backward_pawns
) as f32
+ 0.1 * (player_stats.mobility - opponent_stats.mobility) as f32
}

View file

@ -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 {

View file

@ -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
}

View file

@ -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") {

View file

@ -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;