diff --git a/Cargo.lock b/Cargo.lock index 5210a28..ac6449e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4283,7 +4283,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" dependencies = [ "getrandom", - "serde", + "serde 1.0.188", ] [[package]] diff --git a/sgf/src/game.rs b/sgf/src/game.rs index c1f9d42..8db25d0 100644 --- a/sgf/src/game.rs +++ b/sgf/src/game.rs @@ -1,6 +1,5 @@ -use std::collections::HashSet; - -use crate::{Color, Position}; +use crate::{tree, Color, Position}; +use std::{collections::HashSet, time::Duration}; use uuid::Uuid; #[derive(Clone, Debug, PartialEq)] @@ -13,16 +12,82 @@ pub enum SetupError { ConflictingPosition, } +#[derive(Clone, Debug, PartialEq)] +pub enum ConversionError { + IncompatibleNodeType, + InvalidPositionSyntax, +} + +#[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(f64), - GoodForBlack(f64), - GoodForWhite(f64), - Hotspot(f64), + EvenResult(Double), + GoodForBlack(Double), + GoodForWhite(Double), + Hotspot(Double), NodeName(String), - Unclear(f64), + 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)] @@ -52,12 +117,31 @@ pub enum MoveProperty { 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(_) => {} + 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 contains_move_annotation_property(&self.0) { + if self + .0 + .iter() + .filter(|prop| match prop { + MoveProperty::MoveAnnotationProperty(_) => true, + _ => false, + }) + .count() + > 0 + { return Err(PropertyError::ConflictingProperty); } self.0.push(prop); @@ -70,9 +154,6 @@ impl MoveProperties { #[derive(Clone, Debug, PartialEq)] pub enum SetupProperty { NodeProperty(NodeProperty), - AddBlack(Vec), - AddWhite(Vec), - ClearPoints(Vec), PlayerTurn(Color), } @@ -81,9 +162,13 @@ pub struct SetupProperties(Vec); impl SetupProperties { pub fn add_property(&mut self, prop: SetupProperty) { - unimplemented!() + match prop { + SetupProperty::NodeProperty(_) => {} + SetupProperty::PlayerTurn(_) => self.0.push(prop), + } } } +*/ #[derive(Clone, Debug, PartialEq)] pub enum GameNode { @@ -142,6 +227,13 @@ impl Node for GameNode { } } +impl TryFrom<&tree::Node> for GameNode { + type Error = ConversionError; + fn try_from(n: &tree::Node) -> Result { + unimplemented!() + } +} + // Root node pub struct GameTree { children: Vec, @@ -167,27 +259,37 @@ impl Node for GameTree { #[derive(Clone, Debug, PartialEq)] pub struct MoveNode { id: Uuid, - color: Color, position: Position, - properties: MoveProperties, children: Vec, + + time_left: Option, + moves_left: Option, + name: Option, + evaluation: Option, + value: Option, + comments: Vec, + annotation: Option, + unknown_props: Vec<(String, String)>, } impl MoveNode { pub fn new(color: Color, position: Position) -> Self { Self { id: Uuid::new_v4(), - color, position, - properties: MoveProperties::default(), children: Vec::new(), - } - } - fn add_property(&mut self, prop: MoveProperty) -> Result<(), PropertyError> { - self.properties.add_property(prop) + time_left: None, + moves_left: None, + name: None, + evaluation: None, + value: None, + comments: vec![], + annotation: None, + unknown_props: vec![], + } } } @@ -202,6 +304,31 @@ impl Node for MoveNode { } } +impl TryFrom<&tree::Node> for MoveNode { + type Error = ConversionError; + + fn try_from(n: &tree::Node) -> Result { + let move_ = match (n.find_prop("W"), n.find_prop("B")) { + (Some(white_move), _) => Some((Color::White, Position{ row: white_move + (None, Some(black_move)) => unimplemented!(), + (None, None) => None, + }; + + match move_ { + Some((color, position)) => unimplemented!(), + None => Err(ConversionError::IncompatibleNodeType), + } + } +} + +fn parse_position(s: &str) -> Result { + if s.len() == 2 { + Ok(Position{ row: s[0], column: s[1] }) + } else { + Err(ConversionError::InvalidPositionSyntax) + } +} + #[derive(Clone, Debug, PartialEq)] pub struct SetupNode { id: Uuid, @@ -257,6 +384,7 @@ pub fn path_to_node<'a>(node: &'a GameNode, id: Uuid) -> Vec<&'a GameNode> { #[cfg(test)] mod test { use super::*; + use cool_asserts::assert_matches; #[test] fn it_can_create_an_empty_game_tree() { @@ -291,37 +419,47 @@ mod test { assert_eq!(nodes[1].id(), second_move.id); } - #[test] fn it_can_set_up_a_game() { unimplemented!() } - #[test] fn it_can_load_tree_from_sgf() { unimplemented!() } -} -fn contains_move_annotation_property(lst: &Vec) -> bool { - lst.iter().any(|item| match item { - MoveProperty::MoveAnnotationProperty(_) => true, - _ => false, - }) + #[test] + fn game_node_can_parse_sgf_move_node() { + let n = tree::Node { + properties: vec![ + tree::Property { + ident: "W".to_owned(), + values: vec!["dp".to_owned()], + }, + tree::Property { + ident: "WL".to_owned(), + values: vec!["176.099".to_owned()], + }, + tree::Property { + ident: "C".to_owned(), + values: vec!["Comments in the game".to_owned()], + }, + ], + next: vec![], + }; + assert_matches!(GameNode::try_from(&n), Ok(GameNode::MoveNode(_))); + } } #[cfg(test)] mod root_node_tests { - #[test] fn it_rejects_move_properties() { unimplemented!() } - #[test] fn it_rejects_setup_properties() { unimplemented!() } - #[test] fn it_can_parse_a_root_sgf() { unimplemented!() } @@ -332,48 +470,38 @@ mod move_node_tests { use super::*; use cool_asserts::assert_matches; - #[test] - fn it_rejects_setup_properties() { - unimplemented!() - } - - #[test] - fn it_rejects_root_properties() { - unimplemented!() - } - #[test] fn it_can_parse_an_sgf_move_node() { - unimplemented!() + let n = tree::Node { + properties: vec![ + tree::Property { + ident: "W".to_owned(), + values: vec!["dp".to_owned()], + }, + tree::Property { + ident: "WL".to_owned(), + values: vec!["176.099".to_owned()], + }, + tree::Property { + ident: "C".to_owned(), + values: vec!["Comments in the game".to_owned()], + }, + ], + next: vec![], + }; + assert_matches!(MoveNode::try_from(&n), Ok(node) => { + assert_eq!(node.color, Color::White); + assert_eq!(node.position, Position{ row: 'd', column: 'p' }); + assert_eq!(node.children, vec![]); + assert_eq!(node.time_left, Some(Duration::from_secs(176))); + assert_eq!(node.comments, vec!["Comments in the game".to_owned()]); + }); } #[test] - fn it_rejects_an_sgf_setup_property() { + fn it_rejects_an_sgf_setup_node() { unimplemented!() } - - #[test] - fn it_prevents_multiple_move_annotation_properties() { - let mut node = MoveNode::new( - Color::Black, - Position { - row: 'a', - column: 'b', - }, - ); - assert_matches!( - node.add_property(MoveProperty::MoveAnnotationProperty( - MoveAnnotationProperty::BadMove(5.), - )), - Ok(_) - ); - assert_matches!( - node.add_property(MoveProperty::MoveAnnotationProperty( - MoveAnnotationProperty::Tesuji(5.), - )), - Err(PropertyError::ConflictingProperty) - ); - } } #[cfg(test)] @@ -381,7 +509,6 @@ mod setup_node_tests { use super::*; use cool_asserts::assert_matches; - #[test] fn it_can_parse_an_sgf_setup_node() { unimplemented!() } @@ -438,17 +565,14 @@ mod setup_node_tests { #[cfg(test)] mod path_test { - #[test] fn returns_empty_list_if_no_game_nodes() { unimplemented!() } - #[test] fn returns_empty_list_if_node_not_found() { unimplemented!() } - #[test] fn path_excludes_root_node() { unimplemented!() } 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 91fa828..f9a4aad 100644 --- a/sgf/src/lib.rs +++ b/sgf/src/lib.rs @@ -10,62 +10,8 @@ use tree::parse_collection; use thiserror::Error; -#[derive(Debug)] -pub enum Error { - InvalidField, - InvalidBoardSize, - Incomplete, - InvalidSgf(VerboseNomError), -} - -#[derive(Clone, Debug, PartialEq)] -pub enum Color { - Black, - White, -} - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct Position(String); - -#[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, - }) - } -} +mod types; +pub use types::*; /* pub enum Game { diff --git a/sgf/src/tree.rs b/sgf/src/tree.rs index f9bf3ad..9bd4f19 100644 --- a/sgf/src/tree.rs +++ b/sgf/src/tree.rs @@ -1,4 +1,4 @@ -use crate::{Color, Error, Position}; +use crate::{Color, Error, GameResult}; use nom::{ branch::alt, bytes::complete::{escaped_transform, tag}, @@ -27,6 +27,153 @@ impl From for ParseSizeError { } } +#[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), +} + +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, @@ -51,6 +198,22 @@ impl TryFrom<&str> for Size { } } +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Position(String); + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct PositionList(Vec); + +impl PositionList { + pub fn compressed_list(&self) -> String { + self.0 + .iter() + .map(|v| v.0.clone()) + .collect::>() + .join(":") + } +} + #[derive(Clone, Debug, PartialEq)] pub struct Tree { pub root: Node, @@ -110,21 +273,7 @@ impl Node { // KO // MN -// AB -// AE -// AW -// PL -// DM -// GB -// GW -// HO // N -// UC -// V -// BM -// DO -// IT -// TE // AR // CR // DD @@ -134,33 +283,6 @@ impl Node { // SL // SQ // TR -// AP -// CA -// FF -// GM -// ST -// SZ -// AN -// BR -// BT -// CP -// DT -// EV -// GN -// GC -// ON -// OT -// PB -// PC -// PW -// RE -// RO -// RU -// SO -// TM -// US -// WR -// WT // BL // OB // OW @@ -176,6 +298,129 @@ pub enum Property { // C Comment(Vec), + // BM + BadMove, + + // DO + DoubtfulMove, + + // IT + InterestingMove, + + // TE + Tesuji, + + // AP + Application(String), + + // CA + Charset(String), + + // FF + FileFormat(u8), + + // GM + GameType(GameType), + + // ST + VariationDisplay, + + // SZ + BoardSize(Size), + + // AB + SetupBlackStones(PositionList), + + // AE + ClearStones(PositionList), + + // AW + SetupWhiteStones(PositionList), + + // PL + NextPlayer(Color), + + // DM + EvenResult, + + // GB + GoodForBlack, + + // GW + GoodForWhite, + + // UC + UnclearResult, + + // 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(std::time::Duration), + + // US + User(String), + + // WR + WhiteRank(String), + + // WT + WhiteTeam(String), + Unknown(UnknownProperty), } @@ -196,14 +441,9 @@ pub struct Property { impl ToString for Property { fn to_string(&self) -> String { match self { - Property::Move((color, position)) => format!( - "{}[{}]", - match color { - Color::White => "W", - Color::Black => "B", - }, - position.0 - ), + Property::Move((color, position)) => { + format!("{}[{}]", color.abbreviation(), position.0) + } Property::Comment(values) => format!( "C{}", values @@ -211,6 +451,59 @@ impl ToString for Property { .map(|v| format!("[{}]", v)) .collect::() ), + Property::BadMove => "BM[]".to_owned(), + Property::DoubtfulMove => "DO[]".to_owned(), + Property::InterestingMove => "IT[]".to_owned(), + Property::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::EvenResult => "DM[]".to_owned(), + Property::GoodForBlack => "GB[]".to_owned(), + Property::GoodForWhite => "GW[]".to_owned(), + Property::UnclearResult => "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::Unknown(UnknownProperty { ident, values }) => { let values = values .iter() @@ -282,6 +575,42 @@ fn parse_property<'a, E: nom::error::ParseError<&'a str>>( "W" => Property::Move((Color::White, Position(values[0].clone()))), "B" => Property::Move((Color::Black, Position(values[0].clone()))), "C" => Property::Comment(values), + "BM" => Property::BadMove, + "DO" => Property::DoubtfulMove, + "IT" => Property::InterestingMove, + "TE" => Property::Tesuji, + "AP" => Property::Application(values.join(",")), + "CA" => Property::Charset(values.join(",")), + "FF" => Property::FileFormat(values.join("").parse::().unwrap()), + "GM" => Property::GameType(GameType::from(values.join("").as_ref())), + "ST" => unimplemented!(), + "SZ" => Property::BoardSize(Size::try_from(values.join("").as_ref()).unwrap()), + "DM" => Property::EvenResult, + "GB" => Property::GoodForBlack, + "GW" => Property::GoodForWhite, + "UC" => Property::UnclearResult, + "V" => Property::Value(values.join("").parse::().unwrap()), + "AN" => Property::Annotator(values.join("")), + "BR" => Property::BlackRank(values.join("")), + "BT" => Property::BlackTeam(values.join("")), + "CP" => Property::Copyright(values.join("")), + "DT" => unimplemented!(), + "EV" => Property::EventName(values.join("")), + "GN" => Property::GameName(values.join("")), + "GC" => Property::ExtraGameInformation(values.join("")), + "ON" => Property::GameOpening(values.join("")), + "OT" => Property::Overtime(values.join("")), + "PB" => Property::BlackPlayer(values.join("")), + "PC" => Property::GameLocation(values.join("")), + "PW" => Property::WhitePlayer(values.join("")), + "RE" => Property::Result(GameResult::try_from(values.join("").as_ref()).unwrap()), + "RO" => Property::Round(values.join("")), + "RU" => Property::Ruleset(values.join("")), + "SO" => Property::Source(values.join("")), + "TM" => unimplemented!(), + "US" => Property::User(values.join("")), + "WR" => Property::WhiteRank(values.join("")), + "WT" => Property::WhiteTeam(values.join("")), _ => Property::Unknown(UnknownProperty { ident: ident.to_owned(), values, @@ -470,10 +799,7 @@ mod test { }; let expected = Node { properties: vec![ - Property::Unknown(UnknownProperty { - ident: "FF".to_owned(), - values: vec!["4".to_owned()], - }), + Property::FileFormat(4), Property::Comment(vec!["root".to_owned()]), ], next: vec![a, f], diff --git a/sgf/src/types.rs b/sgf/src/types.rs new file mode 100644 index 0000000..44435f8 --- /dev/null +++ b/sgf/src/types.rs @@ -0,0 +1,109 @@ +use thiserror::Error; + +#[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.clone(), + }) + } +} + +#[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(Clone, Debug, PartialEq)] +pub enum GameResult { + Draw, + Black(Win), + White(Win), + Void, + 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::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())), + }; + 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, +}