From 834dc4ad66a5b6126df7be50291b846f977bdc45 Mon Sep 17 00:00:00 2001 From: Samuel Batissou Tiburcio Date: Tue, 10 Sep 2019 18:05:10 +0200 Subject: [PATCH] Support newlines and passing. (#5) * Support newline in SGF file * Support for passing, with rewrite of `SgfToken::Move` --- .gitignore | 1 + Cargo.toml | 2 +- sgf.pest | 3 ++ src/lib.rs | 2 +- src/node.rs | 4 +- src/parser.rs | 3 ++ src/token.rs | 78 +++++++++++++++++++++-------------- tests/model.rs | 19 +++++---- tests/parse.rs | 57 ++++++++++++------------- tests/sgf/ShusakuvsInseki.sgf | 47 +++++++++++++++++++++ tests/sgf_file.rs | 7 ++++ tests/token.rs | 10 ++--- tests/tree.rs | 17 ++++---- 13 files changed, 165 insertions(+), 85 deletions(-) create mode 100644 tests/sgf/ShusakuvsInseki.sgf create mode 100644 tests/sgf_file.rs diff --git a/.gitignore b/.gitignore index 6936990..d889fbf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target **/*.rs.bk Cargo.lock +.idea diff --git a/Cargo.toml b/Cargo.toml index 24a07ee..49e9f65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,4 +13,4 @@ keywords = ["parser", "sgf", "go", "baduk", "weiqi"] [dependencies] pest = "2.1.0" pest_derive = "2.1.0" -derive_more = "0.14.0" +derive_more = "0.15.0" diff --git a/sgf.pest b/sgf.pest index 4171406..47797e6 100644 --- a/sgf.pest +++ b/sgf.pest @@ -1,3 +1,5 @@ +WHITESPACE = _{ " " | NEWLINE} + game_tree = { "(" ~ sequence? ~ game_tree* ~ ")"} sequence = { node{1,} } node = { ";" ~ property+ } @@ -9,3 +11,4 @@ char = { "\\" ~ "]" | !("]") ~ ANY } + diff --git a/src/lib.rs b/src/lib.rs index e1b92b1..caaba45 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,5 +37,5 @@ mod tree; pub use crate::error::{SgfError, SgfErrorKind}; pub use crate::node::GameNode; pub use crate::parser::parse; -pub use crate::token::{Color, SgfToken}; +pub use crate::token::{Color, SgfToken, Action}; pub use crate::tree::GameTree; diff --git a/src/node.rs b/src/node.rs index 5a1bb58..7df1571 100644 --- a/src/node.rs +++ b/src/node.rs @@ -15,7 +15,7 @@ impl GameNode { SgfToken::Unknown(_) => true, _ => false, }) - .collect::>() + .collect() } /// Gets a vector of all `SgfToken::Invalid` tokens @@ -26,7 +26,7 @@ impl GameNode { SgfToken::Invalid(_) => true, _ => false, }) - .collect::>() + .collect() } } diff --git a/src/parser.rs b/src/parser.rs index 1a55b9f..1d10904 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -126,5 +126,8 @@ fn parse_pair(pair: Pair<'_, Rule>) -> ParserNode<'_> { Rule::char => { unreachable!(); } + Rule::WHITESPACE => { + unreachable!(); + } } } diff --git a/src/token.rs b/src/token.rs index 4522aa3..42ef752 100644 --- a/src/token.rs +++ b/src/token.rs @@ -1,5 +1,6 @@ use crate::{SgfError, SgfErrorKind}; use std::ops::Not; +use crate::token::Action::{Pass, Move}; /// Indicates what color the token is related to #[derive(Debug, PartialEq, Copy, Clone)] @@ -18,11 +19,17 @@ impl Not for Color { } } +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum Action { + Move(u8, u8), + Pass, +} + /// Enum describing all possible SGF Properties #[derive(Debug, PartialEq, Clone)] pub enum SgfToken { Add { color: Color, coordinate: (u8, u8) }, - Move { color: Color, coordinate: (u8, u8) }, + Move { color: Color, action: Action }, Time { color: Color, time: u32 }, PlayerName { color: Color, name: String }, PlayerRank { color: Color, rank: String }, @@ -53,7 +60,10 @@ impl SgfToken { /// use sgf_parser::*; /// /// let token = SgfToken::from_pair("B", "aa"); - /// assert_eq!(token, SgfToken::Move { color: Color::Black, coordinate: (1, 1) }); + /// assert_eq!(token, SgfToken::Move { color: Color::Black, action: Action::Move(1, 1) }); + /// + /// let token = SgfToken::from_pair("B", ""); + /// assert_eq!(token, SgfToken::Move { color: Color::Black, action: Action::Pass }); /// /// let token = SgfToken::from_pair("B", "not_coord"); /// assert_eq!(token, SgfToken::Invalid(("B".to_string(), "not_coord".to_string()))); @@ -87,11 +97,11 @@ impl SgfToken { color: Color::Black, coordinate, }), - "B" => str_to_coordinates(value) + "B" => move_str_to_coord(value) .ok() .map(|coordinate| SgfToken::Move { color: Color::Black, - coordinate, + action: coordinate, }), "BL" => value.parse().ok().map(|time| SgfToken::Time { color: Color::Black, @@ -111,11 +121,11 @@ impl SgfToken { color: Color::White, coordinate, }), - "W" => str_to_coordinates(value) + "W" => move_str_to_coord(value) .ok() .map(|coordinate| SgfToken::Move { color: Color::White, - coordinate, + action: coordinate, }), "WL" => value.parse().ok().map(|time| SgfToken::Time { color: Color::White, @@ -201,12 +211,15 @@ impl Into for &SgfToken { let value = coordinate_to_str(*coordinate); format!("{}[{}]", token, value) } - SgfToken::Move { color, coordinate } => { + SgfToken::Move { color, action } => { let token = match color { Color::Black => "B", Color::White => "W", }; - let value = coordinate_to_str(*coordinate); + let value = match *action { + Move(x,y) => coordinate_to_str((x,y)), + Pass => String::new() + }; format!("{}[{}]", token, value) } SgfToken::Time { color, time } => { @@ -260,14 +273,11 @@ fn split_size_text(input: &str) -> Option<(u32, u32)> { Some((width, height)) } + /// Converts goban coordinates to string representation fn coordinate_to_str(coordinate: (u8, u8)) -> String { - let conv = |n| { - // skips 'I' as a valid coordinate - n + if n >= 9 { 97 } else { 96 } - }; - let x = conv(coordinate.0) as char; - let y = conv(coordinate.1) as char; + let x = (coordinate.0 + 96) as char; + let y = (coordinate.1 + 96) as char; [x, y].iter().collect() } @@ -280,29 +290,35 @@ fn split_label_text(input: &str) -> Option<(&str, &str)> { } } +fn move_str_to_coord(input: &str) -> Result { + if input.is_empty() { + Ok(Pass) + } else { + match str_to_coordinates(input) { + Ok(coordinates) => Ok(Move(coordinates.0, coordinates.1)), + Err(e) => Err(e) + } + } +} + /// Converts a string describing goban coordinates to numeric coordinates -/// skips 'I' as a valid coordinate fn str_to_coordinates(input: &str) -> Result<(u8, u8), SgfError> { if input.len() != 2 { - return Err(SgfErrorKind::ParseError.into()); + Err(SgfErrorKind::ParseError.into()) + } else { + let coords = input + .to_lowercase() + .as_bytes() + .iter() + .map(|c| convert_u8_to_coordinate(*c)) + .collect::>(); + Ok((coords[0], coords[1])) } - let coords = input - .to_lowercase() - .as_bytes() - .iter() - .map(|&c| convert_u8_to_coordinate(c)) - .take(2) - .collect::>(); - Ok((coords[0], coords[1])) } /// Converts a u8 char to numeric coordinates +/// +#[inline] fn convert_u8_to_coordinate(c: u8) -> u8 { - let n = c - 96; - // skips 'I' as a valid coordinate - if n >= 9 { - n - 1 - } else { - n - } + c - 96 } diff --git a/tests/model.rs b/tests/model.rs index 49dcf43..5071f21 100644 --- a/tests/model.rs +++ b/tests/model.rs @@ -1,6 +1,7 @@ #[cfg(test)] mod model_tests { use sgf_parser::*; + use sgf_parser::Action::Move; #[test] fn can_get_unknown_nodes() { @@ -13,7 +14,7 @@ mod model_tests { tokens: vec![ SgfToken::Move { color: Color::White, - coordinate: (5, 6) + action: Move(5, 6), }, SgfToken::Unknown(("AC".to_string(), "23".to_string())) ] @@ -56,7 +57,7 @@ mod model_tests { Some(&GameNode { tokens: vec![SgfToken::Move { color: Color::Black, - coordinate: (4, 3) + action: Move(4, 3), }] }) ); @@ -65,7 +66,7 @@ mod model_tests { Some(&GameNode { tokens: vec![SgfToken::Move { color: Color::White, - coordinate: (5, 6) + action: Move(5, 6), }] }) ); @@ -82,7 +83,7 @@ mod model_tests { Some(&GameNode { tokens: vec![SgfToken::Move { color: Color::Black, - coordinate: (4, 3) + action: Move(4, 3), }] }) ); @@ -91,7 +92,7 @@ mod model_tests { Some(&GameNode { tokens: vec![SgfToken::Move { color: Color::White, - coordinate: (5, 6) + action: Move(5, 6), }] }) ); @@ -100,7 +101,7 @@ mod model_tests { Some(&GameNode { tokens: vec![SgfToken::Move { color: Color::Black, - coordinate: (1, 1) + action: Move(1, 1), }] }) ); @@ -122,7 +123,7 @@ mod model_tests { Some(&GameNode { tokens: vec![SgfToken::Move { color: Color::Black, - coordinate: (4, 3) + action: Move(4, 3), }] }) ); @@ -131,7 +132,7 @@ mod model_tests { Some(&GameNode { tokens: vec![SgfToken::Move { color: Color::White, - coordinate: (5, 6) + action: Move(5, 6), }] }) ); @@ -140,7 +141,7 @@ mod model_tests { Some(&GameNode { tokens: vec![SgfToken::Move { color: Color::Black, - coordinate: (3, 3) + action: Move(3, 3), }] }) ); diff --git a/tests/parse.rs b/tests/parse.rs index 08c8482..4cab101 100644 --- a/tests/parse.rs +++ b/tests/parse.rs @@ -1,6 +1,7 @@ #[cfg(test)] mod parser_tests { use sgf_parser::*; + use sgf_parser::Action::Move; #[test] fn errors_on_invalid_root_token_placement() { @@ -22,7 +23,7 @@ mod parser_tests { nodes: vec![GameNode { tokens: vec![SgfToken::Komi(6.5f32)] }], - variations: vec![] + variations: vec![], } ); } @@ -38,7 +39,7 @@ mod parser_tests { nodes: vec![GameNode { tokens: vec![SgfToken::Copyright("2017".to_string())], }], - variations: vec![] + variations: vec![], } ); } @@ -55,15 +56,15 @@ mod parser_tests { tokens: vec![ SgfToken::Move { color: Color::Black, - coordinate: (4, 3) + action: Move(4, 3), }, SgfToken::Time { color: Color::Black, - time: 3498 + time: 3498, } ], }], - variations: vec![] + variations: vec![], } ); } @@ -80,17 +81,17 @@ mod parser_tests { GameNode { tokens: vec![SgfToken::Move { color: Color::Black, - coordinate: (4, 3) + action: Move(4, 3), }], }, GameNode { tokens: vec![SgfToken::Move { color: Color::White, - coordinate: (5, 6) + action: Move(5, 6), }], } ], - variations: vec![] + variations: vec![], } ); } @@ -106,29 +107,29 @@ mod parser_tests { nodes: vec![GameNode { tokens: vec![SgfToken::Move { color: Color::Black, - coordinate: (1, 1) + action: Move(1, 1), }], - },], + }, ], variations: vec![ GameTree { nodes: vec![GameNode { tokens: vec![SgfToken::Move { color: Color::White, - coordinate: (2, 2) + action: Move(2, 2), }], - },], - variations: vec![] + }, ], + variations: vec![], }, GameTree { nodes: vec![GameNode { tokens: vec![SgfToken::Move { color: Color::White, - coordinate: (3, 3) + action: Move(3, 3), }], - },], - variations: vec![] + }, ], + variations: vec![], } - ] + ], } ); } @@ -147,11 +148,11 @@ mod parser_tests { SgfToken::Event("event".to_string()), SgfToken::PlayerName { color: Color::Black, - name: "black".to_string() + name: "black".to_string(), }, SgfToken::PlayerName { color: Color::White, - name: "white".to_string() + name: "white".to_string(), }, SgfToken::Comment("comment".to_string()), ], @@ -159,11 +160,11 @@ mod parser_tests { GameNode { tokens: vec![SgfToken::Move { color: Color::Black, - coordinate: (1, 1) + action: Move(1, 1), }], } ], - variations: vec![] + variations: vec![], } ); } @@ -180,20 +181,20 @@ mod parser_tests { GameNode { tokens: vec![SgfToken::Move { color: Color::Black, - coordinate: (4, 3) + action: Move(4, 3), }], }, GameNode { - tokens: vec![SgfToken::Unknown(("FO".to_string(), "asdf".to_string())),], + tokens: vec![SgfToken::Unknown(("FO".to_string(), "asdf".to_string())), ], }, GameNode { tokens: vec![SgfToken::Move { color: Color::White, - coordinate: (5, 6) + action: Move(5, 6), }], } ], - variations: vec![] + variations: vec![], } ); } @@ -207,9 +208,9 @@ mod parser_tests { sgf, GameTree { nodes: vec![GameNode { - tokens: vec![SgfToken::Comment("a [wrapped\\] comment".to_string()),], - },], - variations: vec![] + tokens: vec![SgfToken::Comment("a [wrapped\\] comment".to_string()), ], + }, ], + variations: vec![], } ); } diff --git a/tests/sgf/ShusakuvsInseki.sgf b/tests/sgf/ShusakuvsInseki.sgf new file mode 100644 index 0000000..304ba04 --- /dev/null +++ b/tests/sgf/ShusakuvsInseki.sgf @@ -0,0 +1,47 @@ +(;SZ[19] +HA[0] +ST[0] +PB[Shusaku] +PW[Gennan Inseki] +KM[0.0] +RE[B+2] +BR[4d] +WR[8d] +C[Gennan Inseki(white) VS Shusaku(black)] +;B[qd];W[dc];B[pq];W[oc];B[cp];W[cf];B[ep];W[qo] +;B[pe];W[np];B[po];W[pp];B[op];W[qp];B[oq];W[oo];B[pn];W[qq] +;B[nq];W[on];B[pm];W[om];B[pl];W[mp];B[mq];W[ol];B[pk];W[lq] +;B[lr];W[kr];B[lp];W[kq];B[qr];W[rr];B[rs];W[mr];B[nr];W[pr] +;B[ps];W[qs];B[no];W[mo];B[qr];W[rm];B[rl];W[qs];B[lo];W[mn] +;B[qr];W[qm];B[or];W[ql];B[qj];W[rj];B[ri];W[rk];B[ln];W[mm] +;B[qi];W[rq];B[jn];W[ls];B[ns];W[gq];B[go];W[ck];B[kc];W[ic] +;B[pc];W[nj];B[ke];W[og];B[oh];W[pb];B[qb];W[ng];B[mi];W[mj] +;B[nd];W[ph];B[qg];W[pg];B[hq];W[hr];B[ir];W[iq];B[hp];W[jr] +;B[fc];W[lc];B[ld];W[mc];B[lb];W[mb];B[md];W[qf];B[pf];W[qh] +;B[rg];W[rh];B[sh];W[rf];B[sg];W[pj];B[pi];W[oi];B[oj];W[ni] +;B[qk];W[ok];B[qe];W[kb];B[jb];W[ka];B[jc];W[ob];B[ja];W[la] +;B[db];W[cc];B[fe];W[cn];B[gr];W[is];B[fq];W[io];B[ji] +C[The ear-reddening move.] +;W[eb] +;B[fb];W[eg];B[dj];W[dk];B[ej];W[cj];B[dh];W[ij];B[hm];W[gj] +;B[eh];W[fl];B[fg];W[er];B[dm];W[fn];B[dn];W[gn];B[jj];W[jk] +;B[kk];W[ii];B[ik];W[jl];B[kl];W[il];B[jh];W[co];B[do];W[ih] +;B[hn];W[hl];B[bl];W[dg];B[gh];W[ch];B[ig];W[ec];B[cr];W[fd] +;B[gd];W[ed];B[gc];W[bk];B[cm];W[gs];B[gp];W[li];B[kg];W[in] +;B[lj];W[lg];B[gm];W[jf];B[jg];W[im];B[fm];W[kf];B[lf];W[mf] +;B[le];W[gf];B[hf];W[ff];B[gg];W[lk];B[kj];W[km];B[lm];W[ll] +;B[jm];W[ge];B[he];W[ef];B[ea];W[cb];B[fr];W[fs];B[dr];W[qa] +;B[ra];W[pa];B[rb];W[da];B[gi];W[fj];B[fi];W[fa];B[ga];W[gl] +;B[ek];W[em];B[ho];W[el];B[en];W[jo];B[kn];W[ci];B[lh];W[mh] +;B[mg];W[di];B[ei];W[lg];B[qn];W[rn];B[re];W[sl];B[mg];W[bm] +;B[am];W[lg];B[eq];W[es];B[mg];W[ha];B[gb];W[lg];B[ds];W[hs] +;B[mg];W[sj];B[si];W[lg];B[sr];W[sq];B[mg];W[hd];B[hb];W[lg] +;B[ro];W[so];B[mg];W[ss];B[qs];W[lg];B[sn];W[rp];B[mg];W[cl] +;B[bn];W[lg];B[ml];W[mk];B[mg];W[pj];B[sf];W[lg];B[nn];W[nl] +;B[mg];W[ib];B[ia];W[lg];B[nc];W[nb];B[mg];W[jd];B[kd];W[lg] +;B[ma];W[na];B[mg];W[qc];B[rc];W[lg];B[js];W[ks];B[mg];W[hc] +;B[id];W[lg];B[fk];W[hj];B[mg];W[hh];B[hg];W[lg];B[gk];W[hk] +;B[mg];W[ak];B[lg];W[al];B[bm];W[nf];B[od];W[ki];B[ms];W[kp] +;B[ip];W[jp];B[lr];W[oj];B[mr];W[ea];B[sr] +C[Result = Shusaku by 2 points.] +) diff --git a/tests/sgf_file.rs b/tests/sgf_file.rs new file mode 100644 index 0000000..6ccccbd --- /dev/null +++ b/tests/sgf_file.rs @@ -0,0 +1,7 @@ +#[cfg(test)] +mod sgf_files_test { + #[test] + fn parse_sfg() { + let _g = sgf_parser::parse(include_str!("sgf/ShusakuvsInseki.sgf")).unwrap(); + } +} diff --git a/tests/token.rs b/tests/token.rs index cf58a24..c317b60 100644 --- a/tests/token.rs +++ b/tests/token.rs @@ -1,7 +1,7 @@ #[cfg(test)] mod token_tests { use sgf_parser::*; - + use sgf_parser::Action::Move; #[test] fn can_parse_move_tokens() { let token = SgfToken::from_pair("B", "aa"); @@ -9,7 +9,7 @@ mod token_tests { token, SgfToken::Move { color: Color::Black, - coordinate: (1, 1), + action: Move(1, 1), } ); let string_token: String = token.into(); @@ -20,7 +20,7 @@ mod token_tests { token, SgfToken::Move { color: Color::White, - coordinate: (10, 10), + action: Move(11, 11), } ); let string_token: String = token.into(); @@ -216,7 +216,7 @@ mod token_tests { token, SgfToken::Label { label: "foo".to_string(), - coordinate: (10, 10) + coordinate: (11, 11) } ); let string_token: String = token.into(); @@ -241,7 +241,7 @@ mod token_tests { token, SgfToken::Add { color: Color::White, - coordinate: (10, 10), + coordinate: (11, 11), } ); let string_token: String = token.into(); diff --git a/tests/tree.rs b/tests/tree.rs index e7aad46..68bffae 100644 --- a/tests/tree.rs +++ b/tests/tree.rs @@ -1,6 +1,7 @@ #[cfg(test)] mod tree_tests { use sgf_parser::*; + use sgf_parser::Action::Move; #[test] fn can_convert_game_tree_without_variations() { @@ -21,20 +22,20 @@ mod tree_tests { GameNode { tokens: vec![SgfToken::Move { color: Color::Black, - coordinate: (3, 3), + action: Move(3, 3), }], }, GameNode { tokens: vec![SgfToken::Move { color: Color::White, - coordinate: (16, 16), + action: Move(16, 16), }], }, ], variations: vec![], }; let string_tree: String = tree.into(); - assert_eq!(string_tree, "(;PB[black]PW[white];B[cc];W[qq])"); + assert_eq!(string_tree, "(;PB[black]PW[white];B[cc];W[pp])"); } #[test] @@ -56,13 +57,13 @@ mod tree_tests { GameNode { tokens: vec![SgfToken::Move { color: Color::Black, - coordinate: (3, 3), + action: Move(3, 3), }], }, GameNode { tokens: vec![SgfToken::Move { color: Color::White, - coordinate: (16, 16), + action: Move(16, 16), }], }, ], @@ -71,7 +72,7 @@ mod tree_tests { nodes: vec![GameNode { tokens: vec![SgfToken::Move { color: Color::Black, - coordinate: (4, 16), + action: Move(4, 16), }], }], variations: vec![], @@ -80,7 +81,7 @@ mod tree_tests { nodes: vec![GameNode { tokens: vec![SgfToken::Move { color: Color::Black, - coordinate: (16, 4), + action: Move(16, 4), }], }], variations: vec![], @@ -90,7 +91,7 @@ mod tree_tests { let string_tree: String = tree.into(); assert_eq!( string_tree, - "(;PB[black]PW[white];B[cc];W[qq](;B[dq])(;B[qd]))" + "(;PB[black]PW[white];B[cc];W[pp](;B[dp])(;B[pd]))" ); } }