From 942e91009e94ac6ed72f3635d5129ff346ae65d1 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Thu, 19 Oct 2023 02:43:08 -0400 Subject: [PATCH 1/2] Disable sgf::go and provide a shim for a game --- kifu/core/src/database.rs | 14 +++-- kifu/core/src/ui/elements/game_preview.rs | 10 ++-- kifu/core/src/ui/home.rs | 2 +- sgf/src/date.rs | 3 ++ sgf/src/go.rs | 44 ---------------- sgf/src/lib.rs | 46 +++-------------- sgf/src/types.rs | 63 +++++++++++++++++++++++ 7 files changed, 84 insertions(+), 98 deletions(-) create mode 100644 sgf/src/types.rs diff --git a/kifu/core/src/database.rs b/kifu/core/src/database.rs index b98b885..1fbb9ca 100644 --- a/kifu/core/src/database.rs +++ b/kifu/core/src/database.rs @@ -1,6 +1,6 @@ use std::{io::Read, path::PathBuf}; -use sgf::{go, parse_sgf, Game}; +use sgf::{parse_sgf, Game}; use thiserror::Error; #[derive(Error, Debug)] @@ -21,12 +21,12 @@ impl From for Error { #[derive(Debug)] pub struct Database { - games: Vec, + games: Vec, } impl Database { pub fn open_path(path: PathBuf) -> Result { - let mut games: Vec = Vec::new(); + let mut games: Vec = Vec::new(); let extension = PathBuf::from("sgf").into_os_string(); @@ -43,10 +43,7 @@ impl Database { match parse_sgf(&buffer) { Ok(sgfs) => { for sgf in sgfs { - match sgf { - Game::Go(game) => games.push(game), - Game::Unsupported(_) => {} - } + games.push(sgf); } } Err(err) => println!("Error parsing {:?}: {:?}", entry.path(), err), @@ -60,7 +57,7 @@ impl Database { Ok(Database { games }) } - pub fn all_games(&self) -> impl Iterator { + pub fn all_games(&self) -> impl Iterator { self.games.iter() } } @@ -78,6 +75,7 @@ mod test { assert_eq!(db.all_games().count(), 0); } + #[ignore] #[test] fn it_reads_five_games_from_database() { let db = diff --git a/kifu/core/src/ui/elements/game_preview.rs b/kifu/core/src/ui/elements/game_preview.rs index fbd00ea..3a59958 100644 --- a/kifu/core/src/ui/elements/game_preview.rs +++ b/kifu/core/src/ui/elements/game_preview.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use sgf::go::{Game, GameResult, Win}; +use sgf::{Game, GameResult, Win}; use typeshare::typeshare; #[derive(Clone, Debug, Deserialize, Serialize)] @@ -23,13 +23,13 @@ impl GamePreviewElement { None => "unknown".to_owned(), }; - let black_player = match game.info.black_rank { - Some(rank) => format!("{} ({})", black_player, rank.to_string()), + let black_player = match &game.info.black_rank { + Some(rank) => format!("{} ({})", black_player, rank), None => black_player, }; - let white_player = match game.info.white_rank { - Some(rank) => format!("{} ({})", white_player, rank.to_string()), + let white_player = match &game.info.white_rank { + Some(rank) => format!("{} ({})", white_player, rank), None => white_player, }; diff --git a/kifu/core/src/ui/home.rs b/kifu/core/src/ui/home.rs index 59a064b..f19f8d4 100644 --- a/kifu/core/src/ui/home.rs +++ b/kifu/core/src/ui/home.rs @@ -1,6 +1,6 @@ use crate::ui::{Action, GamePreviewElement}; use serde::{Deserialize, Serialize}; -use sgf::go::Game; +use sgf::Game; use typeshare::typeshare; fn rank_strings() -> Vec { diff --git a/sgf/src/date.rs b/sgf/src/date.rs index 8de0e1b..f11c63e 100644 --- a/sgf/src/date.rs +++ b/sgf/src/date.rs @@ -4,6 +4,7 @@ use std::{fmt, num::ParseIntError}; use thiserror::Error; use typeshare::typeshare; +#[allow(dead_code)] #[derive(Debug, Error, PartialEq)] pub enum Error { #[error("Failed to parse integer {0}")] @@ -67,12 +68,14 @@ impl TryFrom<&str> for Date { } */ +#[allow(dead_code)] fn parse_numbers(s: &str) -> Result, Error> { s.split('-') .map(|s| s.parse::().map_err(Error::ParseNumberError)) .collect::, Error>>() } +#[allow(dead_code)] pub fn parse_date_field(s: &str) -> Result, Error> { let date_elements = s.split(','); let mut dates = Vec::new(); diff --git a/sgf/src/go.rs b/sgf/src/go.rs index 2c226f4..2a605e0 100644 --- a/sgf/src/go.rs +++ b/sgf/src/go.rs @@ -234,50 +234,6 @@ pub struct GameInfo { pub result: Option, } -#[derive(Clone, Debug, PartialEq)] -pub enum GameResult { - Annulled, - Draw, - Black(Win), - White(Win), - Unknown(String), -} - -impl TryFrom<&str> for GameResult { - type Error = String; - fn try_from(s: &str) -> Result { - if s == "0" { - Ok(GameResult::Draw) - } else if s == "Void" { - Ok(GameResult::Annulled) - } else { - let parts = s.split('+').collect::>(); - let res = match parts[0].to_ascii_lowercase().as_str() { - "b" => GameResult::Black, - "w" => GameResult::White, - _ => return Ok(GameResult::Unknown(parts[0].to_owned())), - }; - match parts[1].to_ascii_lowercase().as_str() { - "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(f32), - Resignation, - Forfeit, - Time, -} - /* enum PropType { Move, diff --git a/sgf/src/lib.rs b/sgf/src/lib.rs index 3c42229..cd3d056 100644 --- a/sgf/src/lib.rs +++ b/sgf/src/lib.rs @@ -1,13 +1,14 @@ mod date; pub use date::Date; -pub mod go; - mod tree; -use tree::parse_collection; +pub use tree::parse_collection; use thiserror::Error; +mod types; +pub use types::*; + #[derive(Debug)] pub enum Error { InvalidField, @@ -56,41 +57,6 @@ impl From> for ParseError { } } -pub enum Game { - Go(go::Game), - Unsupported(tree::Tree), +pub fn parse_sgf(_input: &str) -> Result, Error> { + Ok(vec![Game::default()]) } - -pub fn parse_sgf(input: &str) -> Result, Error> { - let (_, trees) = parse_collection::>(input)?; - Ok(trees - .into_iter() - .map(|t| match t.root.find_prop("GM") { - Some(prop) if prop.values == vec!["1".to_owned()] => { - Game::Go(go::Game::try_from(t).expect("properly structured game tree")) - } - _ => Game::Unsupported(t), - }) - .collect::>()) -} - -/* -impl From<(&str, VerboseErrorKind)> for - -impl From> for ParseError { - fn from(err: nom::error::VerboseError<&str>) -> Self { - Self::NomErrors( - err.errors - .into_iter() - .map(|err| ParseError::from(err)) - .collect(), - ) - /* - Self::NomError(nom::error::Error { - input: err.input.to_owned(), - code: err.code.clone(), - }) - */ - } -} -*/ diff --git a/sgf/src/types.rs b/sgf/src/types.rs new file mode 100644 index 0000000..206719e --- /dev/null +++ b/sgf/src/types.rs @@ -0,0 +1,63 @@ +use crate::date::Date; + +/// This is a placeholder structure. It is not meant to represent a game, only to provide a mock +/// interface for code already written that expects a Game data type to exist. +#[derive(Debug, Default)] +pub struct Game { + pub info: GameInfo, +} + +#[derive(Debug, Default)] +pub struct GameInfo { + pub black_player: Option, + pub black_rank: Option, + pub white_player: Option, + pub white_rank: Option, + pub result: Option, + pub game_name: Option, + pub date: Vec, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum GameResult { + Annulled, + Draw, + Black(Win), + White(Win), + Unknown(String), +} + +impl TryFrom<&str> for GameResult { + type Error = String; + fn try_from(s: &str) -> Result { + if s == "0" { + Ok(GameResult::Draw) + } else if s == "Void" { + Ok(GameResult::Annulled) + } else { + let parts = s.split('+').collect::>(); + let res = match parts[0].to_ascii_lowercase().as_str() { + "b" => GameResult::Black, + "w" => GameResult::White, + _ => return Ok(GameResult::Unknown(parts[0].to_owned())), + }; + match parts[1].to_ascii_lowercase().as_str() { + "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(f32), + Resignation, + Forfeit, + Time, +} -- 2.44.1 From e461cb99089a6b6c3f947bef1a053c8c2d219ade Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Thu, 19 Oct 2023 03:02:37 -0400 Subject: [PATCH 2/2] Import the new level-one parser This is the parser that does a raw parse of the SGF file, interpreting components but not enforcing node types. --- kifu/core/src/ui/elements/game_preview.rs | 3 +- sgf/src/lib.rs | 4 +- sgf/src/parser.rs | 1276 +++++++++++++++++++++ sgf/src/tree.rs | 547 --------- sgf/src/types.rs | 118 +- 5 files changed, 1395 insertions(+), 553 deletions(-) create mode 100644 sgf/src/parser.rs delete mode 100644 sgf/src/tree.rs diff --git a/kifu/core/src/ui/elements/game_preview.rs b/kifu/core/src/ui/elements/game_preview.rs index 3a59958..92e5120 100644 --- a/kifu/core/src/ui/elements/game_preview.rs +++ b/kifu/core/src/ui/elements/game_preview.rs @@ -43,10 +43,11 @@ impl GamePreviewElement { Win::Time => "Timeout".to_owned(), Win::Forfeit => "Forfeit".to_owned(), Win::Score(score) => format!("{:.1}", score), + Win::Unknown => "Unknown".to_owned(), }; let result = match game.info.result { - Some(GameResult::Annulled) => "Annulled".to_owned(), + Some(GameResult::Void) => "Annulled".to_owned(), Some(GameResult::Draw) => "Draw".to_owned(), Some(GameResult::Black(ref win)) => format!("Black by {}", format_win(win)), Some(GameResult::White(ref win)) => format!("White by {}", format_win(win)), diff --git a/sgf/src/lib.rs b/sgf/src/lib.rs index cd3d056..bf47349 100644 --- a/sgf/src/lib.rs +++ b/sgf/src/lib.rs @@ -1,8 +1,8 @@ mod date; pub use date::Date; -mod tree; -pub use tree::parse_collection; +mod parser; +pub use parser::parse_collection; use thiserror::Error; diff --git a/sgf/src/parser.rs b/sgf/src/parser.rs new file mode 100644 index 0000000..ce128a1 --- /dev/null +++ b/sgf/src/parser.rs @@ -0,0 +1,1276 @@ +use crate::{Color, Date, Error, GameResult, GameType, Win}; +use chrono::Datelike; +use nom::{ + branch::alt, + bytes::complete::{escaped_transform, tag, take_until, take_until1}, + character::complete::{alpha1, digit1, multispace0, multispace1, none_of}, + combinator::{opt, value}, + error::ParseError, + multi::{many0, many1, separated_list1}, + IResult, Parser, +}; +use std::{num::ParseIntError, time::Duration}; + +impl From for Error { + fn from(_: ParseSizeError) -> Self { + Self::InvalidBoardSize + } +} + +#[derive(Debug)] +pub enum ParseSizeError { + ParseIntError(ParseIntError), + InsufficientArguments, +} + +impl From for ParseSizeError { + fn from(e: ParseIntError) -> Self { + Self::ParseIntError(e) + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Annotation { + BadMove, + DoubtfulMove, + InterestingMove, + Tesuji, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Evaluation { + Even, + GoodForBlack, + GoodForWhite, + Unclear, +} + +impl From<&str> for GameType { + fn from(s: &str) -> Self { + match s { + "1" => Self::Go, + "2" => Self::Othello, + "3" => Self::Chess, + "4" => Self::GomokuRenju, + "5" => Self::NineMensMorris, + "6" => Self::Backgammon, + "7" => Self::ChineseChess, + "8" => Self::Shogi, + "9" => Self::LinesOfAction, + "10" => Self::Ataxx, + "11" => Self::Hex, + "12" => Self::Jungle, + "13" => Self::Neutron, + "14" => Self::PhilosophersFootball, + "15" => Self::Quadrature, + "16" => Self::Trax, + "17" => Self::Tantrix, + "18" => Self::Amazons, + "19" => Self::Octi, + "20" => Self::Gess, + "21" => Self::Twixt, + "22" => Self::Zertz, + "23" => Self::Plateau, + "24" => Self::Yinsh, + "25" => Self::Punct, + "26" => Self::Gobblet, + "27" => Self::Hive, + "28" => Self::Exxit, + "29" => Self::Hnefatal, + "30" => Self::Kuba, + "31" => Self::Tripples, + "32" => Self::Chase, + "33" => Self::TumblingDown, + "34" => Self::Sahara, + "35" => Self::Byte, + "36" => Self::Focus, + "37" => Self::Dvonn, + "38" => Self::Tamsk, + "39" => Self::Gipf, + "40" => Self::Kropki, + _ => Self::Other(s.to_owned()), + } + } +} + +impl From<&GameType> for String { + fn from(g: &GameType) -> String { + match g { + GameType::Go => "1".to_owned(), + GameType::Othello => "2".to_owned(), + GameType::Chess => "3".to_owned(), + GameType::GomokuRenju => "4".to_owned(), + GameType::NineMensMorris => "5".to_owned(), + GameType::Backgammon => "6".to_owned(), + GameType::ChineseChess => "7".to_owned(), + GameType::Shogi => "8".to_owned(), + GameType::LinesOfAction => "9".to_owned(), + GameType::Ataxx => "10".to_owned(), + GameType::Hex => "11".to_owned(), + GameType::Jungle => "12".to_owned(), + GameType::Neutron => "13".to_owned(), + GameType::PhilosophersFootball => "14".to_owned(), + GameType::Quadrature => "15".to_owned(), + GameType::Trax => "16".to_owned(), + GameType::Tantrix => "17".to_owned(), + GameType::Amazons => "18".to_owned(), + GameType::Octi => "19".to_owned(), + GameType::Gess => "20".to_owned(), + GameType::Twixt => "21".to_owned(), + GameType::Zertz => "22".to_owned(), + GameType::Plateau => "23".to_owned(), + GameType::Yinsh => "24".to_owned(), + GameType::Punct => "25".to_owned(), + GameType::Gobblet => "26".to_owned(), + GameType::Hive => "27".to_owned(), + GameType::Exxit => "28".to_owned(), + GameType::Hnefatal => "29".to_owned(), + GameType::Kuba => "30".to_owned(), + GameType::Tripples => "31".to_owned(), + GameType::Chase => "32".to_owned(), + GameType::TumblingDown => "33".to_owned(), + GameType::Sahara => "34".to_owned(), + GameType::Byte => "35".to_owned(), + GameType::Focus => "36".to_owned(), + GameType::Dvonn => "37".to_owned(), + GameType::Tamsk => "38".to_owned(), + GameType::Gipf => "39".to_owned(), + GameType::Kropki => "40".to_owned(), + GameType::Other(v) => v.clone(), + } + } +} + +impl ToString for GameType { + fn to_string(&self) -> String { + String::from(self) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Size { + pub width: i32, + pub height: i32, +} + +impl TryFrom<&str> for Size { + type Error = ParseSizeError; + fn try_from(s: &str) -> Result { + let parts = s + .split(':') + .map(|v| v.parse::()) + .collect::, ParseIntError>>()?; + match parts[..] { + [width, height, ..] => Ok(Size { width, height }), + [dim] => Ok(Size { + width: dim, + height: dim, + }), + [] => Err(ParseSizeError::InsufficientArguments), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Position(String); + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct PositionList(pub Vec); + +impl PositionList { + pub fn compressed_list(&self) -> String { + self.0.to_vec().join(":") + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Tree { + pub root: Node, +} + +impl ToString for Tree { + fn to_string(&self) -> String { + format!("({})", self.root.to_string()) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Node { + pub properties: Vec, + pub next: Vec, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum SetupInstr { + Piece((Color, String)), + Clear(String), +} + +impl Node { + pub fn mv(&self) -> Option<(Color, Move)> { + self.find_by(|prop| match prop { + Property::Move(val) => Some(val.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::>(), + ); + } + _ => return None, + } + } + if !setup.is_empty() { + Some(setup) + } else { + None + } + } + + fn find_by(&self, f: F) -> Option + where + F: FnMut(&Property) -> Option, + { + self.properties.iter().filter_map(f).next() + } +} + +impl ToString for Node { + fn to_string(&self) -> String { + let props = self + .properties + .iter() + .map(|prop| prop.to_string()) + .collect::(); + + 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) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum Move { + Move(String), + Pass, +} + +// KO +// MN +// N +// AR +// CR +// DD +// LB +// LN +// MA +// SL +// SQ +// TR +// OB +// OW +// FG +// PM +// VW +#[derive(Clone, Debug, PartialEq)] +pub enum Property { + // B, W + Move((Color, Move)), + + // C + Comment(String), + + // BM, DO, IT, TE + Annotation(Annotation), + + // AP + Application(String), + + // CA + Charset(String), + + // FF + FileFormat(i32), + + // GM + GameType(GameType), + + // ST + VariationDisplay, + + // SZ + BoardSize(Size), + + // AB + SetupBlackStones(PositionList), + + // AE + ClearStones(PositionList), + + // AW + SetupWhiteStones(PositionList), + + // PL + NextPlayer(Color), + + // DM, GB, GW, UC + Evaluation(Evaluation), + + // HO + Hotspot, + + // V + Value(f32), + + // AN + Annotator(String), + + // BR + BlackRank(String), + + // BT + BlackTeam(String), + + // CP + Copyright(String), + + // DT + EventDates(Vec), + + // EV + EventName(String), + + // GN + GameName(String), + + // GC + ExtraGameInformation(String), + + // ON + GameOpening(String), + + // OT + Overtime(String), + + // PB + BlackPlayer(String), + + // PC + GameLocation(String), + + // PW + WhitePlayer(String), + + // RE + Result(GameResult), + + // RO + Round(String), + + // RU + Ruleset(String), + + // SO + Source(String), + + // TM + TimeLimit(Duration), + + // US + User(String), + + // WR + WhiteRank(String), + + // WT + WhiteTeam(String), + + // BL, WL + TimeLeft((Color, Duration)), + + // TW, TB + Territory(Color, Vec), + + Unknown(UnknownProperty), +} + +#[derive(Clone, Debug, PartialEq)] +pub struct UnknownProperty { + pub ident: String, + pub value: String, +} + +impl ToString for Property { + fn to_string(&self) -> String { + match self { + 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()) + } + Property::Comment(value) => format!("C[{}]", value), + 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), + Property::GameType(gt) => format!("GM[{}]", gt.to_string()), + Property::VariationDisplay => unimplemented!(), + Property::BoardSize(Size { width, height }) => { + if width == height { + format!("SZ[{}]", width) + } else { + format!("SZ[{}:{}]", width, height) + } + } + Property::SetupBlackStones(positions) => { + format!("AB[{}]", positions.compressed_list(),) + } + Property::ClearStones(positions) => { + format!("AE[{}]", positions.compressed_list(),) + } + Property::SetupWhiteStones(positions) => { + format!("AW[{}]", positions.compressed_list(),) + } + Property::NextPlayer(color) => format!("PL[{}]", color.abbreviation()), + 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), + Property::BlackRank(value) => format!("BR[{}]", value), + Property::BlackTeam(value) => format!("BT[{}]", value), + Property::Copyright(value) => format!("CP[{}]", value), + Property::EventDates(_) => unimplemented!(), + Property::EventName(value) => format!("EV[{}]", value), + Property::GameName(value) => format!("GN[{}]", value), + Property::ExtraGameInformation(value) => format!("GC[{}]", value), + Property::GameOpening(value) => format!("ON[{}]", value), + Property::Overtime(value) => format!("OT[{}]", value), + Property::BlackPlayer(value) => format!("PB[{}]", value), + Property::GameLocation(value) => format!("PC[{}]", value), + Property::WhitePlayer(value) => format!("PW[{}]", value), + Property::Result(_) => unimplemented!(), + Property::Round(value) => format!("RO[{}]", value), + Property::Ruleset(value) => format!("RU[{}]", value), + Property::Source(value) => format!("SO[{}]", value), + Property::TimeLimit(value) => format!("TM[{}]", value.as_secs()), + Property::User(value) => format!("US[{}]", value), + Property::WhiteRank(value) => format!("WR[{}]", value), + Property::WhiteTeam(value) => format!("WT[{}]", value), + Property::Territory(Color::White, positions) => { + format!( + "TW{}", + positions + .iter() + .map(|Position(p)| format!("[{}]", p)) + .collect::() + ) + } + Property::Territory(Color::Black, positions) => { + format!( + "TB{}", + positions + .iter() + .map(|Position(p)| format!("[{}]", p)) + .collect::() + ) + } + Property::Unknown(UnknownProperty { ident, value }) => { + format!("{}[{}]", ident, value) + } + } + } +} + +pub fn parse_collection<'a, E: nom::error::ParseError<&'a str>>( + input: &'a str, +) -> IResult<&'a str, Vec, E> { + 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, Node, E> { + let (input, _) = multispace0(input)?; + let (input, _) = tag("(")(input)?; + let (input, node) = parse_node(input)?; + let (input, _) = multispace0(input)?; + let (input, _) = tag(")")(input)?; + + 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, _) = opt(tag(";"))(input)?; + let (input, properties) = many1(parse_property)(input)?; + + 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>>( + input: &'a str, +) -> IResult<&'a str, Property, E> { + let (input, _) = multispace0(input)?; + let (input, ident) = alpha1(input)?; + + let (input, prop) = match ident { + "W" => parse_propval(parse_move(Color::White))(input)?, + "B" => parse_propval(parse_move(Color::Black))(input)?, + "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::Annotation(Annotation::BadMove)) + .parse(input)?, + "DO" => discard_propval() + .map(|_| Property::Annotation(Annotation::DoubtfulMove)) + .parse(input)?, + "IT" => discard_propval() + .map(|_| Property::Annotation(Annotation::InterestingMove)) + .parse(input)?, + "TE" => discard_propval() + .map(|_| Property::Annotation(Annotation::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)?, + "GM" => parse_propval(parse_gametype().map(Property::GameType))(input)?, + "ST" => discard_propval() + .map(|_| Property::VariationDisplay) + .parse(input)?, + "SZ" => parse_propval(parse_size().map(Property::BoardSize))(input)?, + "DM" => discard_propval() + .map(|_| Property::Evaluation(Evaluation::Even)) + .parse(input)?, + "GB" => discard_propval() + .map(|_| Property::Evaluation(Evaluation::GoodForBlack)) + .parse(input)?, + "GW" => discard_propval() + .map(|_| Property::Evaluation(Evaluation::GoodForWhite)) + .parse(input)?, + "UC" => discard_propval() + .map(|_| Property::Evaluation(Evaluation::Unclear)) + .parse(input)?, + "V" => unimplemented!(), + "AN" => parse_propval(parse_simple_text().map(Property::Annotator))(input)?, + "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" => 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)?, + "ON" => parse_propval(parse_simple_text().map(Property::GameOpening))(input)?, + "OT" => parse_propval(parse_simple_text().map(Property::Overtime))(input)?, + "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" => 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)?, + "TM" => parse_propval( + parse_real().map(|seconds| Property::TimeLimit(Duration::from_secs(seconds as u64))), + )(input)?, + "US" => parse_propval(parse_simple_text().map(Property::User))(input)?, + "WR" => parse_propval(parse_simple_text().map(Property::WhiteRank))(input)?, + "WT" => parse_propval(parse_simple_text().map(Property::WhiteTeam))(input)?, + "TW" => parse_territory() + .map(|p| Property::Territory(Color::White, p)) + .parse(input)?, + "TB" => parse_territory() + .map(|p| Property::Territory(Color::Black, p)) + .parse(input)?, + _ => parse_propval(parse_simple_text().map(|value| { + Property::Unknown(UnknownProperty { + ident: ident.to_owned(), + value, + }) + }))(input)?, + }; + + Ok((input, prop)) +} + +fn parse_comment<'a, E: nom::error::ParseError<&'a str>>() -> impl Parser<&'a str, Property, E> { + parse_text().map(Property::Comment) +} + +fn parse_move<'a, E: nom::error::ParseError<&'a str>>( + color: Color, +) -> impl Parser<&'a str, Property, E> { + { + let color = color.clone(); + move |input: &'a str| { + 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) + } + } +} + +fn parse_gametype<'a, E: nom::error::ParseError<&'a str>>() -> impl Parser<&'a str, GameType, E> { + |input: &'a str| take_until1("]").map(GameType::from).parse(input) +} + +fn parse_time_left<'a, E: ParseError<&'a str>>( + color: Color, +) -> impl FnMut(&'a str) -> IResult<&'a str, Property, E> { + { + let color = color.clone(); + move |input: &'a str| { + let (input, value) = parse_real().parse(input)?; + Ok(( + input, + Property::TimeLeft((color.clone(), Duration::from_secs(value as u64))), + )) + } + } +} + +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() { + [width] => (width.parse::().unwrap(), width.parse::().unwrap()), + [width, height] => ( + width.parse::().unwrap(), + height.parse::().unwrap(), + ), + _ => (19, 19), + }; + Ok((input, Size { width, height })) + } +} + +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_territory<'a, E: nom::error::ParseError<&'a str>>( +) -> impl Parser<&'a str, Vec, E> { + many1(|input| { + let (input, _) = tag("[")(input)?; + let (input, position) = parse_simple_text().map(Position).parse(input)?; + let (input, _) = tag("]")(input)?; + Ok((input, position)) + }) +} + +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> { + move |input| { + let (input, _) = multispace0(input)?; + let (input, _) = tag("[")(input)?; + let (input, value) = parser.parse(input)?; + let (input, _) = tag("]")(input)?; + + Ok((input, value)) + } +} + +fn discard_propval<'a, E: nom::error::ParseError<&'a str>>() -> impl Parser<&'a str, (), E> { + |input| { + let (input, _) = multispace0(input)?; + let (input, _) = tag("[")(input)?; + let (input, _) = parse_text().parse(input)?; + let (input, _) = tag("]")(input)?; + Ok((input, ())) + } +} + +fn parse_number<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, i32, E> { + |input| { + let (input, sign) = opt(alt((tag("+"), tag("-"))))(input)?; + let (input, value) = digit1(input)?; + let mult = if sign == Some("-") { -1 } else { 1 }; + Ok((input, value.parse::().unwrap() * mult)) + } +} + +fn parse_real<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, f32, E> { + |input| { + let (input, sign) = opt(alt((tag("+"), tag("-"))))(input)?; + let (input, whole_value) = digit1(input)?; + let (input, fractional_value) = opt(|input| { + let (input, _) = tag(".")(input)?; + let (input, fractional_value) = digit1(input)?; + Ok((input, format!(".{}", fractional_value))) + })(input)?; + let value = format!( + "{}{}{}", + sign.unwrap_or("+"), + whole_value, + fractional_value.unwrap_or("".to_owned()) + ) + .parse::() + .unwrap(); + Ok((input, value)) + } +} + +fn parse_simple_text<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, String, E> { + |input| { + let (input, value) = opt(escaped_transform( + none_of("\\]"), + '\\', + alt(( + value("]", tag("]")), + value("\\", tag("\\")), + value("", tag("\n")), + )), + ))(input)?; + Ok((input, value.unwrap_or("".to_owned()))) + } +} + +fn parse_text<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, String, E> { + |input| { + let (input, value) = opt(escaped_transform( + none_of("\\]"), + '\\', + alt(( + value("]", tag("]")), + value("\\", tag("\\")), + value("", tag("\n")), + )), + ))(input)?; + Ok((input, value.unwrap_or("".to_owned()))) + } +} + +enum DateSegment { + One(i32), + Two(i32, i32), + Three(i32, i32, i32), +} + +fn parse_date_field<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, Vec, E> { + |input| { + let (input, first_date) = parse_date().parse(input)?; + let (input, mut more_dates) = many0(parse_date_segment())(input)?; + + let mut date_segments = vec![first_date]; + date_segments.append(&mut more_dates); + + let mut dates = vec![]; + let mut most_recent = None; + + for date_segment in date_segments { + let new_date = match date_segment { + DateSegment::One(v) => match most_recent { + Some(Date::Year(_)) => Date::Year(v), + Some(Date::YearMonth(y, _)) => Date::YearMonth(y, v as u32), + Some(Date::Date(d)) => Date::Date(d.clone().with_day(v as u32).unwrap()), + None => Date::Year(v), + }, + DateSegment::Two(y, m) => Date::YearMonth(y, m as u32), + DateSegment::Three(y, m, d) => { + Date::Date(chrono::NaiveDate::from_ymd_opt(y, m as u32, d as u32).unwrap()) + } + }; + dates.push(new_date.clone()); + most_recent = Some(new_date); + } + + Ok((input, dates)) + } +} + +fn parse_date_segment<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, DateSegment, E> { + |input| { + let (input, _) = tag(",")(input)?; + let (input, element) = parse_date().parse(input)?; + + Ok((input, element)) + } +} + +fn parse_date<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, DateSegment, E> { + alt((parse_three(), parse_two(), parse_one())) +} + +fn parse_one<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, DateSegment, E> { + parse_number().map(DateSegment::One) +} + +fn parse_two<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, DateSegment, E> { + |input| { + let (input, year) = parse_number().parse(input)?; + let (input, _) = tag("-")(input)?; + let (input, month) = parse_number().parse(input)?; + Ok((input, DateSegment::Two(year, month))) + } +} + +fn parse_three<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, DateSegment, E> { + |input| { + let (input, year) = parse_number().parse(input)?; + let (input, _) = tag("-")(input)?; + let (input, month) = parse_number().parse(input)?; + let (input, _) = tag("-")(input)?; + let (input, day) = parse_number().parse(input)?; + Ok((input, DateSegment::Three(year, month, day))) + } +} + +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(WinType::Num), + parse_simple_text().map(WinType::St), + ))(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::*; + + const EXAMPLE: &'static str = "(;FF[4]C[root](;C[a];C[b](;C[c]) +(;C[d];C[e])) +(;C[f](;C[g];C[h];C[i]) +(;C[j])))"; + + #[test] + fn it_can_parse_properties() { + let (_, prop) = parse_property::>("C[a]").unwrap(); + assert_eq!(prop, Property::Comment("a".to_owned())); + } + + #[test] + fn it_can_parse_a_standalone_node() { + let (_, node) = parse_node::>(";B[ab]").unwrap(); + + assert_eq!( + node, + Node { + properties: vec![Property::Move((Color::Black, Move::Move("ab".to_owned())))], + next: vec![] + } + ); + + let (_, node) = + parse_node::>(";B[ab];W[dp];B[pq]C[some comments]") + .unwrap(); + + assert_eq!( + node, + Node { + properties: vec![Property::Move((Color::Black, Move::Move("ab".to_owned())))], + next: vec![Node { + properties: vec![Property::Move((Color::White, Move::Move("dp".to_owned())))], + next: vec![Node { + properties: vec![ + Property::Move((Color::Black, Move::Move("pq".to_owned()))), + Property::Comment("some comments".to_owned()) + ], + next: vec![], + }] + }] + } + ); + } + + #[test] + fn it_can_parse_a_simple_sequence() { + let (_, sequence) = + parse_tree::>("(;B[ab];W[dp];B[pq]C[some comments])") + .unwrap(); + + assert_eq!( + sequence, + Node { + properties: vec![Property::Move((Color::Black, Move::Move("ab".to_owned())))], + next: vec![Node { + properties: vec![Property::Move((Color::White, Move::Move("dp".to_owned())))], + next: vec![Node { + properties: vec![ + Property::Move((Color::Black, Move::Move("pq".to_owned()))), + Property::Comment("some comments".to_owned()) + ], + next: vec![], + }] + }], + }, + ); + } + + #[test] + fn it_can_parse_a_branching_sequence() { + let text = "(;C[a];C[b](;C[c])(;C[d];C[e]))"; + let (_, tree) = parse_tree::>(text).unwrap(); + + let expected = Node { + properties: vec![Property::Comment("a".to_owned())], + next: vec![Node { + properties: vec![Property::Comment("b".to_owned())], + next: vec![ + Node { + properties: vec![Property::Comment("c".to_owned())], + next: vec![], + }, + Node { + properties: vec![Property::Comment("d".to_owned())], + next: vec![Node { + properties: vec![Property::Comment("e".to_owned())], + next: vec![], + }], + }, + ], + }], + }; + + assert_eq!(tree, expected); + } + + #[test] + fn it_can_parse_example_1() { + let (_, tree) = parse_tree::>(EXAMPLE).unwrap(); + + let j = Node { + properties: vec![Property::Comment("j".to_owned())], + next: vec![], + }; + let i = Node { + properties: vec![Property::Comment("i".to_owned())], + next: vec![], + }; + let h = Node { + properties: vec![Property::Comment("h".to_owned())], + next: vec![i], + }; + let g = Node { + properties: vec![Property::Comment("g".to_owned())], + next: vec![h], + }; + let f = Node { + properties: vec![Property::Comment("f".to_owned())], + next: vec![g, j], + }; + let e = Node { + properties: vec![Property::Comment("e".to_owned())], + next: vec![], + }; + let d = Node { + properties: vec![Property::Comment("d".to_owned())], + next: vec![e], + }; + let c = Node { + properties: vec![Property::Comment("c".to_owned())], + next: vec![], + }; + let b = Node { + properties: vec![Property::Comment("b".to_owned())], + next: vec![c, d], + }; + let a = Node { + properties: vec![Property::Comment("a".to_owned())], + next: vec![b], + }; + let expected = Node { + properties: vec![ + Property::FileFormat(4), + Property::Comment("root".to_owned()), + ], + next: vec![a, f], + }; + + 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, Tree { root: tree2 }); + } + + #[test] + fn it_parses_propvals() { + let (_, propval) = parse_propval::>(parse_comment()) + .parse("[]") + .unwrap(); + assert_eq!(propval, Property::Comment("".to_owned())); + + let (_, propval) = parse_propval::>(parse_comment()) + .parse("[normal propval]") + .unwrap(); + assert_eq!(propval, Property::Comment("normal propval".to_owned())); + + let (_, propval) = parse_propval::>(parse_comment()) + .parse(r"[need an [escape\] in the propval]") + .unwrap(); + assert_eq!( + propval, + Property::Comment("need an [escape] in the propval".to_owned()) + ); + } + + #[test] + fn it_parses_propvals_with_hard_linebreaks() { + let (_, propval) = parse_text::>() + .parse( + "There are hard linebreaks & soft linebreaks. +Soft linebreaks...", + ) + .unwrap(); + assert_eq!( + propval, + "There are hard linebreaks & soft linebreaks. +Soft linebreaks..." + ); + } + + #[test] + fn it_parses_propvals_with_escaped_closing_brackets() { + let (_, propval) = parse_text::>() + .parse(r"escaped closing \] bracket") + .unwrap(); + assert_eq!(propval, r"escaped closing ] bracket".to_owned()); + } + + #[test] + fn it_parses_propvals_with_soft_linebreaks() { + let (_, propval) = parse_text::>() + .parse( + r"Soft linebreaks are linebreaks preceeded by '\\' like this one >o\ +k<. Hard line breaks are all other linebreaks.", + ) + .unwrap(); + assert_eq!( + propval, + "Soft linebreaks are linebreaks preceeded by '\\' like this one >ok<. Hard line breaks are all other linebreaks." + ); + } + + #[test] + fn it_parses_sgf_with_newline_in_sequence() { + let data = String::from( + "(;FF[4]C[root](;C[a];C[b](;C[c])(;C[d];C[e] +))(;C[f](;C[g];C[h];C[i])(;C[j])))", + ); + parse_tree::>(&data).unwrap(); + } + + #[test] + fn it_parses_sgf_with_newline_between_two_sequence_closings() { + let data = String::from( + "(;FF[4]C[root](;C[a];C[b](;C[c])(;C[d];C[e]) +)(;C[f](;C[g];C[h];C[i])(;C[j])))", + ); + parse_tree::>(&data).unwrap(); + } +} + +#[cfg(test)] +mod date_test { + use super::*; + use chrono::NaiveDate; + use cool_asserts::assert_matches; + + #[test] + fn it_parses_a_year() { + assert_matches!(parse_date_field::>().parse("1996"), Ok((_, date)) => { + assert_eq!(date, vec![Date::Year(1996)]); + }); + } + + #[test] + fn it_parses_a_month() { + assert_matches!( + parse_date_field::>().parse("1996-12"), + Ok((_, date)) => assert_eq!(date, vec![Date::YearMonth(1996, 12)]) + ); + } + + #[test] + fn it_parses_a_date() { + assert_matches!( + parse_date_field::>().parse("1996-12-27"), + Ok((_, date)) => assert_eq!(date, vec![Date::Date( + NaiveDate::from_ymd_opt(1996, 12, 27).unwrap() + )]) + ); + } + + #[test] + fn it_parses_date_continuation() { + assert_matches!( + parse_date_field::>().parse("1996-12-27,28"), + Ok((_, date)) => assert_eq!(date, vec![ + Date::Date(NaiveDate::from_ymd_opt(1996, 12, 27).unwrap()), + Date::Date(NaiveDate::from_ymd_opt(1996, 12, 28).unwrap()) + ]) + ); + } + + #[test] + fn it_parses_date_crossing_year_boundary() { + assert_matches!( + parse_date_field::>().parse("1996-12-27,28,1997-01-03,04"), + Ok((_, date)) => assert_eq!(date, vec![ + Date::Date(NaiveDate::from_ymd_opt(1996, 12, 27).unwrap()), + Date::Date(NaiveDate::from_ymd_opt(1996, 12, 28).unwrap()), + Date::Date(NaiveDate::from_ymd_opt(1997, 1, 3).unwrap()), + Date::Date(NaiveDate::from_ymd_opt(1997, 1, 4).unwrap()), + ]) + ); + } +} + +#[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/tree.rs b/sgf/src/tree.rs deleted file mode 100644 index 167353d..0000000 --- a/sgf/src/tree.rs +++ /dev/null @@ -1,547 +0,0 @@ -use crate::Error; -use nom::{ - branch::alt, - bytes::complete::{escaped_transform, tag}, - character::complete::{alpha1, multispace0, multispace1, none_of}, - combinator::{opt, value}, - multi::{many0, many1, separated_list1}, - IResult, -}; -use std::num::ParseIntError; - -impl From for Error { - fn from(_: ParseSizeError) -> Self { - Self::InvalidBoardSize - } -} - -#[derive(Debug)] -pub enum ParseSizeError { - ParseIntError(ParseIntError), - InsufficientArguments, -} - -impl From for ParseSizeError { - fn from(e: ParseIntError) -> Self { - Self::ParseIntError(e) - } -} - -#[derive(Clone, Debug, PartialEq)] -pub struct Size { - pub width: i32, - pub height: i32, -} - -impl TryFrom<&str> for Size { - type Error = ParseSizeError; - fn try_from(s: &str) -> Result { - let parts = s - .split(':') - .map(|v| v.parse::()) - .collect::, ParseIntError>>()?; - match parts[..] { - [width, height, ..] => Ok(Size { width, height }), - [dim] => Ok(Size { - width: dim, - height: dim, - }), - [] => Err(ParseSizeError::InsufficientArguments), - } - } -} - -#[derive(Clone, Debug, PartialEq)] -pub struct Tree { - pub root: Node, -} - -impl ToString for Tree { - fn to_string(&self) -> String { - format!("({})", self.root.to_string()) - } -} - -#[derive(Clone, Debug, PartialEq)] -pub struct Node { - pub properties: Vec, - pub next: Vec, -} - -impl ToString for Node { - fn to_string(&self) -> String { - let props = self - .properties - .iter() - .map(|prop| prop.to_string()) - .collect::(); - - 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) - } -} - -impl Node { - pub fn find_prop(&self, ident: &str) -> Option { - self.properties - .iter() - .find(|prop| prop.ident == ident) - .cloned() - } - - pub fn next(&self) -> Option<&Node> { - self.next.get(0) - } -} - -#[derive(Clone, Debug, PartialEq)] -pub struct Property { - pub ident: String, - pub values: Vec, -} - -impl ToString for Property { - fn to_string(&self) -> String { - let values = self - .values - .iter() - .map(|val| format!("[{}]", val)) - .collect::(); - format!("{}{}", self.ident, values) - } -} - -pub fn parse_collection<'a, E: nom::error::ParseError<&'a str>>( - input: &'a str, -) -> IResult<&'a str, Vec, E> { - 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, Node, E> { - let (input, _) = multispace0(input)?; - let (input, _) = tag("(")(input)?; - let (input, node) = parse_node(input)?; - let (input, _) = multispace0(input)?; - let (input, _) = tag(")")(input)?; - - 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, _) = opt(tag(";"))(input)?; - let (input, properties) = many1(parse_property)(input)?; - - 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>>( - input: &'a str, -) -> IResult<&'a str, Property, E> { - let (input, _) = multispace0(input)?; - let (input, ident) = alpha1(input)?; - let (input, values) = many1(parse_propval)(input)?; - let (input, _) = multispace0(input)?; - - let values = values - .into_iter() - .map(|v| v.to_owned()) - .collect::>(); - Ok(( - input, - Property { - ident: ident.to_owned(), - values, - }, - )) -} - -fn parse_propval<'a, E: nom::error::ParseError<&'a str>>( - input: &'a str, -) -> IResult<&'a str, String, E> { - let (input, _) = multispace0(input)?; - let (input, _) = tag("[")(input)?; - let (input, value) = parse_propval_text(input)?; - let (input, _) = tag("]")(input)?; - - Ok((input, value.unwrap_or(String::new()))) -} - -fn parse_propval_text<'a, E: nom::error::ParseError<&'a str>>( - input: &'a str, -) -> IResult<&'a str, Option, E> { - let (input, value) = opt(escaped_transform( - none_of("\\]"), - '\\', - alt(( - value("]", tag("]")), - value("\\", tag("\\")), - value("", tag("\n")), - )), - ))(input)?; - Ok((input, value.map(|v| v.to_owned()))) -} - -#[cfg(test)] -mod test { - use super::*; - - const EXAMPLE: &'static str = "(;FF[4]C[root](;C[a];C[b](;C[c]) -(;C[d];C[e])) -(;C[f](;C[g];C[h];C[i]) -(;C[j])))"; - - #[test] - fn it_can_parse_properties() { - let (_, prop) = parse_property::>("C[a]").unwrap(); - assert_eq!( - prop, - Property { - ident: "C".to_owned(), - values: vec!["a".to_owned()] - } - ); - - let (_, prop) = parse_property::>("C[a][b][c]").unwrap(); - assert_eq!( - prop, - Property { - ident: "C".to_owned(), - values: vec!["a".to_owned(), "b".to_owned(), "c".to_owned()] - } - ); - } - - #[test] - fn it_can_parse_a_standalone_node() { - let (_, node) = parse_node::>(";B[ab]").unwrap(); - - assert_eq!( - node, - Node { - properties: vec![Property { - ident: "B".to_owned(), - values: vec!["ab".to_owned()] - }], - next: vec![] - } - ); - - let (_, node) = - parse_node::>(";B[ab];W[dp];B[pq]C[some comments]") - .unwrap(); - - assert_eq!( - 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(), - values: vec!["pq".to_owned()] - }, - Property { - ident: "C".to_owned(), - values: vec!["some comments".to_owned()] - } - ], - next: vec![], - }] - }] - } - ); - } - - #[test] - fn it_can_parse_a_simple_sequence() { - let (_, sequence) = - parse_tree::>("(;B[ab];W[dp];B[pq]C[some comments])") - .unwrap(); - - assert_eq!( - sequence, - 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(), - values: vec!["pq".to_owned()] - }, - Property { - ident: "C".to_owned(), - values: vec!["some comments".to_owned()] - } - ], - next: vec![], - }] - }], - }, - ); - } - - #[test] - fn it_can_parse_a_branching_sequence() { - let text = "(;C[a];C[b](;C[c])(;C[d];C[e]))"; - let (_, tree) = parse_tree::>(text).unwrap(); - - 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()], - }], - 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![], - }], - }, - ], - }], - }; - - assert_eq!(tree, expected); - } - - #[test] - fn it_can_parse_example_1() { - let (_, tree) = parse_tree::>(EXAMPLE).unwrap(); - - 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!(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, Tree { root: tree2 }); - } - - #[test] - fn it_parses_propvals() { - let (_, propval) = parse_propval::>("[]").unwrap(); - assert_eq!(propval, "".to_owned()); - - let (_, propval) = - parse_propval::>("[normal propval]").unwrap(); - assert_eq!(propval, "normal propval".to_owned()); - - let (_, propval) = - parse_propval::>(r"[need an [escape\] in the propval]") - .unwrap(); - assert_eq!(propval, "need an [escape] in the propval".to_owned()); - } - - #[test] - fn it_parses_propvals_with_hard_linebreaks() { - let (_, propval) = parse_propval_text::>( - "There are hard linebreaks & soft linebreaks. -Soft linebreaks...", - ) - .unwrap(); - assert_eq!( - propval, - Some( - "There are hard linebreaks & soft linebreaks. -Soft linebreaks..." - .to_owned() - ) - ); - } - - #[test] - fn it_parses_propvals_with_escaped_closing_brackets() { - let (_, propval) = - parse_propval_text::>(r"escaped closing \] bracket") - .unwrap(); - assert_eq!( - propval, - Some(r"escaped closing ] bracket".to_owned()).to_owned() - ); - } - - #[test] - fn it_parses_propvals_with_soft_linebreaks() { - let (_, propval) = parse_propval_text::>( - r"Soft linebreaks are linebreaks preceeded by '\\' like this one >o\ -k<. Hard line breaks are all other linebreaks.", - ) - .unwrap(); - assert_eq!( - propval, - Some("Soft linebreaks are linebreaks preceeded by '\\' like this one >ok<. Hard line breaks are all other linebreaks.".to_owned()) - .to_owned() - ); - } - - #[test] - fn it_parses_sgf_with_newline_in_sequence() { - let data = String::from( - "(;FF[4]C[root](;C[a];C[b](;C[c])(;C[d];C[e] -))(;C[f](;C[g];C[h];C[i])(;C[j])))", - ); - parse_tree::>(&data).unwrap(); - } - - #[test] - fn it_parses_sgf_with_newline_between_two_sequence_closings() { - let data = String::from( - "(;FF[4]C[root](;C[a];C[b](;C[c])(;C[d];C[e]) -)(;C[f](;C[g];C[h];C[i])(;C[j])))", - ); - parse_tree::>(&data).unwrap(); - } -} diff --git a/sgf/src/types.rs b/sgf/src/types.rs index 206719e..605f12a 100644 --- a/sgf/src/types.rs +++ b/sgf/src/types.rs @@ -1,5 +1,7 @@ use crate::date::Date; +use thiserror::Error; + /// This is a placeholder structure. It is not meant to represent a game, only to provide a mock /// interface for code already written that expects a Game data type to exist. #[derive(Debug, Default)] @@ -19,11 +21,120 @@ pub struct GameInfo { } #[derive(Clone, Debug, PartialEq)] +pub enum GameType { + Go, + Othello, + Chess, + GomokuRenju, + NineMensMorris, + Backgammon, + ChineseChess, + Shogi, + LinesOfAction, + Ataxx, + Hex, + Jungle, + Neutron, + PhilosophersFootball, + Quadrature, + Trax, + Tantrix, + Amazons, + Octi, + Gess, + Twixt, + Zertz, + Plateau, + Yinsh, + Punct, + Gobblet, + Hive, + Exxit, + Hnefatal, + Kuba, + Tripples, + Chase, + TumblingDown, + Sahara, + Byte, + Focus, + Dvonn, + Tamsk, + Gipf, + Kropki, + Other(String), +} + +#[derive(Debug)] +pub enum Error { + // InvalidField, + // InvalidBoardSize, + Incomplete, + InvalidSgf(VerboseNomError), +} + +#[derive(Debug)] +pub struct VerboseNomError(nom::error::VerboseError); + +impl From> for VerboseNomError { + fn from(err: nom::error::VerboseError<&str>) -> Self { + VerboseNomError(nom::error::VerboseError { + errors: err + .errors + .into_iter() + .map(|err| (err.0.to_owned(), err.1)) + .collect(), + }) + } +} + +impl From>> for Error { + fn from(err: nom::Err>) -> Self { + match err { + nom::Err::Incomplete(_) => Error::Incomplete, + nom::Err::Error(e) => Error::InvalidSgf(VerboseNomError::from(e)), + nom::Err::Failure(e) => Error::InvalidSgf(VerboseNomError::from(e)), + } + } +} + +#[derive(Debug, PartialEq, Error)] +pub enum ParseError { + #[error("An unknown error was found")] + NomError(nom::error::Error), +} + +impl From> for ParseError { + fn from(err: nom::error::Error<&str>) -> Self { + Self::NomError(nom::error::Error { + input: err.input.to_owned(), + code: err.code, + }) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum Color { + Black, + White, +} + +impl Color { + pub fn abbreviation(&self) -> String { + match self { + Color::White => "W", + Color::Black => "B", + } + .to_owned() + } +} + +#[derive(Debug, Clone, PartialEq)] pub enum GameResult { - Annulled, Draw, Black(Win), White(Win), + Void, Unknown(String), } @@ -33,13 +144,13 @@ impl TryFrom<&str> for GameResult { if s == "0" { Ok(GameResult::Draw) } else if s == "Void" { - Ok(GameResult::Annulled) + Ok(GameResult::Void) } else { let parts = s.split('+').collect::>(); let res = match parts[0].to_ascii_lowercase().as_str() { "b" => GameResult::Black, "w" => GameResult::White, - _ => return Ok(GameResult::Unknown(parts[0].to_owned())), + res => return Ok(GameResult::Unknown(res.to_owned())), }; match parts[1].to_ascii_lowercase().as_str() { "r" | "resign" => Ok(res(Win::Resignation)), @@ -60,4 +171,5 @@ pub enum Win { Resignation, Forfeit, Time, + Unknown, } -- 2.44.1