myopic_brain/search/
interactive.rs

1use crate::search::{search as blocking_search, SearchContext, SearchParameters, SearchTerminator};
2use crate::{EvalChessBoard, SearchOutcome};
3use anyhow::Result;
4use myopic_board::Side;
5use std::cmp::{max, min};
6use std::rc::Rc;
7use std::sync::mpsc;
8use std::sync::mpsc::{Receiver, Sender};
9use std::time::Duration;
10
11const INFINITE_DURATION: Duration = Duration::from_secs(1_000_000);
12const INFINITE_DEPTH: usize = 1_000;
13const DEFAULT_SEARCH_DURATION: Duration = Duration::from_secs(30);
14const DEFAULT_SEARCH_DEPTH: usize = 10;
15const DEFAULT_TABLE_SIZE: usize = 100_000;
16const MAX_COMPUTED_MOVE_SEARCH_DURATION: Duration = Duration::from_secs(45);
17
18pub type SearchCommandTx<B> = Sender<SearchCommand<B>>;
19pub type SearchResultRx = Receiver<Result<SearchOutcome>>;
20type CmdRx<B> = Receiver<SearchCommand<B>>;
21type ResultTx = Sender<Result<SearchOutcome>>;
22
23#[derive(Debug, Clone, PartialEq)]
24pub enum SearchCommand<B: EvalChessBoard> {
25    Go,
26    GoOnce,
27    Stop,
28    Close,
29    Root(B),
30    Infinite,
31    Depth(usize),
32    Time(usize),
33    GameTime {
34        w_base: usize,
35        w_inc: usize,
36        b_base: usize,
37        b_inc: usize,
38    },
39}
40
41/// Create an interactive search running on a separate thread, communication happens
42/// via an input channel which accepts a variety of commands and an output channel
43/// which transmits the search results.
44pub fn search<B: EvalChessBoard + 'static>() -> (SearchCommandTx<B>, SearchResultRx) {
45    let (input_tx, input_rx) = mpsc::channel::<SearchCommand<B>>();
46    let (output_tx, output_rx) = mpsc::channel::<Result<SearchOutcome>>();
47    std::thread::spawn(move || {
48        let mut search = InteractiveSearch::new(input_rx, output_tx);
49        loop {
50            match &search.input_rx.recv() {
51                Err(_) => continue,
52                Ok(input) => match input.to_owned() {
53                    SearchCommand::Close => break,
54                    SearchCommand::Stop => (),
55                    SearchCommand::Go => search.execute_then_send(),
56                    SearchCommand::Root(root) => search.root = Some(root),
57                    SearchCommand::Depth(max_depth) => search.max_depth = max_depth,
58                    SearchCommand::Time(max_time) => search.set_max_time(max_time),
59                    SearchCommand::GameTime {
60                        w_base,
61                        w_inc,
62                        b_base,
63                        b_inc,
64                    } => search.set_game_time(w_base, w_inc, b_base, b_inc),
65                    SearchCommand::Infinite => {
66                        search.max_time = INFINITE_DURATION;
67                        search.max_depth = INFINITE_DEPTH;
68                    }
69                    SearchCommand::GoOnce => {
70                        search.execute_then_send();
71                        break;
72                    }
73                },
74            }
75        }
76    });
77    (input_tx, output_rx)
78}
79
80struct InteractiveSearch<B: EvalChessBoard> {
81    input_rx: Rc<CmdRx<B>>,
82    output_tx: ResultTx,
83    root: Option<B>,
84    max_depth: usize,
85    max_time: Duration,
86    transposition_table_size: usize,
87}
88
89impl<B: EvalChessBoard + 'static> InteractiveSearch<B> {
90    pub fn new(input_rx: CmdRx<B>, output_tx: ResultTx) -> InteractiveSearch<B> {
91        InteractiveSearch {
92            input_rx: Rc::new(input_rx),
93            root: None,
94            output_tx,
95            max_depth: DEFAULT_SEARCH_DEPTH,
96            max_time: DEFAULT_SEARCH_DURATION,
97            transposition_table_size: DEFAULT_TABLE_SIZE,
98        }
99    }
100
101    pub fn set_max_time(&mut self, time: usize) {
102        self.max_time = Duration::from_millis(time as u64);
103    }
104
105    // TODO This lets time run out with an increment...
106    pub fn set_game_time(&mut self, w_base: usize, w_inc: usize, b_base: usize, b_inc: usize) {
107        if self.root.is_some() {
108            let active = self.root.as_ref().unwrap().active();
109            let mut time = max(
110                500,
111                match active {
112                    Side::White => w_inc,
113                    _ => b_inc,
114                },
115            );
116            time += match active {
117                Side::White => w_base / 10,
118                Side::Black => b_base / 10,
119            };
120            self.set_max_time(min(
121                time,
122                MAX_COMPUTED_MOVE_SEARCH_DURATION.as_millis() as usize,
123            ));
124        }
125    }
126
127    pub fn execute_then_send(&self) -> () {
128        if self.root.is_some() {
129            match self.output_tx.send(self.execute()) {
130                _ => (),
131            }
132        }
133    }
134
135    pub fn execute(&self) -> Result<SearchOutcome> {
136        let tracker = InteractiveSearchTerminator {
137            max_depth: self.max_depth,
138            max_time: self.max_time,
139            stop_signal: self.input_rx.clone(),
140        };
141        blocking_search(
142            self.root.clone().unwrap(),
143            SearchParameters {
144                terminator: tracker,
145                table_size: self.transposition_table_size,
146            },
147        )
148    }
149}
150
151struct InteractiveSearchTerminator<B: EvalChessBoard> {
152    max_time: Duration,
153    max_depth: usize,
154    stop_signal: Rc<CmdRx<B>>,
155}
156
157impl<B: EvalChessBoard> SearchTerminator for InteractiveSearchTerminator<B> {
158    fn should_terminate(&self, ctx: &SearchContext) -> bool {
159        ctx.start_time.elapsed() > self.max_time
160            || ctx.depth_remaining >= self.max_depth
161            || match self.stop_signal.try_recv() {
162                Ok(SearchCommand::Stop) => true,
163                _ => false,
164            }
165    }
166}