board_game/interface/gtp/
engine.rs

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/// See http://www.lysator.liu.se/~gunnar/gtp/gtp2-spec-draft2/gtp2-spec.html#sec:time-handling
43#[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    // only used for board printing
69    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        // TODO find nice way to handle command arg length checking
151
152        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                    // we consider all existing stones alive
305                    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                    // we don't consider any stones dead
312                    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                // the input stream disconnecting is fine
338                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                // handle quit command here
349                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                // the output stream disconnecting is not really an error
358                // no newlines, already included in response
359                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, // assume A plays first
384        }
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    /// *Warning*: returns inf if there are no time limits
442    pub fn simple_time_to_use(&self, expected_stones_left: f32) -> f32 {
443        // TODO consider byo_yomi
444        let expected_stones_left = f32::max(2.0, expected_stones_left);
445        self.time_left.time_left as f32 / expected_stones_left
446    }
447}