From 96c6f2dfbf21f1c523cce9c2143a522e3cf0e739 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Thu, 20 Jul 2023 23:22:00 -0400 Subject: [PATCH] Start parsing game information --- go-sgf/src/go.rs | 142 +++++++++++++++++++++++++++++++++++++++------ go-sgf/src/tree.rs | 15 +---- 2 files changed, 126 insertions(+), 31 deletions(-) diff --git a/go-sgf/src/go.rs b/go-sgf/src/go.rs index ee7b810..a4f87f9 100644 --- a/go-sgf/src/go.rs +++ b/go-sgf/src/go.rs @@ -1,3 +1,4 @@ +// https://red-bean.com/sgf/ // https://red-bean.com/sgf/user_guide/index.html // https://red-bean.com/sgf/sgf4.html @@ -94,16 +95,38 @@ impl<'a> From for Error<'a> { } } -#[derive(Debug)] -pub struct GameTree { - pub file_format: i8, - pub app: Option, - pub game_type: GameType, - pub board_size: Size, - - pub text: String, +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Rank { + Kyu(u8), + Dan(u8), + Pro(u8), } +impl TryFrom<&str> for Rank { + type Error = String; + fn try_from(r: &str) -> Result { + let parts = r.split(" ").map(|s| s.to_owned()).collect::>(); + let cnt = parts[0].parse::().map_err(|err| format!("{:?}", err))?; + match parts[1].to_ascii_lowercase().as_str() { + "kyu" => Ok(Rank::Kyu(cnt)), + "dan" => Ok(Rank::Dan(cnt)), + "pro" => Ok(Rank::Pro(cnt)), + _ => Err("unparsable".to_owned()), + } + } +} + +#[derive(Clone, Debug)] +pub struct GameTree { + pub file_format: i8, + pub app_name: Option, + pub game_type: GameType, + pub board_size: Size, + pub info: GameInfo, + // pub text: String, +} + +#[derive(Clone, Debug, Default)] pub struct GameInfo { pub annotator: Option, pub copyright: Option, @@ -118,16 +141,17 @@ pub struct GameInfo { pub source: Option, pub time_limits: Option, pub game_keeper: Option, + pub komi: Option, pub game_name: Option, pub game_comments: Option, pub black_player: Option, - pub black_rank: Option, + pub black_rank: Option, pub black_team: Option, pub white_player: Option, - pub white_rank: Option, + pub white_rank: Option, pub white_team: Option, pub opening: Option, @@ -135,6 +159,7 @@ pub struct GameInfo { pub result: Option, } +#[derive(Clone, Debug, PartialEq)] pub enum GameResult { Annulled, Draw, @@ -142,14 +167,43 @@ pub enum GameResult { White(Win), } +impl TryFrom<&str> for GameResult { + type Error = String; + fn try_from(s: &str) -> Result { + println!("Result try_from: {:?}", s); + if s == "0" { + Ok(GameResult::Draw) + } else if s == "Void" { + Ok(GameResult::Annulled) + } else { + let parts = s.split("+").collect::>(); + let res = match parts[0] { + "B" => GameResult::Black, + "W" => GameResult::White, + _ => panic!("unknown result format"), + }; + match parts[1] { + "R" | "Resign" => Ok(res(Win::Resignation)), + "T" | "Time" => Ok(res(Win::Time)), + "F" | "Forfeit" => Ok(res(Win::Forfeit)), + _ => { + let score = parts[1].parse::().unwrap(); + Ok(res(Win::Score(score))) + } + } + } + } +} + +#[derive(Clone, Debug, PartialEq)] pub enum Win { - Score(i32), + Score(f32), Resignation, Forfeit, Time, } -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub enum GameType { Go, Unsupported, @@ -185,7 +239,7 @@ pub fn parse_sgf<'a>(input: &'a str) -> Result, Error<'a>> { Some(prop) => prop.values[0].parse::().unwrap(), None => 4, }; - let app = tree.sequence[0] + let app_name = tree.sequence[0] .find_prop("AP") .map(|prop| prop.values[0].clone()); let board_size = match tree.sequence[0].find_prop("SZ") { @@ -195,14 +249,38 @@ pub fn parse_sgf<'a>(input: &'a str) -> Result, Error<'a>> { height: 19, }, }; + let mut info = GameInfo::default(); + info.black_player = tree.sequence[0] + .find_prop("PB") + .map(|prop| prop.values.join(", ")); + + info.black_rank = tree.sequence[0] + .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_rank = tree.sequence[0] + .find_prop("WR") + .and_then(|prop| Rank::try_from(prop.values[0].as_str()).ok()); + + info.result = tree.sequence[0] + .find_prop("RE") + .and_then(|prop| GameResult::try_from(prop.values[0].as_str()).ok()); + + info.time_limits = tree.sequence[0] + .find_prop("TM") + .and_then(|prop| prop.values[0].parse::().ok()) + .and_then(|seconds| Some(std::time::Duration::from_secs(seconds))); Ok(GameTree { file_format, - - app, + app_name, game_type: GameType::Go, board_size, - text: input.to_owned(), + info, }) }) .collect::, Error>>()?; @@ -239,7 +317,7 @@ mod tests { assert_eq!(trees.len(), 1); let tree = &trees[0]; assert_eq!(tree.file_format, 4); - assert_eq!(tree.app, None); + assert_eq!(tree.app_name, None); assert_eq!(tree.game_type, GameType::Go); assert_eq!( tree.board_size, @@ -255,7 +333,7 @@ mod tests { assert_eq!(trees.len(), 1); let tree = &trees[0]; assert_eq!(tree.file_format, 4); - assert_eq!(tree.app, None); + assert_eq!(tree.app_name, None); assert_eq!(tree.game_type, GameType::Go); assert_eq!( tree.board_size, @@ -266,4 +344,32 @@ mod tests { ); }); } + + #[test] + fn it_parses_game_info() { + with_file(std::path::Path::new("test_data/print1.sgf"), |trees| { + assert_eq!(trees.len(), 1); + let tree = &trees[0]; + assert_eq!(tree.info.black_player, Some("Takemiya Masaki".to_owned())); + assert_eq!(tree.info.black_rank, Some(Rank::Dan(9))); + assert_eq!(tree.info.white_player, Some("Cho Chikun".to_owned())); + assert_eq!(tree.info.white_rank, Some(Rank::Dan(9))); + assert_eq!(tree.info.result, Some(GameResult::White(Win::Resignation))); + assert_eq!( + tree.info.time_limits, + Some(std::time::Duration::from_secs(28800)) + ); + assert_eq!( + tree.info.date_time, + vec![ + chrono::NaiveDate::from_ymd_opt(1996, 10, 18).unwrap(), + chrono::NaiveDate::from_ymd_opt(1996, 10, 19).unwrap(), + ] + ); + assert_eq!(tree.info.event, Some("21st Meijin".to_owned())); + assert_eq!(tree.info.event, Some("2 (final)".to_owned())); + assert_eq!(tree.info.source, Some("Go World #78".to_owned())); + assert_eq!(tree.info.game_keeper, Some("Arno Hollosi".to_owned())); + }); + } } diff --git a/go-sgf/src/tree.rs b/go-sgf/src/tree.rs index deb050b..7c2681d 100644 --- a/go-sgf/src/tree.rs +++ b/go-sgf/src/tree.rs @@ -1,5 +1,3 @@ -use std::num::ParseIntError; - use nom::{ branch::alt, bytes::complete::{escaped_transform, tag}, @@ -9,6 +7,7 @@ use nom::{ sequence::delimited, IResult, }; +use std::num::ParseIntError; #[derive(Debug)] pub enum ParseSizeError { @@ -22,7 +21,7 @@ impl From for ParseSizeError { } } -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct Size { pub width: i32, pub height: i32, @@ -120,7 +119,6 @@ pub fn parse_collection<'a, E: nom::error::ParseError<&'a str>>( // 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> { - println!("::: parse_tree: {}", input); let (input, _) = multispace0(input)?; delimited(tag("("), parse_sequence, tag(")"))(input) } @@ -128,7 +126,6 @@ fn parse_tree<'a, E: nom::error::ParseError<&'a str>>(input: &'a str) -> IResult fn parse_sequence<'a, E: nom::error::ParseError<&'a str>>( input: &'a str, ) -> IResult<&'a str, Tree, E> { - println!("::: parse_sequence: {}", input); let (input, _) = multispace0(input)?; let (input, nodes) = many1(parse_node)(input)?; let (input, _) = multispace0(input)?; @@ -145,7 +142,6 @@ fn parse_sequence<'a, E: nom::error::ParseError<&'a str>>( } fn parse_node<'a, E: nom::error::ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, Node, E> { - println!("::: parse_node: {}", input); let (input, _) = multispace0(input)?; let (input, _) = tag(";")(input)?; let (input, properties) = many1(parse_property)(input)?; @@ -155,7 +151,6 @@ fn parse_node<'a, E: nom::error::ParseError<&'a str>>(input: &'a str) -> IResult fn parse_property<'a, E: nom::error::ParseError<&'a str>>( input: &'a str, ) -> IResult<&'a str, Property, E> { - println!(":: parse_property: {}", input); let (input, _) = multispace0(input)?; let (input, ident) = alpha1(input)?; let (input, values) = many1(parse_propval)(input)?; @@ -178,14 +173,8 @@ fn parse_propval<'a, E: nom::error::ParseError<&'a str>>( input: &'a str, ) -> IResult<&'a str, String, E> { let (input, _) = multispace0(input)?; - println!("- {}", input); - let (input, _) = tag("[")(input)?; - println!("-- {}", input); - let (input, value) = parse_propval_text(input)?; - println!("--- {}", input); - let (input, _) = tag("]")(input)?; Ok((input, value.unwrap_or(String::new())))