1use std::cmp::Ordering;
2use std::io::{BufRead, Write};
3use std::str::FromStr;
4
5use itertools::Itertools;
6use nohash_hasher::IntSet;
7
8use crate::board::{Board, BoardDone, PlayError, Player};
9use crate::games::go::{go_player_from_symbol, Chains, GoBoard, Komi, Move, Rules, State, Tile, Zobrist, GO_MAX_SIZE};
10use crate::interface::gtp::command::{Command, CommandKind, FinalStatusKind, Response, ResponseInner};
11
12#[derive(Debug, Copy, Clone, Eq, PartialEq)]
13pub enum Action {
14 Resign,
15 Move(Move),
16}
17
18pub trait GtpBot {
19 fn select_action(&mut self, board: &GoBoard, time: &TimeInfo, log: &mut impl Write) -> Result<Action, BoardDone>;
20}
21
22#[derive(Debug)]
23pub struct GtpEngineState {
24 size: u8,
25 rules: Rules,
26 komi: Komi,
27 time_settings: TimeSettings,
28 time_left_a: TimeLeft,
29 time_left_b: TimeLeft,
30
31 state: BoardState,
32 stack: Vec<BoardState>,
33}
34
35#[derive(Debug, Copy, Clone)]
36pub struct TimeInfo {
37 pub settings: TimeSettings,
38 pub time_left: TimeLeft,
39 pub time_left_opponent: TimeLeft,
40}
41
42#[derive(Debug, Copy, Clone)]
44pub struct TimeSettings {
45 pub main_time: u32,
46 pub byo_yomi_time: u32,
47 pub byo_yomi_stones: u32,
48}
49
50#[derive(Debug, Copy, Clone)]
51pub struct TimeLeft {
52 pub time_left: u32,
53 pub stones_left: u32,
54}
55
56#[derive(Debug)]
57struct BoardState {
58 chains: Chains,
59 state: State,
60
61 #[allow(dead_code)]
62 captured_a: u32,
63 #[allow(dead_code)]
64 captured_b: u32,
65
66 history: IntSet<Zobrist>,
67
68 prev_player: Player,
70}
71
72impl GtpEngineState {
73 pub fn new() -> Self {
74 let size = 19;
75 GtpEngineState {
76 size,
77 komi: Komi::zero(),
78 rules: Rules::cgos(),
79 state: BoardState::new(size),
80 stack: vec![],
81 time_settings: TimeSettings {
82 main_time: 0,
83 byo_yomi_time: 0,
84 byo_yomi_stones: 0,
85 },
86 time_left_a: TimeLeft {
87 time_left: 5,
88 stones_left: 1,
89 },
90 time_left_b: TimeLeft {
91 time_left: 5,
92 stones_left: 1,
93 },
94 }
95 }
96
97 fn clear(&mut self) {
98 self.state = BoardState::new(self.size);
99 self.stack.clear();
100 }
101
102 fn arbitrary(&mut self) {
103 self.clear();
104 }
105
106 fn board(&self, next_player: Player) -> GoBoard {
107 GoBoard::from_parts(
108 self.rules,
109 self.state.chains.clone(),
110 next_player,
111 self.state.state,
112 self.state.history.clone(),
113 self.komi,
114 )
115 }
116
117 fn time_info(&self, player: Player) -> TimeInfo {
118 let (time_left, time_left_opponent) = match player {
119 Player::A => (self.time_left_a, self.time_left_b),
120 Player::B => (self.time_left_b, self.time_left_a),
121 };
122
123 TimeInfo {
124 settings: self.time_settings,
125 time_left,
126 time_left_opponent,
127 }
128 }
129
130 fn play(&mut self, player: Player, mv: Move) -> Result<(), PlayError> {
131 let mut board = self.board(player);
132 board.play(mv)?;
133
134 let new_state = BoardState {
135 chains: board.chains().clone(),
136 state: board.state(),
137 captured_a: captured(Player::A, player, &self.state.chains, board.chains()),
138 captured_b: captured(Player::B, player, &self.state.chains, board.chains()),
139 history: board.history().clone(),
140 prev_player: player,
141 };
142 let old_state = std::mem::replace(&mut self.state, new_state);
143 self.stack.push(old_state);
144
145 Ok(())
146 }
147
148 fn handle_command(&mut self, command: Command, engine: &mut impl GtpBot, log: &mut impl Write) -> ResponseInner {
149 let kind = CommandKind::from_str(&command.name);
150 match kind {
153 Ok(CommandKind::Name) => {
154 check_arg_count(&command, 0)?;
155 Ok(Some("kZero".to_string()))
156 }
157 Ok(CommandKind::ProtocolVersion) => {
158 check_arg_count(&command, 0)?;
159 Ok(Some("2".to_string()))
160 }
161 Ok(CommandKind::Version) => {
162 check_arg_count(&command, 0)?;
163 Ok(Some("0.1.0".to_string()))
164 }
165 Ok(CommandKind::KnownCommand) => {
166 check_arg_count(&command, 1)?;
167 let command_name = &command.args[0];
168 let known = CommandKind::from_str(command_name).is_ok();
169 Ok(Some(known.to_string()))
170 }
171 Ok(CommandKind::ListCommands) => {
172 check_arg_count(&command, 0)?;
173 let list = CommandKind::ALL.iter().map(|c| format!("{}", c)).join("\n");
174 Ok(Some(list))
175 }
176 Ok(CommandKind::Quit) => unreachable!(),
177 Ok(CommandKind::BoardSize) => {
178 check_arg_count(&command, 1)?;
179 let new_size = &command.args[0];
180
181 if let Ok(new_size) = u8::from_str(new_size) {
182 if new_size <= GO_MAX_SIZE {
183 self.size = new_size;
184 self.arbitrary();
185 return Ok(None);
186 }
187 }
188
189 Err("unacceptable size".to_string())
190 }
191 Ok(CommandKind::ClearBoard) => {
192 check_arg_count(&command, 0)?;
193 self.clear();
194 Ok(None)
195 }
196 Ok(CommandKind::Komi) => {
197 check_arg_count(&command, 1)?;
198 let new_komi = &command.args[0];
199 match Komi::from_str(new_komi) {
200 Ok(new_komi) => {
201 self.komi = new_komi;
202 Ok(None)
203 }
204 Err(_) => Err("syntax error".to_string()),
205 }
206 }
207 Ok(CommandKind::Play) => {
208 check_arg_count(&command, 2)?;
209 let color = &command.args[0];
210 let vertex = &command.args[1];
211
212 let player = player_from_color(color)?;
213 let mv = match Move::from_str(vertex) {
214 Err(_) => return Err("invalid move".to_string()),
215 Ok(tile) => tile,
216 };
217
218 match self.play(player, mv) {
219 Ok(()) => Ok(None),
220 Err(_) => Err("illegal move".to_string()),
221 }
222 }
223 Ok(CommandKind::GenMove) => {
224 check_arg_count(&command, 1)?;
225 let color = &command.args[0];
226 let player = player_from_color(color)?;
227
228 let board = self.board(player);
229 let time_info = self.time_info(player);
230
231 let action = engine
232 .select_action(&board, &time_info, log)
233 .map_err(|_| "board done".to_string())?;
234
235 let vertex = match action {
236 Action::Move(mv) => {
237 self.play(player, mv).unwrap();
238 match mv {
239 Move::Pass => "pass".to_string(),
240 Move::Place(tile) => tile.to_string(),
241 }
242 }
243 Action::Resign => "resign".to_string(),
244 };
245 Ok(Some(vertex))
246 }
247 Ok(CommandKind::Undo) => {
248 check_arg_count(&command, 0)?;
249 if let Some(old_state) = self.stack.pop() {
250 self.state = old_state;
251 Ok(None)
252 } else {
253 Err("cannot undo".to_string())
254 }
255 }
256 Ok(CommandKind::TimeSettings) => {
257 check_arg_count(&command, 3)?;
258
259 let main_time = u32::from_str(&command.args[0]).map_err(|_| "syntax error".to_string())?;
260 let byo_yomi_time = u32::from_str(&command.args[1]).map_err(|_| "syntax error".to_string())?;
261 let byo_yomi_stones = u32::from_str(&command.args[2]).map_err(|_| "syntax error".to_string())?;
262
263 self.time_settings.main_time = main_time;
264 self.time_settings.byo_yomi_time = byo_yomi_time;
265 self.time_settings.byo_yomi_stones = byo_yomi_stones;
266
267 Ok(None)
268 }
269 Ok(CommandKind::TimeLeft) => {
270 check_arg_count(&command, 3)?;
271
272 let color = &command.args[0];
273 let player = player_from_color(color)?;
274
275 let time_left = u32::from_str(&command.args[1]).map_err(|_| "syntax error".to_string())?;
276 let stones_left = u32::from_str(&command.args[2]).map_err(|_| "syntax error".to_string())?;
277
278 let time_left = TimeLeft { time_left, stones_left };
279 match player {
280 Player::A => self.time_left_a = time_left,
281 Player::B => self.time_left_b = time_left,
282 }
283
284 Ok(None)
285 }
286 Ok(CommandKind::FinalScore) => {
287 let score = self.state.chains.score();
288 let str = match score.a.cmp(&score.b) {
289 Ordering::Equal => "0".to_string(),
290 Ordering::Greater => format!("B+{}", score.a - score.b),
291 Ordering::Less => format!("W+{}", score.b - score.a),
292 };
293 Ok(Some(str))
294 }
295 Ok(CommandKind::FinalStatusList) => {
296 check_arg_count(&command, 1)?;
297 let kind = &command.args[0];
298 let kind = match FinalStatusKind::from_str(kind) {
299 Ok(kind) => kind,
300 Err(_) => return Err("invalid status".to_string()),
301 };
302
303 let stones = match kind {
304 FinalStatusKind::Alive | FinalStatusKind::Seki => {
306 let chains = &self.state.chains;
307 Tile::all(self.size)
308 .filter(|&tile| chains.stone_at(tile.to_flat(chains.size())).is_some())
309 .collect_vec()
310 }
311 FinalStatusKind::Dead => vec![],
313 };
314
315 let list = stones.iter().map(|&tile| tile.to_string()).join("\n");
316 Ok(Some(list))
317 }
318 Ok(CommandKind::ShowBoard) => {
319 let board = self.board(self.state.prev_player.other());
320 let board_str = board.to_string();
321 Ok(Some(board_str))
322 }
323 Err(_) => Err("unknown command".to_string()),
324 }
325 }
326
327 pub fn run_loop(
328 &mut self,
329 mut engine: impl GtpBot,
330 input: impl BufRead,
331 mut output: impl Write,
332 mut log: impl Write,
333 ) -> std::io::Result<()> {
334 for line in input.lines() {
335 let line = match line {
336 Ok(line) => line,
337 Err(_) => break,
339 };
340
341 let line = preprocess_input(&line);
342 let line = line.trim_end();
343
344 writeln!(&mut log, "> {}", line)?;
345 log.flush()?;
346
347 if let Ok(command) = Command::from_str(line) {
348 if command.name == "quit" {
350 break;
351 }
352
353 let id = command.id;
354 let inner = self.handle_command(command, &mut engine, &mut log);
355 let response = Response::new(id, inner);
356
357 if write!(&mut output, "{}", response).is_err() {
360 break;
361 }
362 if output.flush().is_err() {
363 break;
364 }
365
366 writeln!(&mut log, "< {}", response.to_string().trim_end_matches("\n\n"))?;
367 log.flush()?;
368 }
369 }
370
371 Ok(())
372 }
373}
374
375impl BoardState {
376 pub fn new(size: u8) -> Self {
377 BoardState {
378 chains: Chains::new(size),
379 state: State::Normal,
380 captured_a: 0,
381 captured_b: 0,
382 history: Default::default(),
383 prev_player: Player::B, }
385 }
386}
387
388fn captured(target: Player, prev: Player, before: &Chains, after: &Chains) -> u32 {
389 let expected = before.stone_count_from(target) as u32 + (target == prev) as u32;
390 let actual = after.stone_count_from(target) as u32;
391 assert!(expected >= actual);
392 expected - actual
393}
394
395fn player_from_color(s: &str) -> Result<Player, String> {
396 match s.to_lowercase().as_str() {
397 "black" => return Ok(Player::A),
398 "white" => return Ok(Player::B),
399 s if s.len() == 1 => {
400 if let Some(player) = go_player_from_symbol(s.chars().next().unwrap()) {
401 return Ok(player);
402 }
403 }
404 _ => {}
405 }
406
407 Err("invalid color".to_string())
408}
409
410fn check_arg_count(command: &Command, count: usize) -> Result<(), String> {
411 if command.args.len() == count {
412 Ok(())
413 } else {
414 Err(format!(
415 "wrong arg count, expected {} got {}",
416 count,
417 command.args.len()
418 ))
419 }
420}
421
422fn preprocess_input(before: &str) -> String {
423 assert!(before.is_ascii());
424
425 let cleaned = before.replace(|c: char| c.is_ascii_control() && c != '\n' && c != '\t', "");
426
427 let mut result = String::new();
428
429 for line in cleaned.lines() {
430 if line.starts_with('#') || line.chars().all(|c| c.is_ascii_whitespace()) {
431 continue;
432 };
433 result.push_str(&line.replace('\t', " "));
434 result.push('\n');
435 }
436
437 result
438}
439
440impl TimeInfo {
441 pub fn simple_time_to_use(&self, expected_stones_left: f32) -> f32 {
443 let expected_stones_left = f32::max(2.0, expected_stones_left);
445 self.time_left.time_left as f32 / expected_stones_left
446 }
447}