mod random;
mod two_ply;
use std::time::Duration;
use cozy_chess::util::display_uci_move;
use cozy_chess::{Board, Move};
use crate::search::Limits;
use crate::uci;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum Strategy {
#[default]
Random,
TwoPly,
}
impl Strategy {
pub const OPTION: &'static str = "Strategy";
const ALL: [Strategy; 2] = [Strategy::Random, Strategy::TwoPly];
pub fn name(self) -> &'static str {
match self {
Strategy::Random => "random",
Strategy::TwoPly => "two-ply",
}
}
pub fn from_name(name: &str) -> Option<Strategy> {
Strategy::ALL
.into_iter()
.find(|strategy| strategy.name().eq_ignore_ascii_case(name))
}
pub fn option() -> String {
let mut line = format!(
"option name {} type combo default {}",
Strategy::OPTION,
Strategy::default().name()
);
for strategy in Strategy::ALL {
line.push_str(" var ");
line.push_str(strategy.name());
}
line
}
pub fn pick(self, board: &Board, limits: &Limits) -> Option<Move> {
match self {
Strategy::Random => random::pick(board, limits),
Strategy::TwoPly => two_ply::pick(board, limits),
}
}
}
fn root_moves(board: &Board, limits: &Limits) -> Vec<Move> {
if !limits.search_moves.is_empty() {
return limits.search_moves.clone();
}
let mut moves = Vec::new();
board.generate_moves(|piece_moves| {
moves.extend(piece_moves);
false
});
moves
}
fn report(board: &Board, depth: u8, score: i32, nodes: u64, elapsed: Duration, best: Move) {
uci::send(&format!(
"info depth {depth} time {} nodes {nodes} score cp {score} pv {}",
elapsed.as_millis(),
display_uci_move(board, best)
));
}
#[cfg(test)]
mod tests {
use cozy_chess::GameStatus;
use super::*;
use crate::uci::Go;
#[test]
fn names_round_trip() {
for strategy in Strategy::ALL {
assert_eq!(Strategy::from_name(strategy.name()), Some(strategy));
}
assert_eq!(Strategy::from_name("nonsense"), None);
}
#[test]
fn offers_every_strategy() {
assert_eq!(
Strategy::option(),
"option name Strategy type combo default random var random var two-ply"
);
}
#[test]
fn every_strategy_answers_within_searchmoves() {
let board = Board::startpos();
let limits = Limits::new(
&Go {
depth: Some(2),
search_moves: ["a2a3", "h2h3"].map(str::to_owned).into(),
..Go::default()
},
&board,
);
for strategy in Strategy::ALL {
let played = strategy.pick(&board, &limits).expect("a legal move");
let played = display_uci_move(&board, played).to_string();
assert!(["a2a3", "h2h3"].contains(&played.as_str()), "{played}");
}
}
#[test]
fn self_play_stays_legal() {
let mut board = Board::startpos();
let limits = Limits::new(
&Go {
depth: Some(2),
..Go::default()
},
&board,
);
for ply in 0..40 {
if board.status() != GameStatus::Ongoing {
break;
}
let strategy = if ply % 2 == 0 {
Strategy::Random
} else {
Strategy::TwoPly
};
let played = strategy.pick(&board, &limits).expect("a legal move");
assert!(board.is_legal(played));
board.play_unchecked(played);
}
}
}