diff --git a/sgf/Cargo.toml b/sgf/Cargo.toml index ab75ce2..2dbd6bf 100644 --- a/sgf/Cargo.toml +++ b/sgf/Cargo.toml @@ -11,3 +11,6 @@ nom = { version = "7" } serde = { version = "1", features = [ "derive" ] } thiserror = { version = "1"} typeshare = { version = "1" } + +[dev-dependencies] +cool_asserts = { version = "2" } diff --git a/sgf/src/go.rs b/sgf/src/go.rs index 167b0c2..f2d0cef 100644 --- a/sgf/src/go.rs +++ b/sgf/src/go.rs @@ -74,6 +74,7 @@ use crate::{ Error, }; use serde::{Deserialize, Serialize}; +use std::ops::Deref; use typeshare::typeshare; #[derive(Clone, Debug)] @@ -84,11 +85,18 @@ pub struct Game { pub tree: Tree, } +impl Deref for Game { + type Target = Tree; + fn deref(&self) -> &Self::Target { + &self.tree + } +} + impl TryFrom for Game { type Error = Error; fn try_from(tree: Tree) -> Result { - let board_size = match tree.sequence[0].find_prop("SZ") { + let board_size = match tree.root.find_prop("SZ") { Some(prop) => Size::try_from(prop.values[0].as_str())?, None => Size { width: 19, @@ -96,35 +104,34 @@ impl TryFrom for Game { }, }; let mut info = GameInfo::default(); - info.app_name = tree.sequence[0] - .find_prop("AP") - .map(|prop| prop.values[0].clone()); - info.black_player = tree.sequence[0] - .find_prop("PB") - .map(|prop| prop.values.join(", ")); + info.app_name = tree.root.find_prop("AP").map(|prop| prop.values[0].clone()); + info.black_player = tree.root.find_prop("PB").map(|prop| prop.values.join(", ")); - info.black_rank = tree.sequence[0] + info.black_rank = tree + .root .find_prop("BR") .and_then(|prop| Rank::try_from(prop.values[0].as_str()).ok()); - info.white_player = tree.sequence[0] - .find_prop("PW") - .map(|prop| prop.values.join(", ")); + info.white_player = tree.root.find_prop("PW").map(|prop| prop.values.join(", ")); - info.white_rank = tree.sequence[0] + info.white_rank = tree + .root .find_prop("WR") .and_then(|prop| Rank::try_from(prop.values[0].as_str()).ok()); - info.result = tree.sequence[0] + info.result = tree + .root .find_prop("RE") .and_then(|prop| GameResult::try_from(prop.values[0].as_str()).ok()); - info.time_limits = tree.sequence[0] + info.time_limits = tree + .root .find_prop("TM") .and_then(|prop| prop.values[0].parse::().ok()) .and_then(|seconds| Some(std::time::Duration::from_secs(seconds))); - info.date = tree.sequence[0] + info.date = tree + .root .find_prop("DT") .and_then(|prop| { let v = prop @@ -144,21 +151,13 @@ impl TryFrom for Game { }) .unwrap_or(vec![]); - info.event = tree.sequence[0] - .find_prop("EV") - .map(|prop| prop.values.join(", ")); + info.event = tree.root.find_prop("EV").map(|prop| prop.values.join(", ")); - info.round = tree.sequence[0] - .find_prop("RO") - .map(|prop| prop.values.join(", ")); + info.round = tree.root.find_prop("RO").map(|prop| prop.values.join(", ")); - info.source = tree.sequence[0] - .find_prop("SO") - .map(|prop| prop.values.join(", ")); + info.source = tree.root.find_prop("SO").map(|prop| prop.values.join(", ")); - info.game_keeper = tree.sequence[0] - .find_prop("US") - .map(|prop| prop.values.join(", ")); + info.game_keeper = tree.root.find_prop("US").map(|prop| prop.values.join(", ")); Ok(Game { board_size, @@ -301,7 +300,7 @@ mod tests { use super::*; use crate::{ date::Date, - tree::{parse_collection, Size}, + tree::{parse_collection, Property, Size}, }; use std::fs::File; use std::io::Read; @@ -384,4 +383,32 @@ mod tests { assert_eq!(tree.info.game_keeper, Some("Arno Hollosi".to_owned())); }); } + + #[test] + fn it_presents_a_mainline() { + with_file( + std::path::Path::new("test_data/2020 USGO DDK, Round 1.sgf"), + |trees| { + assert_eq!(trees.len(), 1); + let tree = &trees[0]; + + let node = &tree.root; + println!("{:?}", node); + + let node = node.next(); + println!("{:?}", node); + + /* + assert_eq!( + node.find_prop("B"), + Some(Property { + ident: "B".to_owned(), + values: vec!["dp".to_owned()] + }) + ); + */ + assert!(false); + }, + ); + } } diff --git a/sgf/src/lib.rs b/sgf/src/lib.rs index 4b4f8fa..d30fa13 100644 --- a/sgf/src/lib.rs +++ b/sgf/src/lib.rs @@ -61,6 +61,7 @@ pub enum Game { Unsupported(tree::Tree), } +/* pub fn parse_sgf(input: &str) -> Result, Error> { let (_, trees) = parse_collection::>(input)?; Ok(trees @@ -73,6 +74,7 @@ pub fn parse_sgf(input: &str) -> Result, Error> { }) .collect::>()) } +*/ /* impl From<(&str, VerboseErrorKind)> for diff --git a/sgf/src/tree.rs b/sgf/src/tree.rs index 59d24c0..1d9a804 100644 --- a/sgf/src/tree.rs +++ b/sgf/src/tree.rs @@ -54,29 +54,19 @@ impl TryFrom<&str> for Size { #[derive(Clone, Debug, PartialEq)] pub struct Tree { - pub sequence: Vec, - pub sub_sequences: Vec, + pub root: Node, } impl ToString for Tree { fn to_string(&self) -> String { - let sequence = self - .sequence - .iter() - .map(|node| node.to_string()) - .collect::(); - let subsequences = self - .sub_sequences - .iter() - .map(|seq| seq.to_string()) - .collect::(); - format!("({}{})", sequence, subsequences) + format!("({})", self.root.to_string()) } } #[derive(Clone, Debug, PartialEq)] pub struct Node { pub properties: Vec, + pub next: Vec, } impl ToString for Node { @@ -86,7 +76,21 @@ impl ToString for Node { .iter() .map(|prop| prop.to_string()) .collect::(); - format!(";{}", props) + + let next = if self.next.len() == 1 { + self.next + .iter() + .map(|node| node.to_string()) + .collect::>() + .join("") + } else { + self.next + .iter() + .map(|node| format!("({})", node.to_string())) + .collect::>() + .join("") + }; + format!(";{}{}", props, next) } } @@ -97,6 +101,10 @@ impl Node { .find(|prop| prop.ident == ident) .cloned() } + + pub fn next<'a>(&'a self) -> Option<&'a Node> { + self.next.get(0) + } } #[derive(Clone, Debug, PartialEq)] @@ -119,40 +127,40 @@ impl ToString for Property { pub fn parse_collection<'a, E: nom::error::ParseError<&'a str>>( input: &'a str, ) -> IResult<&'a str, Vec, E> { - separated_list1(multispace1, parse_tree)(input) + let (input, roots) = separated_list1(multispace1, parse_tree)(input)?; + let trees = roots + .into_iter() + .map(|root| Tree { root }) + .collect::>(); + + Ok((input, trees)) } // note: must preserve unknown properties // note: must fix or preserve illegally formatted game-info properties // note: must correct or delete illegally foramtted properties, but display a warning -fn parse_tree<'a, E: nom::error::ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, Tree, E> { +fn parse_tree<'a, E: nom::error::ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, Node, E> { let (input, _) = multispace0(input)?; - delimited(tag("("), parse_sequence, tag(")"))(input) -} + let (input, _) = tag("(")(input)?; + let (input, node) = parse_node(input)?; + let (input, _) = multispace0(input)?; + let (input, _) = tag(")")(input)?; -fn parse_sequence<'a, E: nom::error::ParseError<&'a str>>( - input: &'a str, -) -> IResult<&'a str, Tree, E> { - let (input, _) = multispace0(input)?; - let (input, nodes) = many1(parse_node)(input)?; - let (input, _) = multispace0(input)?; - let (input, sub_sequences) = many0(parse_tree)(input)?; - let (input, _) = multispace0(input)?; - - Ok(( - input, - Tree { - sequence: nodes, - sub_sequences, - }, - )) + Ok((input, node)) } fn parse_node<'a, E: nom::error::ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, Node, E> { let (input, _) = multispace0(input)?; - let (input, _) = tag(";")(input)?; + let (input, _) = opt(tag(";"))(input)?; let (input, properties) = many1(parse_property)(input)?; - Ok((input, Node { properties })) + + let (input, next) = opt(parse_node)(input)?; + let (input, mut next_seq) = many0(parse_tree)(input)?; + + let mut next = next.map(|n| vec![n]).unwrap_or(vec![]); + next.append(&mut next_seq); + + Ok((input, Node { properties, next })) } fn parse_property<'a, E: nom::error::ParseError<&'a str>>( @@ -219,9 +227,8 @@ pub fn parse_size<'a, E: nom::error::ParseError<&'a str>>( #[cfg(test)] mod test { - use std::{fs::File, io::Read}; - use super::*; + use cool_asserts::assert_matches; const EXAMPLE: &'static str = "(;FF[4]C[root](;C[a];C[b](;C[c]) (;C[d];C[e])) @@ -259,7 +266,8 @@ mod test { properties: vec![Property { ident: "B".to_owned(), values: vec!["ab".to_owned()] - }] + }], + next: vec![] } ); @@ -273,6 +281,25 @@ mod test { properties: vec![Property { ident: "B".to_owned(), values: vec!["ab".to_owned()] + }], + next: vec![Node { + properties: vec![Property { + ident: "W".to_owned(), + values: vec!["dp".to_owned()] + }], + next: vec![Node { + properties: vec![ + Property { + ident: "B".to_owned(), + values: vec!["pq".to_owned()] + }, + Property { + ident: "C".to_owned(), + values: vec!["some comments".to_owned()] + } + ], + next: vec![], + }] }] } ); @@ -286,21 +313,17 @@ mod test { assert_eq!( sequence, - Tree { - sequence: vec![ - Node { - properties: vec![Property { - ident: "B".to_owned(), - values: vec!["ab".to_owned()] - }] - }, - Node { - properties: vec![Property { - ident: "W".to_owned(), - values: vec!["dp".to_owned()] - }] - }, - Node { + Node { + properties: vec![Property { + ident: "B".to_owned(), + values: vec!["ab".to_owned()] + }], + next: vec![Node { + properties: vec![Property { + ident: "W".to_owned(), + values: vec!["dp".to_owned()] + }], + next: vec![Node { properties: vec![ Property { ident: "B".to_owned(), @@ -310,114 +333,158 @@ mod test { ident: "C".to_owned(), values: vec!["some comments".to_owned()] } - ] - } - ], - sub_sequences: vec![], - } + ], + next: vec![], + }] + }], + }, ); } #[test] - fn it_can_parse_a_sequence_with_subsequences() { + fn it_can_parse_a_branching_sequence() { let text = "(;C[a];C[b](;C[c])(;C[d];C[e]))"; - let (_, sequence) = parse_tree::>(text).unwrap(); + let (_, tree) = parse_tree::>(text).unwrap(); - let main_sequence = vec![ - Node { - properties: vec![Property { - ident: "C".to_owned(), - values: vec!["a".to_owned()], - }], - }, - Node { + let expected = Node { + properties: vec![Property { + ident: "C".to_owned(), + values: vec!["a".to_owned()], + }], + next: vec![Node { properties: vec![Property { ident: "C".to_owned(), values: vec!["b".to_owned()], }], - }, - ]; - let subsequence_1 = Tree { - sequence: vec![Node { - properties: vec![Property { - ident: "C".to_owned(), - values: vec!["c".to_owned()], - }], + next: vec![ + Node { + properties: vec![Property { + ident: "C".to_owned(), + values: vec!["c".to_owned()], + }], + next: vec![], + }, + Node { + properties: vec![Property { + ident: "C".to_owned(), + values: vec!["d".to_owned()], + }], + next: vec![Node { + properties: vec![Property { + ident: "C".to_owned(), + values: vec!["e".to_owned()], + }], + next: vec![], + }], + }, + ], }], - sub_sequences: vec![], - }; - let subsequence_2 = Tree { - sequence: vec![ - Node { - properties: vec![Property { - ident: "C".to_owned(), - values: vec!["d".to_owned()], - }], - }, - Node { - properties: vec![Property { - ident: "C".to_owned(), - values: vec!["e".to_owned()], - }], - }, - ], - sub_sequences: vec![], }; - assert_eq!( - sequence, - Tree { - sequence: main_sequence, - sub_sequences: vec![subsequence_1, subsequence_2], - } - ); + assert_eq!(tree, expected); } #[test] fn it_can_parse_example_1() { - let (_, ex_tree) = parse_tree::>(EXAMPLE).unwrap(); - assert_eq!(ex_tree.sequence.len(), 1); + let (_, tree) = parse_tree::>(EXAMPLE).unwrap(); - assert_eq!(ex_tree.sequence[0].properties.len(), 2); - assert_eq!( - ex_tree.sequence[0].properties[0], - Property { - ident: "FF".to_owned(), - values: vec!["4".to_owned()] - } - ); - assert_eq!(ex_tree.sub_sequences.len(), 2); + let j = Node { + properties: vec![Property { + ident: "C".to_owned(), + values: vec!["j".to_owned()], + }], + next: vec![], + }; + let i = Node { + properties: vec![Property { + ident: "C".to_owned(), + values: vec!["i".to_owned()], + }], + next: vec![], + }; + let h = Node { + properties: vec![Property { + ident: "C".to_owned(), + values: vec!["h".to_owned()], + }], + next: vec![i], + }; + let g = Node { + properties: vec![Property { + ident: "C".to_owned(), + values: vec!["g".to_owned()], + }], + next: vec![h], + }; + let f = Node { + properties: vec![Property { + ident: "C".to_owned(), + values: vec!["f".to_owned()], + }], + next: vec![g, j], + }; + let e = Node { + properties: vec![Property { + ident: "C".to_owned(), + values: vec!["e".to_owned()], + }], + next: vec![], + }; + let d = Node { + properties: vec![Property { + ident: "C".to_owned(), + values: vec!["d".to_owned()], + }], + next: vec![e], + }; + let c = Node { + properties: vec![Property { + ident: "C".to_owned(), + values: vec!["c".to_owned()], + }], + next: vec![], + }; + let b = Node { + properties: vec![Property { + ident: "C".to_owned(), + values: vec!["b".to_owned()], + }], + next: vec![c, d], + }; + let a = Node { + properties: vec![Property { + ident: "C".to_owned(), + values: vec!["a".to_owned()], + }], + next: vec![b], + }; + let expected = Node { + properties: vec![ + Property { + ident: "FF".to_owned(), + values: vec!["4".to_owned()], + }, + Property { + ident: "C".to_owned(), + values: vec!["root".to_owned()], + }, + ], + next: vec![a, f], + }; - assert_eq!(ex_tree.sub_sequences[0].sequence.len(), 2); - assert_eq!( - ex_tree.sub_sequences[0].sequence, - vec![ - Node { - properties: vec![Property { - ident: "C".to_owned(), - values: vec!["a".to_owned()] - }] - }, - Node { - properties: vec![Property { - ident: "C".to_owned(), - values: vec!["b".to_owned()] - }] - }, - ] - ); - assert_eq!(ex_tree.sub_sequences[0].sub_sequences.len(), 2); + assert_eq!(tree, expected); } #[test] fn it_can_regenerate_the_tree() { let (_, tree1) = parse_tree::>(EXAMPLE).unwrap(); + let tree1 = Tree { root: tree1 }; assert_eq!( tree1.to_string(), "(;FF[4]C[root](;C[a];C[b](;C[c])(;C[d];C[e]))(;C[f](;C[g];C[h];C[i])(;C[j])))" ); let (_, tree2) = parse_tree::>(&tree1.to_string()).unwrap(); - assert_eq!(tree1, tree2); + assert_eq!(tree1, Tree { root: tree2 }); } #[test]