myopic_brain/search/
interactive.rs1use 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
41pub 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 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}