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 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 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}