sgf_parse/
sgf_node.rs

1use crate::props::{PropertyType, SgfProp};
2
3/// A node in an SGF Game Tree.
4///
5/// Any succesfully constructed node will be serializable, but may or may not be valid.
6/// All game-specific information is encoded in the `Prop` type. Use
7/// [`go::Prop`](`crate::go::Prop`) for go games, and
8/// [`unknown_game::Prop`](`crate::unknown_game::Prop`) for all other games.
9#[derive(Clone, Debug, PartialEq)]
10pub struct SgfNode<Prop: SgfProp> {
11    pub properties: Vec<Prop>,
12    pub children: Vec<Self>,
13    pub is_root: bool,
14}
15
16impl<Prop: SgfProp> Default for SgfNode<Prop> {
17    fn default() -> Self {
18        Self {
19            properties: vec![],
20            children: vec![],
21            is_root: false,
22        }
23    }
24}
25
26impl<Prop: SgfProp> SgfNode<Prop> {
27    /// Returns a new node.
28    ///
29    /// # Examples
30    /// ```
31    /// use sgf_parse::{SgfNode, SgfProp};
32    /// use sgf_parse::go::Prop;
33    ///
34    /// let children = vec![
35    ///     SgfNode::<Prop>::new(
36    ///         vec![Prop::new("B".to_string(), vec!["dd".to_string()])],
37    ///         vec![],
38    ///         false,
39    ///     ),
40    /// ];
41    /// let node = SgfNode::new(vec![Prop::SZ((19, 19))], children, true);
42    /// ```
43    pub fn new(properties: Vec<Prop>, children: Vec<Self>, is_root: bool) -> Self {
44        Self {
45            properties,
46            children,
47            is_root,
48        }
49    }
50
51    /// Returns the property with the provided identifier for the node (if present).
52    ///
53    /// # Examples
54    /// ```
55    /// use sgf_parse::go::{parse, Prop};
56    ///
57    /// let node = parse("(;SZ[13:13];B[de])").unwrap().into_iter().next().unwrap();
58    /// let board_size = match node.get_property("SZ") {
59    ///     Some(Prop::SZ(size)) => size.clone(),
60    ///     None => (19, 19),
61    ///     _ => unreachable!(),
62    /// };
63    /// ```
64    pub fn get_property(&self, identifier: &str) -> Option<&Prop> {
65        self.properties
66            .iter()
67            .find(|&prop| prop.identifier() == identifier)
68    }
69
70    /// Returns an iterator over the children of this node.
71    ///
72    /// # Examples
73    /// ```
74    /// use sgf_parse::go::parse;
75    ///
76    /// let node = parse("(;SZ[19](;B[de])(;B[dd]HO[2]))").unwrap().into_iter().next().unwrap();
77    /// for child in node.children() {
78    ///     if let Some(prop) = child.get_property("HO") {
79    ///        println!("Found a hotspot!")
80    ///     }
81    /// }
82    /// ```
83    pub fn children(&self) -> impl Iterator<Item = &Self> {
84        self.children.iter()
85    }
86
87    /// Returns an iterator over the properties of this node.
88    ///
89    /// # Examples
90    /// ```
91    /// use sgf_parse::go::{parse, Move, Prop};
92    ///
93    /// let node = parse("(;B[de]C[A comment])").unwrap().into_iter().next().unwrap();
94    /// for prop in node.properties() {
95    ///     match prop {
96    ///         Prop::B(mv) => match mv {
97    ///             Move::Move(p) => println!("B Move at {}, {}", p.x, p.y),
98    ///             Move::Pass => println!("B Pass"),
99    ///         }
100    ///         Prop::W(mv) => match mv {
101    ///             Move::Move(p) => println!("W Move at {}, {}", p.x, p.y),
102    ///             Move::Pass => println!("W Pass"),
103    ///         }
104    ///         _ => {},
105    ///     }
106    /// }
107    /// ```
108    pub fn properties(&self) -> impl Iterator<Item = &Prop> {
109        self.properties.iter()
110    }
111
112    /// Returns the serialized SGF for this SgfNode as a complete GameTree.
113    ///
114    /// # Examples
115    /// ```
116    /// use sgf_parse::go::parse;
117    ///
118    /// let sgf = "(;SZ[13:13];B[de])";
119    /// let node = parse(sgf).unwrap().into_iter().next().unwrap();
120    /// assert_eq!(node.serialize(), sgf);
121    /// ```
122    pub fn serialize(&self) -> String {
123        format!("({self})")
124    }
125
126    /// Returns `Ok` if the node's properties are valid according to the SGF FF\[4\] spec.
127    ///
128    /// # Errors
129    /// Returns an error if the node has invalid properties.
130    ///
131    /// # Examples
132    /// ```
133    /// use sgf_parse::InvalidNodeError;
134    /// use sgf_parse::go::parse;
135    ///
136    /// let node = parse("(;B[de]C[A comment]C[Another])").unwrap().into_iter().next().unwrap();
137    /// let result = node.validate();
138    /// assert!(matches!(result, Err(InvalidNodeError::RepeatedIdentifier(_))));
139    /// ```
140    pub fn validate(&self) -> Result<(), InvalidNodeError> {
141        // TODO: Implement this non-recursively
142        self.validate_helper()?;
143        Ok(())
144    }
145
146    // Helper that returns whether a child has any game info in its descendents.
147    fn validate_helper(&self) -> Result<bool, InvalidNodeError> {
148        Prop::validate_properties(&self.properties, self.is_root)?;
149        let has_game_info = self.has_game_info();
150        let mut child_has_game_info = false;
151        for child in self.children() {
152            child_has_game_info |= child.validate_helper()?;
153        }
154        if child_has_game_info && has_game_info {
155            return Err(InvalidNodeError::UnexpectedGameInfo(format!(
156                "{:?}",
157                self.properties
158            )));
159        }
160        Ok(has_game_info)
161    }
162
163    /// Returns an iterator over the nodes of the main variation.
164    ///
165    /// This is a convenience method for iterating through the first child of each node until the
166    /// main line ends.
167    ///
168    /// # Examples
169    /// ```
170    /// use crate::sgf_parse::SgfProp;
171    /// use sgf_parse::go::{parse, Prop};
172    ///
173    /// let sgf = "(;B[ee];W[ce](;B[ge](;W[gd])(;W[gf]))(;B[ce]))";
174    /// let node = &parse(sgf).unwrap()[0];
175    ///
176    /// let moves: Vec<Prop> = node
177    ///     .main_variation()
178    ///     .map(|n| {
179    ///         n.get_property("B")
180    ///             .or_else(|| n.get_property("W"))
181    ///             .unwrap()
182    ///             .clone()
183    ///     })
184    ///     .collect();
185    /// let expected = vec![
186    ///     Prop::new("B".to_string(), vec!["ee".to_string()]),
187    ///     Prop::new("W".to_string(), vec!["ce".to_string()]),
188    ///     Prop::new("B".to_string(), vec!["ge".to_string()]),
189    ///     Prop::new("W".to_string(), vec!["gd".to_string()]),
190    /// ];
191    ///
192    /// assert_eq!(moves, expected);
193    /// ```
194    pub fn main_variation(&self) -> impl Iterator<Item = &Self> {
195        MainVariationIter {
196            node: Some(self),
197            started: false,
198        }
199    }
200
201    /// Returns the move property (if present) on the node.
202    ///
203    /// # Examples
204    /// ```
205    /// use crate::sgf_parse::SgfProp;
206    /// use sgf_parse::go::{parse, Prop};
207    /// let sgf = "(;GM[1]B[tt]C[Comment])";
208    /// let node = &parse(sgf).unwrap()[0];
209    ///
210    /// let mv = node.get_move();
211    /// assert_eq!(mv, Some(&Prop::new("B".to_string(), vec!["tt".to_string()])));
212    /// ```
213    pub fn get_move(&self) -> Option<&Prop> {
214        // Since there can only be one move per node in an sgf, this is safe.
215        self.properties()
216            .find(|p| p.property_type() == Some(PropertyType::Move))
217    }
218
219    fn has_game_info(&self) -> bool {
220        for prop in self.properties() {
221            if let Some(PropertyType::GameInfo) = prop.property_type() {
222                return true;
223            }
224        }
225        false
226    }
227}
228
229impl<Prop: SgfProp> std::fmt::Display for SgfNode<Prop> {
230    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
231        // TODO: Implement this non-recursively
232        let prop_string = self
233            .properties()
234            .map(|x| x.to_string())
235            .collect::<Vec<_>>()
236            .join("");
237        let child_count = self.children().count();
238        let child_string = match child_count {
239            0 => "".to_string(),
240            1 => self.children().next().unwrap().to_string(),
241            _ => self
242                .children()
243                .map(|x| format!("({x})"))
244                .collect::<Vec<_>>()
245                .join(""),
246        };
247        write!(f, ";{prop_string}{child_string}")
248    }
249}
250
251#[derive(Debug)]
252struct MainVariationIter<'a, Prop: SgfProp> {
253    node: Option<&'a SgfNode<Prop>>,
254    started: bool,
255}
256
257impl<'a, Prop: SgfProp> Iterator for MainVariationIter<'a, Prop> {
258    type Item = &'a SgfNode<Prop>;
259
260    fn next(&mut self) -> Option<Self::Item> {
261        if self.started {
262            self.node = self.node.and_then(|n| n.children().next());
263        } else {
264            self.started = true;
265        }
266        self.node
267    }
268}
269
270/// Err type for [`SgfNode::validate`].
271#[derive(Debug, Clone, PartialEq, Eq)]
272pub enum InvalidNodeError {
273    UnexpectedRootProperties(String),
274    UnexpectedGameInfo(String),
275    RepeatedMarkup(String),
276    MultipleMoves(String),
277    RepeatedIdentifier(String),
278    SetupAndMove(String),
279    KoWithoutMove(String),
280    MultipleMoveAnnotations(String),
281    UnexpectedMoveAnnotation(String),
282    MultipleExclusiveAnnotations(String),
283    InvalidProperty(String),
284}
285
286impl std::fmt::Display for InvalidNodeError {
287    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288        match self {
289            InvalidNodeError::UnexpectedRootProperties(context) => {
290                write!(f, "Root properties in non-root node: {context:?}")
291            }
292            InvalidNodeError::UnexpectedGameInfo(context) => {
293                write!(f, "GameInfo properties in node and a child {context:?}")
294            }
295            InvalidNodeError::RepeatedMarkup(context) => {
296                write!(f, "Multiple markup properties on same point {context:?}")
297            }
298            InvalidNodeError::MultipleMoves(context) => {
299                write!(f, "B and W moves in same node {context:?}")
300            }
301            InvalidNodeError::RepeatedIdentifier(context) => {
302                write!(f, "Identifier repeated in node {context:?}")
303            }
304            InvalidNodeError::SetupAndMove(context) => {
305                write!(f, "Setup and move properties in same node {context:?}")
306            }
307            InvalidNodeError::KoWithoutMove(context) => {
308                write!(f, "Ko in node without B or W {context:?}")
309            }
310            InvalidNodeError::MultipleMoveAnnotations(context) => {
311                write!(f, "Multiple move annotations in same node {context:?}")
312            }
313            InvalidNodeError::UnexpectedMoveAnnotation(context) => {
314                write!(f, "Move annotation without move in node {context:?}")
315            }
316            InvalidNodeError::MultipleExclusiveAnnotations(context) => {
317                write!(
318                    f,
319                    "Multiple DM, UC, GW or GB properties in node {context:?}"
320                )
321            }
322            InvalidNodeError::InvalidProperty(context) => {
323                write!(f, "Invalid property: {context:?}")
324            }
325        }
326    }
327}
328
329impl std::error::Error for InvalidNodeError {}
330
331#[cfg(test)]
332mod tests {
333    use super::InvalidNodeError;
334    use crate::go::parse;
335
336    #[test]
337    fn validate_sample_sgf_valid() {
338        let mut sgf_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
339        sgf_path.push("resources/test/ff4_ex.sgf");
340        let sgf = std::fs::read_to_string(sgf_path).unwrap();
341        let node = &parse(&sgf).unwrap()[0];
342        assert!(node.validate().is_ok());
343    }
344
345    #[test]
346    fn validate_valid_node() {
347        let sgf = "(;SZ[9]HA[3]C[Some comment];B[de];W[fe])";
348        let node = &parse(sgf).unwrap()[0];
349        assert!(node.validate().is_ok());
350    }
351
352    #[test]
353    fn validate_unexpected_root_properties() {
354        let sgf = "(;SZ[9]C[Some comment];GM[1])";
355        let node = &parse(sgf).unwrap()[0];
356        assert!(matches!(
357            node.validate(),
358            Err(InvalidNodeError::UnexpectedRootProperties(_))
359        ));
360    }
361
362    #[test]
363    fn validate_unexpected_game_info() {
364        let sgf = "(;SZ[9]KM[3.5]C[Some comment];HA[3])";
365        let node = &parse(sgf).unwrap()[0];
366        assert!(matches!(
367            node.validate(),
368            Err(InvalidNodeError::UnexpectedGameInfo(_))
369        ));
370    }
371
372    #[test]
373    fn validate_repeated_markup() {
374        let sgf = "(;SZ[9]KM[3.5]C[Some comment];CR[dd]TR[dd])";
375        let node = &parse(sgf).unwrap()[0];
376        assert!(matches!(
377            node.validate(),
378            Err(InvalidNodeError::RepeatedMarkup(_))
379        ));
380    }
381
382    #[test]
383    fn validate_multiple_moves() {
384        let sgf = "(;SZ[9]C[Some comment];B[dd]W[cd])";
385        let node = &parse(sgf).unwrap()[0];
386        assert!(matches!(
387            node.validate(),
388            Err(InvalidNodeError::MultipleMoves(_))
389        ));
390    }
391
392    #[test]
393    fn validate_repeated_identifier() {
394        let sgf = "(;SZ[9]HA[3]HA[4])";
395        let node = &parse(sgf).unwrap()[0];
396        assert!(matches!(
397            node.validate(),
398            Err(InvalidNodeError::RepeatedIdentifier(_))
399        ));
400    }
401
402    #[test]
403    fn validate_setup_and_move() {
404        let sgf = "(;AB[dd]B[cc])";
405        let node = &parse(sgf).unwrap()[0];
406        assert!(matches!(
407            node.validate(),
408            Err(InvalidNodeError::SetupAndMove(_))
409        ));
410    }
411
412    #[test]
413    fn validate_ko_without_move() {
414        let sgf = "(;KO[])";
415        let node = &parse(sgf).unwrap()[0];
416        assert!(matches!(
417            node.validate(),
418            Err(InvalidNodeError::KoWithoutMove(_))
419        ));
420    }
421
422    #[test]
423    fn validate_multiple_move_annotations() {
424        let sgf = "(;B[dd]DO[]BM[1])";
425        let node = &parse(sgf).unwrap()[0];
426        assert!(matches!(
427            node.validate(),
428            Err(InvalidNodeError::MultipleMoveAnnotations(_))
429        ));
430    }
431
432    #[test]
433    fn validate_unexpected_move_annotation() {
434        let sgf = "(;BM[1])";
435        let node = &parse(sgf).unwrap()[0];
436        assert!(matches!(
437            node.validate(),
438            Err(InvalidNodeError::UnexpectedMoveAnnotation(_))
439        ));
440    }
441
442    #[test]
443    fn validate_multiple_exclusive_annotations() {
444        let sgf = "(;UC[2]GW[2])";
445        let node = &parse(sgf).unwrap()[0];
446        assert!(matches!(
447            node.validate(),
448            Err(InvalidNodeError::MultipleExclusiveAnnotations(_))
449        ));
450    }
451
452    #[test]
453    fn validate_invalid_property() {
454        let sgf = "(;BM[Invalid])";
455        let node = &parse(sgf).unwrap()[0];
456        assert!(matches!(
457            node.validate(),
458            Err(InvalidNodeError::InvalidProperty(_))
459        ));
460    }
461}