// https://red-bean.com/sgf/user_guide/index.html // https://red-bean.com/sgf/sgf4.html // todo: support collections in a file // Properties to support. Remove each one as it gets support. // B // KO // MN // W // AB // AE // AW // PL // C // DM // GB // GW // HO // N // UC // V // BM // DO // IT // TE // AR // CR // DD // LB // LN // MA // 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 // WL // FG // PM // VW use nom::{ bytes::complete::{tag, take_until}, character::complete::{alpha1, anychar, multispace0}, combinator::eof, multi::{many0, many1, many_till}, sequence::{delimited, terminated}, IResult, Parser, }; use thiserror::Error; pub enum Warning {} #[derive(Debug, PartialEq, Error)] pub enum ParseError { #[error("An unknown error was found")] UnknownError, } // todo: support ST root node #[derive(Debug)] pub struct GameTree { pub file_format: i8, pub app: Option, pub game_type: GameType, pub board_size: Size, pub text: String, } pub struct GameInfo { pub annotator: Option, pub copyright: Option, pub event: Option, // Games can be played across multiple days, even multiple years. The format specifies // shortcuts. pub date_time: Vec, pub location: Option, // special rules for the round-number and type pub round: Option, pub ruleset: Option, pub source: Option, pub time_limits: Option, pub game_keeper: Option, pub game_name: Option, pub game_comments: Option, pub black_player: Option, pub black_rank: Option, pub black_team: Option, pub white_player: Option, pub white_rank: Option, pub white_team: Option, pub opening: Option, pub overtime: Option, pub result: Option, } pub enum GameResult { Annulled, Draw, Black(Win), White(Win), } pub enum Win { Score(i32), Resignation, Forfeit, Time, } #[derive(Debug, PartialEq)] pub struct Size { width: i32, height: i32, } #[derive(Debug, PartialEq)] pub enum GameType { Go, Unsupported, } struct Sequence(Node); /* struct Node { // properties } */ enum PropType { Move, Setup, Root, GameInfo, } enum PropValue { Empty, Number, Real, Double, Color, SimpleText, Text, Point, Move, Stone, } #[derive(Debug, PartialEq)] struct Tree { sequence: Vec, sub_sequences: Vec, } impl ToString for Tree { fn to_string(&self) -> String { let sequence = self .sequence .iter() .map(|node| node.to_string()) .collect::(); let subsequences = self .sub_sequences .iter() .map(|seq| seq.to_string()) .collect::(); format!("({}{})", sequence, subsequences) } } #[derive(Debug, PartialEq)] struct Node { properties: Vec, } impl ToString for Node { fn to_string(&self) -> String { let props = self .properties .iter() .map(|prop| prop.to_string()) .collect::(); format!(";{}", props) } } #[derive(Debug, PartialEq)] struct Property { ident: String, 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) } } // 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 /* pub fn parse_sgf(input: &str) -> Result<(GameTree, Vec), ParseError> { let (_, gameinfo) = parse_gametree(input).unwrap(); Ok((gameinfo, vec![])) } */ fn parse_tree(input: &str) -> IResult<&str, Tree> { println!("parse_tree: {}", input); let (input, _) = multispace0(input)?; delimited(tag("("), parse_sequence, tag(")"))(input) } fn parse_sequence(input: &str) -> IResult<&str, Tree> { println!("parse_sequence: {}", input); let (input, _) = multispace0(input)?; let (input, nodes) = many1(parse_node)(input)?; let (input, sub_sequences) = many0(parse_tree)(input)?; Ok(( input, Tree { sequence: nodes, sub_sequences, }, )) } fn parse_node(input: &str) -> IResult<&str, Node> { println!("parse_node: {}", input); let (input, _) = multispace0(input)?; let (input, _) = tag(";")(input)?; let (input, properties) = many1(parse_property)(input)?; Ok((input, Node { properties })) } fn parse_property(input: &str) -> IResult<&str, Property> { println!("parse_property: {}", input); let (input, ident) = alpha1(input)?; let (input, values) = many1(delimited(tag("["), take_until("]"), tag("]")))(input)?; let values = values .into_iter() .map(|v| v.to_owned()) .collect::>(); Ok(( input, Property { ident: ident.to_owned(), values, }, )) } /* fn parse_gametree(input: &str) -> IResult<&str, GameTree> { let (input, _) = tag("(;")(input)?; let (input, properties) = many1(parse_property)(input)?; let (input, _) = tag(")")(input)?; println!("properties: {:?}", properties); Ok((input, unimplemented!())) } */ pub fn add(left: usize, right: usize) -> usize { left + right } #[cfg(test)] mod tests { use super::*; const EXAMPLE_1: &'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])))"; const EXAMPLE_2: &'static str = "(;FF[4]GM[1]SZ[19]AP[SGFC:1.13b] PB[troy]BR[12k*] PW[john]WR[11k*] KM[0.5]RE[W+12.5] DT[1998-06-15] TM[600] ;B[pd];W[dp];B[pq];W[dd];B[qk];W[jd];B[fq];W[dj];B[jp];W[jj] ;B[cn]LB[dn:A][po:B]C[dada: other ideas are 'A' (d6) or 'B' (q5)] ;W[eo](;B[dl]C[dada: hm - looks troublesome. Usually B plays the 3,3 invasion - see variation];W[qo];B[qp] ... ;W[sr];B[sk];W[sg];B[pa];W[gc];B[pi];W[ph];B[de];W[ed];B[kn] ;W[dh];B[eh];W[se];B[sd];W[af];B[ie];W[id];B[hf];W[hd];B[if] ;W[fp];B[gq];W[qj];B[sj];W[rh];B[sn];W[so];B[sm];W[ep];B[mn]) ... (;W[dq]N[wrong direction];B[qo];W[qp]))"; #[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()] }] } ); 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()] }] } ); } #[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, Tree { sequence: vec![ Node { properties: vec![Property { ident: "B".to_owned(), values: vec!["ab".to_owned()] }] }, Node { properties: vec![Property { ident: "W".to_owned(), values: vec!["dp".to_owned()] }] }, Node { properties: vec![ Property { ident: "B".to_owned(), values: vec!["pq".to_owned()] }, Property { ident: "C".to_owned(), values: vec!["some comments".to_owned()] } ] } ], sub_sequences: vec![], } ); } #[test] fn it_can_parse_a_sequence_with_subsequences() { let text = "(;C[a];C[b](;C[c])(;C[d];C[e]))"; let (_, sequence) = parse_tree(text).unwrap(); let main_sequence = vec![ Node { properties: vec![Property { ident: "C".to_owned(), values: vec!["a".to_owned()], }], }, Node { properties: vec![Property { ident: "C".to_owned(), values: vec!["b".to_owned()], }], }, ]; let subsequence_1 = Tree { sequence: vec![Node { properties: vec![Property { ident: "C".to_owned(), values: vec!["c".to_owned()], }], }], sub_sequences: vec![], }; let subsequence_2 = Tree { sequence: vec![ Node { properties: vec![Property { ident: "C".to_owned(), values: vec!["d".to_owned()], }], }, Node { properties: vec![Property { ident: "C".to_owned(), values: vec!["e".to_owned()], }], }, ], sub_sequences: vec![], }; assert_eq!( sequence, Tree { sequence: main_sequence, sub_sequences: vec![subsequence_1, subsequence_2], } ); } #[test] fn it_can_parse_example_1() { let (_, ex_tree) = parse_tree(EXAMPLE_1).unwrap(); assert_eq!(ex_tree.sequence.len(), 1); assert_eq!(ex_tree.sequence[0].properties.len(), 2); assert_eq!( ex_tree.sequence[0].properties[0], Property { ident: "FF".to_owned(), values: vec!["4".to_owned()] } ); assert_eq!(ex_tree.sub_sequences.len(), 2); assert_eq!(ex_tree.sub_sequences[0].sequence.len(), 2); assert_eq!( ex_tree.sub_sequences[0].sequence, vec![ Node { properties: vec![Property { ident: "C".to_owned(), values: vec!["a".to_owned()] }] }, Node { properties: vec![Property { ident: "C".to_owned(), values: vec!["b".to_owned()] }] }, ] ); assert_eq!(ex_tree.sub_sequences[0].sub_sequences.len(), 2); } #[test] fn it_can_regenerate_the_tree() { let (_, tree1) = parse_tree(EXAMPLE_1).unwrap(); println!("{}", tree1.to_string()); 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, tree2); } /* fn with_examples(f: impl FnOnce(GameTree, GameTree)) { let (example_1, _) = parse_sgf(EXAMPLE_1).unwrap(); let (example_2, _) = parse_sgf(EXAMPLE_2).unwrap(); f(example_1, example_2); } #[test] fn it_parses_game_root() { with_examples(|ex_1, ex_2| { assert_eq!(ex_1.file_format, 4); assert_eq!(ex_1.app, None); assert_eq!(ex_1.game_type, GameType::Go); assert_eq!( ex_1.board_size, Size { width: 19, height: 19 } ); assert_eq!(ex_1.text, EXAMPLE_1.to_owned()); assert_eq!(ex_2.file_format, 4); assert_eq!(ex_2.app, Some("SGFC:1.13b".to_owned())); assert_eq!(ex_2.game_type, GameType::Go); assert_eq!( ex_2.board_size, Size { width: 19, height: 19 } ); assert_eq!(ex_2.text, EXAMPLE_2.to_owned()); }); } */ }