chess_tui/game_logic/
bot.rs1use crate::constants::{BOT_DIFFICULTY_DEPTH, BOT_DIFFICULTY_ELO, BOT_DIFFICULTY_MOVETIME_MS};
2use ruci::{Engine, Go};
3use shakmaty::fen::Fen;
4use shakmaty::uci::UciMove;
5use std::borrow::Cow;
6use std::process::Command;
7use std::str::FromStr;
8
9#[derive(Clone)]
10pub struct Bot {
11 pub engine_path: String,
12 pub bot_will_move: bool,
14 pub is_bot_starting: bool,
16 pub depth: u8,
18 pub difficulty: Option<u8>,
20}
21
22impl Bot {
23 pub fn new(engine_path: &str, is_bot_starting: bool, depth: u8, difficulty: Option<u8>) -> Bot {
24 Self {
25 engine_path: engine_path.to_string(),
26 bot_will_move: false,
27 is_bot_starting,
28 depth,
29 difficulty,
30 }
31 }
32
33 fn effective_depth(&self) -> u8 {
35 self.difficulty
36 .and_then(|i| {
37 let idx = i as usize;
38 if idx < BOT_DIFFICULTY_DEPTH.len() {
39 Some(BOT_DIFFICULTY_DEPTH[idx])
40 } else {
41 None
42 }
43 })
44 .unwrap_or(self.depth)
45 }
46
47 fn movetime_ms(&self) -> Option<u64> {
49 self.difficulty.and_then(|i| {
50 let idx = i as usize;
51 if idx < BOT_DIFFICULTY_MOVETIME_MS.len() {
52 Some(BOT_DIFFICULTY_MOVETIME_MS[idx])
53 } else {
54 None
55 }
56 })
57 }
58
59 fn elo(&self) -> Option<u16> {
61 self.difficulty.and_then(|i| {
62 let idx = i as usize;
63 if idx < BOT_DIFFICULTY_ELO.len() {
64 Some(BOT_DIFFICULTY_ELO[idx])
65 } else {
66 None
67 }
68 })
69 }
70
71 pub fn get_move(&self, fen: &str) -> UciMove {
81 let parts: Vec<&str> = self.engine_path.split_whitespace().collect();
84 let (command, args) = if parts.is_empty() {
85 (self.engine_path.as_str(), &[] as &[&str])
86 } else {
87 (parts[0], &parts[1..])
88 };
89
90 let mut cmd = Command::new(command);
91 if !args.is_empty() {
92 cmd.args(args);
93 }
94
95 let mut process = cmd
96 .stdin(std::process::Stdio::piped())
97 .stdout(std::process::Stdio::piped())
98 .spawn()
99 .unwrap_or_else(|e| panic!("Failed to spawn engine process: {e}"));
100
101 let mut engine = Engine::from_process(&mut process, false)
102 .unwrap_or_else(|e| panic!("Failed to initialize engine: {e}"));
103
104 engine
106 .use_uci(|_| {})
107 .unwrap_or_else(|e| panic!("Failed UCI handshake: {e}"));
108
109 if let Some(elo) = self.elo() {
111 engine
112 .send(ruci::gui::SetOption {
113 name: Cow::Borrowed("UCI_LimitStrength"),
114 value: Some(Cow::Borrowed("true")),
115 })
116 .unwrap_or_else(|e| panic!("Failed to set UCI_LimitStrength: {e}"));
117 engine
118 .send(ruci::gui::SetOption {
119 name: Cow::Borrowed("UCI_Elo"),
120 value: Some(Cow::Owned(elo.to_string())),
121 })
122 .unwrap_or_else(|e| panic!("Failed to set UCI_Elo: {e}"));
123 }
124
125 let fen_parsed =
126 Fen::from_str(fen).unwrap_or_else(|e| panic!("Failed to parse FEN '{fen}': {e}"));
127
128 engine
129 .send(ruci::Position::Fen {
130 fen: Cow::Owned(fen_parsed),
131 moves: Cow::Borrowed(&[]),
132 })
133 .unwrap_or_else(|e| panic!("Failed to send position to engine: {e}"));
134
135 let depth = self.effective_depth();
136 let move_time_ms = self.movetime_ms();
137
138 engine
139 .go(
140 &Go {
141 depth: Some(depth as usize),
142 move_time: move_time_ms.map(|ms| ms as usize),
143 ..Default::default()
144 },
145 |_| {},
146 )
147 .unwrap_or_else(|e| panic!("Engine failed to compute move: {e}"))
148 .take_normal()
149 .unwrap_or_else(|| panic!("Engine returned non-normal move"))
150 .r#move
151 }
152}