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 {:?}",
320                    context
321                )
322            }
323            InvalidNodeError::InvalidProperty(context) => {
324                write!(f, "Invalid property: {}", context)
325            }
326        }
327    }
328}
329
330impl std::error::Error for InvalidNodeError {}
331
332#[cfg(test)]
333mod tests {
334    use super::InvalidNodeError;
335    use crate::go::parse;
336
337    #[test]
338    fn validate_sample_sgf_valid() {
339        let mut sgf_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
340        sgf_path.push("resources/test/ff4_ex.sgf");
341        let sgf = std::fs::read_to_string(sgf_path).unwrap();
342        let node = &parse(&sgf).unwrap()[0];
343        assert!(node.validate().is_ok());
344    }
345
346    #[test]
347    fn validate_valid_node() {
348        let sgf = "(;SZ[9]HA[3]C[Some comment];B[de];W[fe])";
349        let node = &parse(sgf).unwrap()[0];
350        assert!(node.validate().is_ok());
351    }
352
353    #[test]
354    fn validate_unexpected_root_properties() {
355        let sgf = "(;SZ[9]C[Some comment];GM[1])";
356        let node = &parse(sgf).unwrap()[0];
357        assert!(matches!(
358            node.validate(),
359            Err(InvalidNodeError::UnexpectedRootProperties(_))
360        ));
361    }
362
363    #[test]
364    fn validate_unexpected_game_info() {
365        let sgf = "(;SZ[9]KM[3.5]C[Some comment];HA[3])";
366        let node = &parse(sgf).unwrap()[0];
367        assert!(matches!(
368            node.validate(),
369            Err(InvalidNodeError::UnexpectedGameInfo(_))
370        ));
371    }
372
373    #[test]
374    fn validate_repeated_markup() {
375        let sgf = "(;SZ[9]KM[3.5]C[Some comment];CR[dd]TR[dd])";
376        let node = &parse(sgf).unwrap()[0];
377        assert!(matches!(
378            node.validate(),
379            Err(InvalidNodeError::RepeatedMarkup(_))
380        ));
381    }
382
383    #[test]
384    fn validate_multiple_moves() {
385        let sgf = "(;SZ[9]C[Some comment];B[dd]W[cd])";
386        let node = &parse(sgf).unwrap()[0];
387        assert!(matches!(
388            node.validate(),
389            Err(InvalidNodeError::MultipleMoves(_))
390        ));
391    }
392
393    #[test]
394    fn validate_repeated_identifier() {
395        let sgf = "(;SZ[9]HA[3]HA[4])";
396        let node = &parse(sgf).unwrap()[0];
397        assert!(matches!(
398            node.validate(),
399            Err(InvalidNodeError::RepeatedIdentifier(_))
400        ));
401    }
402
403    #[test]
404    fn validate_setup_and_move() {
405        let sgf = "(;AB[dd]B[cc])";
406        let node = &parse(sgf).unwrap()[0];
407        assert!(matches!(
408            node.validate(),
409            Err(InvalidNodeError::SetupAndMove(_))
410        ));
411    }
412
413    #[test]
414    fn validate_ko_without_move() {
415        let sgf = "(;KO[])";
416        let node = &parse(sgf).unwrap()[0];
417        assert!(matches!(
418            node.validate(),
419            Err(InvalidNodeError::KoWithoutMove(_))
420        ));
421    }
422
423    #[test]
424    fn validate_multiple_move_annotations() {
425        let sgf = "(;B[dd]DO[]BM[1])";
426        let node = &parse(sgf).unwrap()[0];
427        assert!(matches!(
428            node.validate(),
429            Err(InvalidNodeError::MultipleMoveAnnotations(_))
430        ));
431    }
432
433    #[test]
434    fn validate_unexpected_move_annotation() {
435        let sgf = "(;BM[1])";
436        let node = &parse(sgf).unwrap()[0];
437        assert!(matches!(
438            node.validate(),
439            Err(InvalidNodeError::UnexpectedMoveAnnotation(_))
440        ));
441    }
442
443    #[test]
444    fn validate_multiple_exclusive_annotations() {
445        let sgf = "(;UC[2]GW[2])";
446        let node = &parse(sgf).unwrap()[0];
447        assert!(matches!(
448            node.validate(),
449            Err(InvalidNodeError::MultipleExclusiveAnnotations(_))
450        ));
451    }
452
453    #[test]
454    fn validate_invalid_property() {
455        let sgf = "(;BM[Invalid])";
456        let node = &parse(sgf).unwrap()[0];
457        assert!(matches!(
458            node.validate(),
459            Err(InvalidNodeError::InvalidProperty(_))
460        ));
461    }
462}