diff --git a/sgf/src/game.rs b/sgf/src/game.rs index c7b0578..68754e0 100644 --- a/sgf/src/game.rs +++ b/sgf/src/game.rs @@ -1,175 +1,34 @@ -use crate::{parser, Color}; +use crate::{ + parser::{self, Annotation, Evaluation, UnknownProperty}, + Color, +}; use std::{collections::HashSet, time::Duration}; use uuid::Uuid; +/* #[derive(Clone, Debug, PartialEq)] pub enum PropertyError { ConflictingProperty, } + */ #[derive(Clone, Debug, PartialEq)] -pub enum SetupError { +pub enum MoveNodeError { + IncompatibleProperty, + ConflictingProperty, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum SetupNodeError { + IncompatibleProperty, ConflictingPosition, } #[derive(Clone, Debug, PartialEq)] -pub enum ConversionError { - IncompatibleNodeType, - InvalidPositionSyntax, +pub enum GameNodeError { + UnsupportedGameNode, } -#[derive(Clone, Debug, PartialEq)] -pub enum Evaluation { - Even, - GoodForBlack, - GoodForWhite, - Unclear, -} - -#[derive(Clone, Debug, PartialEq)] -pub enum Annotation { - BadMove, - DoubtfulMove, - InterestingMove, - Tesuji, -} - -/* -#[derive(Clone, Debug, PartialEq)] -pub enum NodeProperty { - Comment(String), - EvenResult(Double), - GoodForBlack(Double), - GoodForWhite(Double), - Hotspot(Double), - NodeName(String), - Unclear(Double), - Value(f64), - Unknown((String, String)), -} - -#[derive(Clone, Debug, PartialEq)] -pub struct NodeProperties(Vec); - -impl NodeProperties { - fn property_conflicts(&self, prop: &NodeProperty) -> bool { - match prop { - NodeProperty::EvenResult(_) - | NodeProperty::GoodForBlack(_) - | NodeProperty::GoodForWhite(_) - | NodeProperty::Unclear(_) => self.contains_exclusive(), - _ => false, - } - } - - fn contains_exclusive(&self) -> bool { - self.0.iter().any(|prop| match prop { - NodeProperty::EvenResult(_) - | NodeProperty::GoodForBlack(_) - | NodeProperty::GoodForWhite(_) - | NodeProperty::Unclear(_) => true, - _ => false, - }) - } -} - -impl From<&MoveProperties> for NodeProperties { - fn from(props: &MoveProperties) -> NodeProperties { - let props: Vec = props - .0 - .iter() - .filter_map(|prop| { - if let MoveProperty::NodeProperty(p) = prop { - Some(p.clone()) - } else { - None - } - }) - .collect(); - NodeProperties(props) - } -} - -#[derive(Clone, Debug, PartialEq)] -pub enum TimingProperty { - BlackTimeLeft(f64), - BlackMovesLeft(f64), - WhiteMovesLeft(f64), - WhiteTimeLeft(f64), -} - -#[derive(Clone, Debug, PartialEq)] -pub enum MoveAnnotationProperty { - BadMove(f64), - DoubtfulMove(f64), - InterestingMove(f64), - Tesuji(f64), -} - -#[derive(Clone, Debug, PartialEq)] -pub enum MoveProperty { - NodeProperty(NodeProperty), - TimingProperty(TimingProperty), - MoveAnnotationProperty(MoveAnnotationProperty), -} - -#[derive(Clone, Debug, PartialEq, Default)] -pub struct MoveProperties(Vec); - -impl MoveProperties { - pub fn len(&self) -> usize { - self.0.len() - } - - pub fn add_property(&mut self, prop: MoveProperty) -> Result<(), PropertyError> { - match prop { - MoveProperty::NodeProperty(node_prop) => { - if NodeProperties::from(self).property_conflicts(&node_prop) { - self.0.push(prop); - } else { - return Err(PropertyError::ConflictingProperty); - } - } - MoveProperty::TimingProperty(_) => {} - MoveProperty::MoveAnnotationProperty(_) => { - if self - .0 - .iter() - .filter(|prop| match prop { - MoveProperty::MoveAnnotationProperty(_) => true, - _ => false, - }) - .count() - > 0 - { - return Err(PropertyError::ConflictingProperty); - } - self.0.push(prop); - } - } - return Ok(()); - } -} - -#[derive(Clone, Debug, PartialEq)] -pub enum SetupProperty { - NodeProperty(NodeProperty), - PlayerTurn(Color), -} - -#[derive(Clone, Debug, PartialEq, Default)] -pub struct SetupProperties(Vec); - -impl SetupProperties { - pub fn add_property(&mut self, prop: SetupProperty) { - match prop { - SetupProperty::NodeProperty(_) => {} - SetupProperty::PlayerTurn(_) => self.0.push(prop), - } - } -} -*/ - #[derive(Clone, Debug, PartialEq)] pub enum GameNode { MoveNode(MoveNode), @@ -228,9 +87,12 @@ impl Node for GameNode { } impl TryFrom<&parser::Node> for GameNode { - type Error = ConversionError; + type Error = GameNodeError; fn try_from(n: &parser::Node) -> Result { - unimplemented!() + MoveNode::try_from(n) + .map(GameNode::MoveNode) + .or_else(|_| SetupNode::try_from(n).map(GameNode::SetupNode)) + .map_err(|_| Self::Error::UnsupportedGameNode) } } @@ -305,19 +167,57 @@ impl Node for MoveNode { } impl TryFrom<&parser::Node> for MoveNode { - type Error = ConversionError; + type Error = MoveNodeError; fn try_from(n: &parser::Node) -> Result { - match n.move_() { + match n.mv() { Some((color, position)) => { let mut s = Self::new(color, position); - s.time_left = n.time_left(); - s.comments = n.comments(); + for prop in n.properties.iter() { + match prop { + parser::Property::Move((color, position)) => { + if s.color != *color || s.position != *position { + return Err(Self::Error::ConflictingProperty); + } + } + parser::Property::TimeLeft((color, duration)) => { + if s.color != *color { + return Err(Self::Error::ConflictingProperty); + } + if s.time_left.is_some() { + return Err(Self::Error::ConflictingProperty); + } + s.time_left = Some(duration.clone()); + } + parser::Property::Comment(cmt) => { + if s.comments.is_some() { + return Err(Self::Error::ConflictingProperty); + } + s.comments = Some(cmt.clone()); + } + parser::Property::Evaluation(evaluation) => { + if s.evaluation.is_some() { + return Err(Self::Error::ConflictingProperty); + } + s.evaluation = Some(*evaluation) + } + parser::Property::Annotation(annotation) => { + if s.annotation.is_some() { + return Err(Self::Error::ConflictingProperty); + } + s.annotation = Some(*annotation) + } + parser::Property::Unknown(UnknownProperty { ident, value }) => { + s.unknown_props.push((ident.clone(), value.clone())); + } + _ => return Err(Self::Error::IncompatibleProperty), + } + } Ok(s) } - None => Err(ConversionError::IncompatibleNodeType), + None => Err(MoveNodeError::IncompatibleProperty), } } } @@ -339,20 +239,12 @@ fn parse_position(s: &str) -> Result { pub struct SetupNode { id: Uuid, - positions: Vec<(Option, String)>, + positions: Vec, children: Vec, } impl SetupNode { - pub fn new(positions: Vec<(Option, String)>) -> Result { - let mut coords: HashSet = HashSet::new(); - for coord in positions.iter().map(|p| p.1.clone()) { - if coords.contains(&coord) { - return Err(SetupError::ConflictingPosition); - } - coords.insert(coord); - } - + pub fn new(positions: Vec) -> Result { Ok(Self { id: Uuid::new_v4(), positions, @@ -371,6 +263,17 @@ impl Node for SetupNode { } } +impl TryFrom<&parser::Node> for SetupNode { + type Error = SetupNodeError; + + fn try_from(n: &parser::Node) -> Result { + match n.setup() { + Some(elements) => Self::new(elements), + None => Err(Self::Error::IncompatibleProperty), + } + } +} + pub fn path_to_node<'a>(node: &'a GameNode, id: Uuid) -> Vec<&'a GameNode> { if node.id() == id { return vec![node]; @@ -452,6 +355,8 @@ mod root_node_tests { #[cfg(test)] mod move_node_tests { + use crate::parser::PositionList; + use super::*; use cool_asserts::assert_matches; @@ -476,12 +381,28 @@ mod move_node_tests { #[test] fn it_rejects_an_sgf_setup_node() { - unimplemented!() + let n = parser::Node { + properties: vec![ + parser::Property::Move((Color::White, "dp".to_owned())), + parser::Property::TimeLeft((Color::White, Duration::from_secs(176))), + parser::Property::SetupBlackStones(PositionList(vec![ + "dd".to_owned(), + "de".to_owned(), + ])), + ], + next: vec![], + }; + assert_eq!( + MoveNode::try_from(&n), + Err(MoveNodeError::IncompatibleProperty) + ); } } #[cfg(test)] mod setup_node_tests { + use crate::parser::SetupInstr; + use super::*; use cool_asserts::assert_matches; @@ -493,18 +414,18 @@ mod setup_node_tests { fn it_rejects_conflicting_placement_properties() { assert_matches!( SetupNode::new(vec![ - (Some(Color::Black), "dd".to_owned(),), - (Some(Color::Black), "dd".to_owned(),), + SetupInstr::Piece((Color::Black, "dd".to_owned())), + SetupInstr::Piece((Color::Black, "dd".to_owned())), ]), - Err(SetupError::ConflictingPosition) + Err(SetupNodeError::ConflictingPosition) ); assert_matches!( SetupNode::new(vec![ - (Some(Color::Black), "dd".to_owned(),), - (Some(Color::Black), "ee".to_owned(),), - (Some(Color::White), "ee".to_owned(),), + SetupInstr::Piece((Color::Black, "dd".to_owned())), + SetupInstr::Piece((Color::Black, "ee".to_owned())), + SetupInstr::Piece((Color::White, "ee".to_owned())), ]), - Err(SetupError::ConflictingPosition) + Err(SetupNodeError::ConflictingPosition) ); } } diff --git a/sgf/src/parser.rs b/sgf/src/parser.rs index 6a2a216..c5f7d93 100644 --- a/sgf/src/parser.rs +++ b/sgf/src/parser.rs @@ -28,12 +28,28 @@ impl From for ParseSizeError { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq)] pub enum Double { Normal, Emphasized, } +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Annotation { + BadMove, + DoubtfulMove, + InterestingMove, + Tesuji, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Evaluation { + Even, + GoodForBlack, + GoodForWhite, + Unclear, +} + #[derive(Clone, Debug, PartialEq)] pub enum GameType { Go, @@ -209,13 +225,13 @@ impl TryFrom<&str> for Size { pub struct Position(String); #[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct PositionList(Vec); +pub struct PositionList(pub Vec); impl PositionList { pub fn compressed_list(&self) -> String { self.0 .iter() - .map(|v| v.0.clone()) + .map(|v| v.clone()) .collect::>() .join(":") } @@ -238,26 +254,59 @@ pub struct Node { pub next: Vec, } +#[derive(Clone, Debug, PartialEq)] +pub enum SetupInstr { + Piece((Color, String)), + Clear(String), +} + impl Node { - pub fn move_(&self) -> Option<(Color, String)> { + pub fn mv(&self) -> Option<(Color, String)> { self.find_by(|prop| match prop { Property::Move(val) => Some(val.clone()), _ => None, }) } - pub fn time_left(&self) -> Option { - self.find_by(|prop| match prop { - Property::TimeLeft((_, duration)) => Some(duration.clone()), - _ => None, - }) - } - - pub fn comments(&self) -> Option { - self.find_by(|prop| match prop { - Property::Comment(c) => Some(c.clone()), - _ => None, - }) + pub fn setup(&self) -> Option> { + let mut setup = Vec::new(); + for prop in self.properties.iter() { + match prop { + Property::SetupBlackStones(positions) => { + setup.append( + &mut positions + .0 + .iter() + .map(|pos| SetupInstr::Piece((Color::Black, pos.clone()))) + .collect::>(), + ); + } + Property::SetupWhiteStones(positions) => { + setup.append( + &mut positions + .0 + .iter() + .map(|pos| SetupInstr::Piece((Color::White, pos.clone()))) + .collect::>(), + ); + } + Property::ClearStones(positions) => { + setup.append( + &mut positions + .0 + .iter() + .map(|pos| SetupInstr::Clear(pos.clone())) + .collect::>(), + ); + } + _ => unimplemented!(), + } + } + if setup.len() > 0 { + Some(setup) + } else { + None + } } fn find_by(&self, f: F) -> Option @@ -318,17 +367,8 @@ pub enum Property { // C Comment(String), - // BM - BadMove, - - // DO - DoubtfulMove, - - // IT - InterestingMove, - - // TE - Tesuji, + // BM, DO, IT, TE + Annotation(Annotation), // AP Application(String), @@ -360,17 +400,8 @@ pub enum Property { // PL NextPlayer(Color), - // DM - EvenResult, - - // GB - GoodForBlack, - - // GW - GoodForWhite, - - // UC - UnclearResult, + // DM, GB, GW, UC + Evaluation(Evaluation), // HO Hotspot, @@ -449,8 +480,8 @@ pub enum Property { #[derive(Clone, Debug, PartialEq)] pub struct UnknownProperty { - ident: String, - value: String, + pub ident: String, + pub value: String, } impl ToString for Property { @@ -463,10 +494,10 @@ impl ToString for Property { format!("{}[{}]", color.abbreviation(), time.as_secs()) } Property::Comment(value) => format!("C[{}]", value), - Property::BadMove => "BM[]".to_owned(), - Property::DoubtfulMove => "DO[]".to_owned(), - Property::InterestingMove => "IT[]".to_owned(), - Property::Tesuji => "TE[]".to_owned(), + Property::Annotation(Annotation::BadMove) => "BM[]".to_owned(), + Property::Annotation(Annotation::DoubtfulMove) => "DO[]".to_owned(), + Property::Annotation(Annotation::InterestingMove) => "IT[]".to_owned(), + Property::Annotation(Annotation::Tesuji) => "TE[]".to_owned(), Property::Application(app) => format!("AP[{}]", app), Property::Charset(set) => format!("CA[{}]", set), Property::FileFormat(ff) => format!("FF[{}]", ff), @@ -489,10 +520,10 @@ impl ToString for Property { format!("AW[{}]", positions.compressed_list(),) } Property::NextPlayer(color) => format!("PL[{}]", color.abbreviation()), - Property::EvenResult => "DM[]".to_owned(), - Property::GoodForBlack => "GB[]".to_owned(), - Property::GoodForWhite => "GW[]".to_owned(), - Property::UnclearResult => "UC[]".to_owned(), + Property::Evaluation(Evaluation::Even) => "DM[]".to_owned(), + Property::Evaluation(Evaluation::GoodForBlack) => "GB[]".to_owned(), + Property::Evaluation(Evaluation::GoodForWhite) => "GW[]".to_owned(), + Property::Evaluation(Evaluation::Unclear) => "UC[]".to_owned(), Property::Hotspot => "HO[]".to_owned(), Property::Value(value) => format!("V[{}]", value), Property::Annotator(value) => format!("AN[{}]", value), @@ -574,14 +605,18 @@ fn parse_property<'a, E: nom::error::ParseError<&'a str>>( "C" => parse_propval(parse_comment())(input)?, "WL" => parse_propval(parse_time_left(Color::White))(input)?, "BL" => parse_propval(parse_time_left(Color::Black))(input)?, - "BM" => discard_propval().map(|_| Property::BadMove).parse(input)?, + "BM" => discard_propval() + .map(|_| Property::Annotation(Annotation::BadMove)) + .parse(input)?, "DO" => discard_propval() - .map(|_| Property::DoubtfulMove) + .map(|_| Property::Annotation(Annotation::DoubtfulMove)) .parse(input)?, "IT" => discard_propval() - .map(|_| Property::InterestingMove) + .map(|_| Property::Annotation(Annotation::InterestingMove)) + .parse(input)?, + "TE" => discard_propval() + .map(|_| Property::Annotation(Annotation::Tesuji)) .parse(input)?, - "TE" => discard_propval().map(|_| Property::Tesuji).parse(input)?, "AP" => parse_propval(parse_simple_text().map(Property::Application))(input)?, "CA" => parse_propval(parse_simple_text().map(Property::Charset))(input)?, "FF" => parse_propval(parse_number().map(Property::FileFormat))(input)?, @@ -589,16 +624,16 @@ fn parse_property<'a, E: nom::error::ParseError<&'a str>>( "ST" => unimplemented!(), "SZ" => unimplemented!(), "DM" => discard_propval() - .map(|_| Property::EvenResult) + .map(|_| Property::Evaluation(Evaluation::Even)) .parse(input)?, "GB" => discard_propval() - .map(|_| Property::GoodForBlack) + .map(|_| Property::Evaluation(Evaluation::GoodForBlack)) .parse(input)?, "GW" => discard_propval() - .map(|_| Property::GoodForWhite) + .map(|_| Property::Evaluation(Evaluation::GoodForWhite)) .parse(input)?, "UC" => discard_propval() - .map(|_| Property::UnclearResult) + .map(|_| Property::Evaluation(Evaluation::Unclear)) .parse(input)?, "V" => unimplemented!(), "AN" => parse_propval(parse_simple_text().map(Property::Annotator))(input)?,