sgf_render/
goban.rs

1use std::collections::{HashMap, HashSet, VecDeque};
2
3use sgf_parse::{go, ParseOptions, SgfNode};
4
5use crate::errors::GobanError;
6use crate::render::{NodeDescription, NodeNumber};
7use crate::sgf_traversal::variation_nodes;
8
9pub struct Goban {
10    size: (u8, u8),
11    stones: HashMap<(u8, u8), StoneColor>,
12    stones_before_move: HashMap<u64, HashSet<Stone>>,
13    moves: Vec<(u64, Stone)>,
14    move_number: u64,
15    marks: HashSet<(u8, u8)>,
16    triangles: HashSet<(u8, u8)>,
17    circles: HashSet<(u8, u8)>,
18    squares: HashSet<(u8, u8)>,
19    selected: HashSet<(u8, u8)>,
20    lines: HashSet<((u8, u8), (u8, u8))>,
21    arrows: HashSet<((u8, u8), (u8, u8))>,
22    dimmed: HashSet<(u8, u8)>,
23    labels: HashMap<(u8, u8), String>,
24}
25
26impl Goban {
27    const DEFAULT_HOSHIS: [(u8, u8); 0] = [];
28    const NINE_HOSHIS: [(u8, u8); 4] = [(2, 2), (2, 6), (6, 2), (6, 6)];
29    const THIRTEEN_HOSHIS: [(u8, u8); 5] = [(3, 3), (3, 9), (6, 6), (9, 3), (9, 9)];
30    const NINETEEN_HOSHIS: [(u8, u8); 9] = [
31        (3, 3),
32        (3, 9),
33        (3, 15),
34        (9, 3),
35        (9, 9),
36        (9, 15),
37        (15, 3),
38        (15, 9),
39        (15, 15),
40    ];
41
42    pub fn from_sgf(
43        sgf: &str,
44        node_description: &NodeDescription,
45        strict: bool,
46    ) -> Result<Self, GobanError> {
47        let parse_options = ParseOptions {
48            lenient: !strict,
49            ..Default::default()
50        };
51        let collection = sgf_parse::parse_with_options(sgf, &parse_options)?;
52        let root_node = collection
53            .get(node_description.game_number as usize)
54            .ok_or(GobanError::MissingGame)?
55            .as_go_node()?;
56        let board_size = get_board_size(root_node)?;
57        let mut goban = Goban::new(board_size);
58        let nodes = variation_nodes(root_node, node_description.variation)?;
59        let mut node_count = 0;
60        for node in nodes {
61            goban.process_node(node.sgf_node)?;
62            node_count += 1;
63            if let NodeNumber::Number(n) = node_description.node_number {
64                if node_count > n {
65                    break;
66                }
67            }
68        }
69        if let NodeNumber::Number(n) = node_description.node_number {
70            if n >= node_count {
71                return Err(GobanError::InsufficientSgfNodes);
72            }
73        }
74        Ok(goban)
75    }
76
77    pub fn stones(&self) -> impl Iterator<Item = Stone> + '_ {
78        self.stones.iter().map(|(point, color)| Stone {
79            x: point.0,
80            y: point.1,
81            color: *color,
82        })
83    }
84
85    pub fn stones_before_move(&self, move_number: u64) -> Box<dyn Iterator<Item = Stone> + '_> {
86        let stones = self.stones_before_move.get(&move_number);
87        match stones {
88            Some(stones) => Box::new(stones.iter().copied()),
89            None => Box::new(self.stones.iter().map(|(point, color)| Stone {
90                x: point.0,
91                y: point.1,
92                color: *color,
93            })),
94        }
95    }
96
97    pub fn stone_color(&self, x: u8, y: u8) -> Option<StoneColor> {
98        self.stones.get(&(x, y)).copied()
99    }
100
101    pub fn moves(&self) -> impl Iterator<Item = (u64, Stone)> + '_ {
102        self.moves.iter().copied()
103    }
104
105    pub fn hoshi_points(&self) -> impl Iterator<Item = (u8, u8)> {
106        match self.size {
107            (9, 9) => Self::NINE_HOSHIS.iter().copied(),
108            (13, 13) => Self::THIRTEEN_HOSHIS.iter().copied(),
109            (19, 19) => Self::NINETEEN_HOSHIS.iter().copied(),
110            _ => Self::DEFAULT_HOSHIS.iter().copied(),
111        }
112    }
113
114    pub fn size(&self) -> (u8, u8) {
115        self.size
116    }
117
118    pub fn marks(&self) -> impl Iterator<Item = (u8, u8)> + '_ {
119        self.marks.iter().copied()
120    }
121
122    pub fn triangles(&self) -> impl Iterator<Item = (u8, u8)> + '_ {
123        self.triangles.iter().copied()
124    }
125
126    pub fn circles(&self) -> impl Iterator<Item = (u8, u8)> + '_ {
127        self.circles.iter().copied()
128    }
129
130    pub fn squares(&self) -> impl Iterator<Item = (u8, u8)> + '_ {
131        self.squares.iter().copied()
132    }
133
134    pub fn selected(&self) -> impl Iterator<Item = (u8, u8)> + '_ {
135        self.selected.iter().copied()
136    }
137
138    pub fn dimmed(&self) -> impl Iterator<Item = (u8, u8)> + '_ {
139        self.dimmed.iter().copied()
140    }
141
142    pub fn lines(&self) -> impl Iterator<Item = ((u8, u8), (u8, u8))> + '_ {
143        self.lines.iter().copied()
144    }
145
146    pub fn arrows(&self) -> impl Iterator<Item = ((u8, u8), (u8, u8))> + '_ {
147        self.arrows.iter().copied()
148    }
149
150    pub fn labels(&self) -> impl Iterator<Item = (&(u8, u8), &String)> {
151        self.labels.iter()
152    }
153
154    fn new(board_size: (u8, u8)) -> Self {
155        Self {
156            size: board_size,
157            stones: HashMap::new(),
158            stones_before_move: HashMap::new(),
159            moves: Vec::new(),
160            move_number: 0,
161            marks: HashSet::new(),
162            triangles: HashSet::new(),
163            circles: HashSet::new(),
164            squares: HashSet::new(),
165            selected: HashSet::new(),
166            lines: HashSet::new(),
167            arrows: HashSet::new(),
168            dimmed: HashSet::new(),
169            labels: HashMap::new(),
170        }
171    }
172
173    fn process_node(&mut self, sgf_node: &SgfNode<go::Prop>) -> Result<(), GobanError> {
174        self.marks.clear();
175        self.triangles.clear();
176        self.circles.clear();
177        self.squares.clear();
178        self.selected.clear();
179        self.dimmed.clear();
180        self.labels.clear();
181        self.lines.clear();
182        self.arrows.clear();
183        for prop in sgf_node.properties() {
184            match prop {
185                go::Prop::B(go::Move::Move(point)) => {
186                    if !self.is_tt_pass(*point) {
187                        self.play_stone(Stone::new(point.x, point.y, StoneColor::Black))?;
188                    }
189                }
190                go::Prop::W(go::Move::Move(point)) => {
191                    if !self.is_tt_pass(*point) {
192                        self.play_stone(Stone::new(point.x, point.y, StoneColor::White))?;
193                    }
194                }
195                go::Prop::AB(points) => {
196                    for point in points.iter() {
197                        self.add_stone(Stone::new(point.x, point.y, StoneColor::Black))?;
198                    }
199                }
200                go::Prop::AW(points) => {
201                    for point in points.iter() {
202                        self.add_stone(Stone::new(point.x, point.y, StoneColor::White))?;
203                    }
204                }
205                go::Prop::AE(points) => {
206                    for point in points.iter() {
207                        self.clear_point((point.x, point.y));
208                    }
209                }
210                go::Prop::MN(num) => self.set_move_number(*num as u64),
211                go::Prop::MA(points) => self.marks = points.iter().map(|p| (p.x, p.y)).collect(),
212                go::Prop::TR(points) => {
213                    self.triangles = points.iter().map(|p| (p.x, p.y)).collect()
214                }
215                go::Prop::CR(points) => self.circles = points.iter().map(|p| (p.x, p.y)).collect(),
216                go::Prop::SQ(points) => self.squares = points.iter().map(|p| (p.x, p.y)).collect(),
217                go::Prop::SL(points) => self.selected = points.iter().map(|p| (p.x, p.y)).collect(),
218                go::Prop::DD(points) => self.dimmed = points.iter().map(|p| (p.x, p.y)).collect(),
219                go::Prop::LB(labels) => {
220                    self.labels = labels
221                        .iter()
222                        .map(|(p, t)| ((p.x, p.y), t.to_string()))
223                        .collect()
224                }
225                go::Prop::LN(pairs) => {
226                    self.lines = pairs
227                        .iter()
228                        .map(|(p1, p2)| ((p1.x, p1.y), (p2.x, p2.y)))
229                        .collect()
230                }
231                go::Prop::AR(pairs) => {
232                    self.arrows = pairs
233                        .iter()
234                        .map(|(p1, p2)| ((p1.x, p1.y), (p2.x, p2.y)))
235                        .collect()
236                }
237                _ => {}
238            }
239        }
240
241        Ok(())
242    }
243
244    fn add_stone(&mut self, stone: Stone) -> Result<(), GobanError> {
245        if stone.x > self.size.0 || stone.y > self.size.1 {
246            return Err(GobanError::InvalidMove);
247        }
248        let key = (stone.x, stone.y);
249        self.stones.insert(key, stone.color);
250
251        Ok(())
252    }
253
254    fn play_stone(&mut self, stone: Stone) -> Result<(), GobanError> {
255        self.stones_before_move.insert(
256            self.move_number,
257            self.stones
258                .iter()
259                .map(|(point, color)| Stone {
260                    x: point.0,
261                    y: point.1,
262                    color: *color,
263                })
264                .collect(),
265        );
266        self.add_stone(stone)?;
267        let opponent_color = match stone.color {
268            StoneColor::Black => StoneColor::White,
269            StoneColor::White => StoneColor::Black,
270        };
271        // Remove any neighboring groups with no liberties.
272        let key = (stone.x, stone.y);
273        for neighbor in self.neighbors(key) {
274            if let Some(color) = self.stones.get(&neighbor) {
275                if *color == opponent_color {
276                    self.process_captures(neighbor);
277                }
278            }
279        }
280        // Now remove the played stone if still neccessary
281        self.process_captures(key);
282        self.move_number += 1;
283        self.moves.push((self.move_number, stone));
284
285        Ok(())
286    }
287
288    fn clear_point(&mut self, point: (u8, u8)) {
289        self.stones.remove(&point);
290    }
291
292    fn set_move_number(&mut self, num: u64) {
293        self.move_number = num;
294    }
295
296    fn neighbors(&self, point: (u8, u8)) -> impl Iterator<Item = (u8, u8)> {
297        let (x, y) = point;
298        let mut neighbors = vec![];
299        if x < self.size.0 - 1 {
300            neighbors.push((x + 1, y));
301        }
302        if x > 0 {
303            neighbors.push((x - 1, y));
304        }
305        if y < self.size.1 - 1 {
306            neighbors.push((x, y + 1));
307        }
308        if y > 0 {
309            neighbors.push((x, y - 1));
310        }
311
312        neighbors.into_iter()
313    }
314
315    fn process_captures(&mut self, start_point: (u8, u8)) {
316        let group_color = match self.stones.get(&start_point) {
317            Some(color) => color,
318            None => return,
319        };
320        let mut group = HashSet::new();
321        let mut to_process = VecDeque::new();
322        to_process.push_back(start_point);
323        while let Some(p) = to_process.pop_back() {
324            group.insert(p);
325            for neighbor in self.neighbors(p) {
326                if group.contains(&neighbor) {
327                    continue;
328                }
329                match self.stones.get(&neighbor) {
330                    None => return,
331                    Some(c) if c == group_color => {
332                        to_process.push_back(neighbor);
333                    }
334                    _ => {}
335                }
336            }
337        }
338        for stone in group {
339            self.stones.remove(&stone);
340        }
341    }
342
343    fn is_tt_pass(&self, point: go::Point) -> bool {
344        point.x == 19 && point.y == 19 && self.size.0 < 20 && self.size.1 < 20
345    }
346}
347
348#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
349pub enum StoneColor {
350    Black,
351    White,
352}
353
354#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
355pub struct Stone {
356    pub x: u8,
357    pub y: u8,
358    pub color: StoneColor,
359}
360
361impl Stone {
362    pub fn new(x: u8, y: u8, color: StoneColor) -> Stone {
363        Stone { x, y, color }
364    }
365}
366
367fn get_board_size(sgf_node: &SgfNode<go::Prop>) -> Result<(u8, u8), GobanError> {
368    match sgf_node.get_property("SZ") {
369        Some(go::Prop::SZ((x, y))) if *x <= 52 && *y <= 52 => Ok((*x, *y)),
370        None => Ok((19, 19)),
371        _ => Err(GobanError::InvalidSzProperty),
372    }
373}
374
375#[cfg(test)]
376mod tests {
377    use crate::errors::GobanError;
378
379    use super::{get_board_size, Goban};
380
381    #[test]
382    fn play_over_existing_stone() {
383        let result = Goban::from_sgf("(;AB[ac];B[ac])", &Default::default(), true);
384        assert!(result.is_ok());
385    }
386
387    #[test]
388    fn invalid_sz() {
389        let collection = sgf_parse::go::parse("(;SZ[foo])").unwrap();
390        let result = get_board_size(&collection[0]);
391        assert!(matches!(result, Err(GobanError::InvalidSzProperty)));
392    }
393
394    #[test]
395    fn strict_parsing_fails_for_bad_sgf() {
396        let result = Goban::from_sgf("(;AB[ac];B[ac]", &Default::default(), true);
397        assert!(!result.is_ok());
398    }
399
400    #[test]
401    fn lenient_parsing_works_for_bad_sgf() {
402        let result = Goban::from_sgf("(;AB[ac];B[ac]", &Default::default(), false);
403        assert!(result.is_ok());
404    }
405}