From d6a91b6af6bf6768ca64b1ea07c2926eb588acbd Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sun, 17 Sep 2023 00:04:58 -0400 Subject: [PATCH] Process GameResult, fix TimeLeft, and allow Pass moves --- sgf/src/game.rs | 28 ++++----- sgf/src/parser.rs | 147 ++++++++++++++++++++++++++++++++++++++++------ sgf/src/types.rs | 1 + 3 files changed, 143 insertions(+), 33 deletions(-) diff --git a/sgf/src/game.rs b/sgf/src/game.rs index 143786d..dd9018a 100644 --- a/sgf/src/game.rs +++ b/sgf/src/game.rs @@ -1,5 +1,5 @@ use crate::{ - parser::{self, Annotation, Evaluation, GameType, SetupInstr, Size, UnknownProperty}, + parser::{self, Annotation, Evaluation, GameType, Move, SetupInstr, Size, UnknownProperty}, Color, Date, GameResult, }; use std::{collections::HashSet, time::Duration}; @@ -188,7 +188,7 @@ impl TryFrom<&parser::Node> for GameNode { pub struct MoveNode { id: Uuid, color: Color, - position: String, + mv: Move, children: Vec, time_left: Option, @@ -202,11 +202,11 @@ pub struct MoveNode { } impl MoveNode { - pub fn new(color: Color, position: String) -> Self { + pub fn new(color: Color, mv: Move) -> Self { Self { id: Uuid::new_v4(), color, - position, + mv, children: Vec::new(), time_left: None, @@ -237,13 +237,13 @@ impl TryFrom<&parser::Node> for MoveNode { fn try_from(n: &parser::Node) -> Result { match n.mv() { - Some((color, position)) => { - let mut s = Self::new(color, position); + Some((color, mv)) => { + let mut s = Self::new(color, mv); for prop in n.properties.iter() { match prop { - parser::Property::Move((color, position)) => { - if s.color != *color || s.position != *position { + parser::Property::Move((color, mv)) => { + if s.color != *color || s.mv != *mv { return Err(Self::Error::ConflictingProperty); } } @@ -386,9 +386,9 @@ mod test { Player::default(), ); - let first_move = MoveNode::new(Color::Black, "dd".to_owned()); + let first_move = MoveNode::new(Color::Black, Move::Move("dd".to_owned())); let first_ = game.add_child(GameNode::MoveNode(first_move.clone())); - let second_move = MoveNode::new(Color::White, "qq".to_owned()); + let second_move = MoveNode::new(Color::White, Move::Move("qq".to_owned())); first_.add_child(GameNode::MoveNode(second_move.clone())); let nodes = game.nodes(); @@ -409,7 +409,7 @@ mod test { fn game_node_can_parse_sgf_move_node() { let n = parser::Node { properties: vec![ - parser::Property::Move((Color::White, "dp".to_owned())), + parser::Property::Move((Color::White, Move::Move("dp".to_owned()))), parser::Property::TimeLeft((Color::White, Duration::from_secs(176))), parser::Property::Comment("Comments in the game".to_owned()), ], @@ -445,7 +445,7 @@ mod move_node_tests { fn it_can_parse_an_sgf_move_node() { let n = parser::Node { properties: vec![ - parser::Property::Move((Color::White, "dp".to_owned())), + parser::Property::Move((Color::White, Move::Move("dp".to_owned()))), parser::Property::TimeLeft((Color::White, Duration::from_secs(176))), parser::Property::Comment("Comments in the game".to_owned()), ], @@ -453,7 +453,7 @@ mod move_node_tests { }; assert_matches!(MoveNode::try_from(&n), Ok(node) => { assert_eq!(node.color, Color::White); - assert_eq!(node.position, "dp".to_owned()); + assert_eq!(node.mv, Move::Move("dp".to_owned())); assert_eq!(node.children, vec![]); assert_eq!(node.time_left, Some(Duration::from_secs(176))); assert_eq!(node.comments, Some("Comments in the game".to_owned())); @@ -464,7 +464,7 @@ mod move_node_tests { fn it_rejects_an_sgf_setup_node() { let n = parser::Node { properties: vec![ - parser::Property::Move((Color::White, "dp".to_owned())), + parser::Property::Move((Color::White, Move::Move("dp".to_owned()))), parser::Property::TimeLeft((Color::White, Duration::from_secs(176))), parser::Property::SetupBlackStones(PositionList(vec![ "dd".to_owned(), diff --git a/sgf/src/parser.rs b/sgf/src/parser.rs index 73fdb54..5d2fd2c 100644 --- a/sgf/src/parser.rs +++ b/sgf/src/parser.rs @@ -1,8 +1,8 @@ -use crate::{Color, Date, Error, GameResult}; +use crate::{Color, Date, Error, GameResult, Win}; use chrono::{Datelike, NaiveDate}; use nom::{ branch::alt, - bytes::complete::{escaped_transform, tag, take_until1}, + bytes::complete::{escaped_transform, tag, take_until, take_until1}, character::complete::{alpha1, digit1, multispace0, multispace1, none_of}, combinator::{opt, value}, error::ParseError, @@ -262,7 +262,7 @@ pub enum SetupInstr { } impl Node { - pub fn mv(&self) -> Option<(Color, String)> { + pub fn mv(&self) -> Option<(Color, Move)> { self.find_by(|prop| match prop { Property::Move(val) => Some(val.clone()), _ => None, @@ -343,6 +343,12 @@ impl ToString for Node { } } +#[derive(Clone, Debug, PartialEq)] +pub enum Move { + Move(String), + Pass, +} + // KO // MN // N @@ -363,7 +369,7 @@ impl ToString for Node { #[derive(Clone, Debug, PartialEq)] pub enum Property { // B, W - Move((Color, String)), + Move((Color, Move)), // C Comment(String), @@ -488,8 +494,11 @@ pub struct UnknownProperty { impl ToString for Property { fn to_string(&self) -> String { match self { - Property::Move((color, position)) => { - format!("{}[{}]", color.abbreviation(), position) + Property::Move((color, Move::Move(mv))) => { + format!("{}[{}]", color.abbreviation(), mv) + } + Property::Move((color, Move::Pass)) => { + format!("{}[]", color.abbreviation()) } Property::TimeLeft((color, time)) => { format!("{}[{}]", color.abbreviation(), time.as_secs()) @@ -643,7 +652,7 @@ fn parse_property<'a, E: nom::error::ParseError<&'a str>>( "BR" => parse_propval(parse_simple_text().map(Property::BlackRank))(input)?, "BT" => parse_propval(parse_simple_text().map(Property::BlackTeam))(input)?, "CP" => parse_propval(parse_simple_text().map(Property::Copyright))(input)?, - "DT" => unimplemented!(), + "DT" => parse_propval(parse_date_field().map(Property::EventDates))(input)?, "EV" => parse_propval(parse_simple_text().map(Property::EventName))(input)?, "GN" => parse_propval(parse_simple_text().map(Property::GameName))(input)?, "GC" => parse_propval(parse_simple_text().map(Property::ExtraGameInformation))(input)?, @@ -652,7 +661,7 @@ fn parse_property<'a, E: nom::error::ParseError<&'a str>>( "PB" => parse_propval(parse_simple_text().map(Property::BlackPlayer))(input)?, "PC" => parse_propval(parse_simple_text().map(Property::GameLocation))(input)?, "PW" => parse_propval(parse_simple_text().map(Property::WhitePlayer))(input)?, - "RE" => unimplemented!(), + "RE" => parse_propval(parse_game_result().map(Property::Result))(input)?, "RO" => parse_propval(parse_simple_text().map(Property::Round))(input)?, "RU" => parse_propval(parse_simple_text().map(Property::Ruleset))(input)?, "SO" => parse_propval(parse_simple_text().map(Property::Source))(input)?, @@ -683,8 +692,14 @@ fn parse_move<'a, E: nom::error::ParseError<&'a str>>( { let color = color.clone(); move |input: &'a str| { - take_until1("]") - .map(|text: &'a str| Property::Move((color.clone(), text.to_owned()))) + take_until("]") + .map(|text: &'a str| { + if text.is_empty() { + Property::Move((color.clone(), Move::Pass)) + } else { + Property::Move((color.clone(), Move::Move(text.to_owned()))) + } + }) .parse(input) } } @@ -705,7 +720,6 @@ fn parse_time_left<'a, E: ParseError<&'a str>>( let color = color.clone(); move |input: &'a str| { let (input, value) = parse_real().parse(input)?; - let (input, _) = tag("]")(input)?; Ok(( input, Property::TimeLeft((color.clone(), Duration::from_secs(value as u64))), @@ -714,7 +728,7 @@ fn parse_time_left<'a, E: ParseError<&'a str>>( } } -pub fn parse_size<'a, E: nom::error::ParseError<&'a str>>() -> impl Parser<&'a str, Size, E> { +fn parse_size<'a, E: nom::error::ParseError<&'a str>>() -> impl Parser<&'a str, Size, E> { |input: &'a str| { let (input, dimensions) = separated_list1(tag(":"), digit1)(input)?; let (width, height) = match dimensions.as_slice() { @@ -729,6 +743,11 @@ pub fn parse_size<'a, E: nom::error::ParseError<&'a str>>() -> impl Parser<&'a s } } +fn parse_game_result<'a, E: nom::error::ParseError<&'a str>>() -> impl Parser<&'a str, GameResult, E> +{ + alt((parse_draw_result(), parse_void_result(), parse_win_result())) +} + fn parse_propval<'a, E: nom::error::ParseError<&'a str>>( mut parser: impl Parser<&'a str, Property, E>, ) -> impl FnMut(&'a str) -> IResult<&'a str, Property, E> { @@ -891,6 +910,62 @@ fn parse_three<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, DateSegment } } +fn parse_draw_result<'a, E: nom::error::ParseError<&'a str>>() -> impl Parser<&'a str, GameResult, E> +{ + tag("0").map(|_| GameResult::Draw) +} + +fn parse_void_result<'a, E: nom::error::ParseError<&'a str>>() -> impl Parser<&'a str, GameResult, E> +{ + tag("Void").map(|_| GameResult::Void) +} + +fn parse_win_result<'a, E: nom::error::ParseError<&'a str>>() -> impl Parser<&'a str, GameResult, E> +{ + |input: &'a str| { + let (input, color) = alt(( + tag("B").map(|_| Color::Black), + tag("b").map(|_| Color::Black), + tag("W").map(|_| Color::White), + tag("w").map(|_| Color::White), + ))(input)?; + let (input, _) = tag("+")(input)?; + let (input, score) = parse_win_score().parse(input)?; + + Ok(( + input, + match color { + Color::Black => GameResult::Black(score), + Color::White => GameResult::White(score), + }, + )) + } +} + +enum WinType { + St(String), + Num(f32), +} + +fn parse_win_score<'a, E: nom::error::ParseError<&'a str>>() -> impl Parser<&'a str, Win, E> { + |input: &'a str| { + let (input, win) = alt(( + parse_real().map(|v| WinType::Num(v)), + parse_simple_text().map(|s| WinType::St(s)), + ))(input)?; + let w = match win { + WinType::St(s) => match s.to_ascii_lowercase().as_str() { + "r" | "resign" => Win::Resignation, + "t" | "time" => Win::Time, + "f" | "forfeit" => Win::Forfeit, + _ => Win::Unknown, + }, + WinType::Num(n) => Win::Score(n), + }; + Ok((input, w)) + } +} + #[cfg(test)] mod test { use super::*; @@ -913,7 +988,7 @@ mod test { assert_eq!( node, Node { - properties: vec![Property::Move((Color::Black, "ab".to_owned()))], + properties: vec![Property::Move((Color::Black, Move::Move("ab".to_owned())))], next: vec![] } ); @@ -925,12 +1000,12 @@ mod test { assert_eq!( node, Node { - properties: vec![Property::Move((Color::Black, "ab".to_owned()))], + properties: vec![Property::Move((Color::Black, Move::Move("ab".to_owned())))], next: vec![Node { - properties: vec![Property::Move((Color::White, "dp".to_owned()))], + properties: vec![Property::Move((Color::White, Move::Move("dp".to_owned())))], next: vec![Node { properties: vec![ - Property::Move((Color::Black, "pq".to_owned())), + Property::Move((Color::Black, Move::Move("pq".to_owned()))), Property::Comment("some comments".to_owned()) ], next: vec![], @@ -949,12 +1024,12 @@ mod test { assert_eq!( sequence, Node { - properties: vec![Property::Move((Color::Black, "ab".to_owned()))], + properties: vec![Property::Move((Color::Black, Move::Move("ab".to_owned())))], next: vec![Node { - properties: vec![Property::Move((Color::White, "dp".to_owned()))], + properties: vec![Property::Move((Color::White, Move::Move("dp".to_owned())))], next: vec![Node { properties: vec![ - Property::Move((Color::Black, "pq".to_owned())), + Property::Move((Color::Black, Move::Move("pq".to_owned()))), Property::Comment("some comments".to_owned()) ], next: vec![], @@ -1190,3 +1265,37 @@ mod date_test { ); } } + +#[cfg(test)] +mod property_tests { + use super::*; + + #[test] + fn it_can_parse_time_left() { + let (_, val) = + parse_time_left::>(Color::Black)("170]").unwrap(); + assert_eq!( + val, + Property::TimeLeft((Color::Black, Duration::from_secs(170))) + ); + + let (_, val) = + parse_time_left::>(Color::Black)("170.6]").unwrap(); + assert_eq!( + val, + Property::TimeLeft((Color::Black, Duration::from_secs(170))) + ); + + let (_, prop) = parse_property::>("BL[170]").unwrap(); + assert_eq!( + prop, + Property::TimeLeft((Color::Black, Duration::from_secs(170))) + ); + + let (_, prop) = parse_property::>("BL[170.5]").unwrap(); + assert_eq!( + prop, + Property::TimeLeft((Color::Black, Duration::from_secs(170))) + ); + } +} diff --git a/sgf/src/types.rs b/sgf/src/types.rs index 0362733..05af7f8 100644 --- a/sgf/src/types.rs +++ b/sgf/src/types.rs @@ -106,4 +106,5 @@ pub enum Win { Resignation, Forfeit, Time, + Unknown, }