use crate::{ parser::{self, Annotation, Evaluation, Move, SetupInstr, Size, UnknownProperty}, Color, Date, GameResult, GameType, }; use serde::{Deserialize, Serialize}; use std::{collections::HashSet, time::Duration}; use uuid::Uuid; #[derive(Clone, Debug, PartialEq)] pub enum GameError { InvalidGame, RequiredPropertiesMissing, InvalidGameNode(GameNodeError), } #[derive(Clone, Debug, PartialEq)] pub enum MoveNodeError { IncompatibleProperty(parser::Property), ConflictingProperty, NotAMoveNode, ChildError(Box), } #[derive(Clone, Debug, PartialEq)] pub enum SetupNodeError { IncompatibleProperty(parser::Property), ConflictingProperty, ConflictingPosition, NotASetupNode, ChildError(Box), } #[derive(Clone, Debug, PartialEq)] pub enum GameNodeError { UnsupportedGameNode(MoveNodeError, SetupNodeError), ConflictingProperty, ConflictingPosition, } #[derive(Clone, Debug, PartialEq, Default, Deserialize, Serialize)] pub struct Player { pub name: Option, pub rank: Option, pub team: Option, } /// This represents the more semantic version of the game parser. Where the `parser` crate pulls /// out a raw set of nodes, this structure is guaranteed to be a well-formed game. Getting to this /// level, the interpreter will reject any games that have setup properties and move properties /// mixed in a single node. If there are other semantic problems, the interpreter will reject /// those, as well. Where the function of the parser is to understand and correct fundamental /// syntax issues, the result of the GameRecord is to have a fully-understood game. However, this /// doesn't (yet?) go quite to the level of apply the game type (i.e., this is Go, Chess, Yinsh, or /// whatever). #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct GameRecord { pub game_type: GameType, // TODO: board size is not necessary in all games. Hive has no defined board size. pub board_size: Size, pub black_player: Player, pub white_player: Player, pub app: Option, pub annotator: Option, pub copyright: Option, pub dates: Vec, pub event: Option, pub game_name: Option, pub extra_info: Option, pub opening_info: Option, pub location: Option, pub result: Option, pub round: Option, pub rules: Option, pub source: Option, pub time_limit: Option, pub overtime: Option, pub transcriber: Option, pub children: Vec, } impl GameRecord { pub fn new( game_type: GameType, board_size: Size, black_player: Player, white_player: Player, ) -> Self { Self { game_type, board_size, black_player, white_player, app: None, annotator: None, copyright: None, dates: vec![], event: None, game_name: None, extra_info: None, opening_info: None, location: None, result: None, round: None, rules: None, source: None, time_limit: None, overtime: None, transcriber: None, children: vec![], } } /// Generate a list of moves which constitute the main line of the game. This is the game as it /// was actually played out, and by convention consists of the first node in each list of /// children. pub fn mainline(&self) -> Vec<&GameNode> { let mut moves: Vec<&GameNode> = vec![]; let mut next = self.children.first(); while let Some(node) = next { // Given that I know that I have a node, and I know that I'm going to push a reference // to it onto my final list, I want to get the first of its children. And I want to // keep doing that until there are no more first children. // // Just going to push references onto the list. No need to copy the nodes for this. // // Pushing a reference onto the list implicitely clones the reference, but not the data // it is pointing to. This means that each time through the loop, `next` points to // something else. This isn't being described very well, though, so it's worth // reviewing in the future. moves.push(node); next = match node { GameNode::MoveNode(node) => node.children.first(), GameNode::SetupNode(node) => node.children.first(), }; } moves } } impl Node for GameRecord { fn children<'a>(&'a self) -> Vec<&'a GameNode> { self.children.iter().collect::>() } fn add_child(&mut self, node: GameNode) -> &mut GameNode { self.children.push(node); self.children.last_mut().unwrap() } } impl TryFrom<&parser::Tree> for GameRecord { type Error = GameError; fn try_from(tree: &parser::Tree) -> Result { let mut ty = None; let mut size = None; let mut black_player = Player { name: None, rank: None, team: None, }; let mut white_player = Player { name: None, rank: None, team: None, }; for prop in tree.root.properties.iter() { match prop { parser::Property::GameType(ty_) => ty = Some(ty_.clone()), parser::Property::BoardSize(size_) => size = Some(size_.clone()), parser::Property::BlackPlayer(name) => { black_player.name = Some(name.clone()); } parser::Property::WhitePlayer(name) => { white_player.name = Some(name.clone()); } parser::Property::BlackRank(rank) => { black_player.rank = Some(rank.clone()); } parser::Property::WhiteRank(rank) => { white_player.rank = Some(rank.clone()); } parser::Property::BlackTeam(team) => { black_player.team = Some(team.clone()); } parser::Property::WhiteTeam(team) => { white_player.team = Some(team.clone()); } _ => {} } } let mut s = match (ty, size) { (Some(ty), Some(size)) => Ok(Self::new(ty, size, black_player, white_player)), _ => Err(Self::Error::RequiredPropertiesMissing), }?; for prop in tree.root.properties.iter() { match prop { parser::Property::GameType(_) | parser::Property::BoardSize(_) | parser::Property::BlackPlayer(_) | parser::Property::WhitePlayer(_) | parser::Property::BlackRank(_) | parser::Property::WhiteRank(_) | parser::Property::BlackTeam(_) | parser::Property::WhiteTeam(_) => {} parser::Property::Application(v) => s.app = Some(v.clone()), parser::Property::Annotator(v) => s.annotator = Some(v.clone()), parser::Property::Copyright(v) => s.copyright = Some(v.clone()), parser::Property::EventDates(v) => s.dates = v.clone(), parser::Property::EventName(v) => s.event = Some(v.clone()), parser::Property::GameName(v) => s.game_name = Some(v.clone()), parser::Property::ExtraGameInformation(v) => s.extra_info = Some(v.clone()), parser::Property::GameOpening(v) => s.opening_info = Some(v.clone()), parser::Property::GameLocation(v) => s.location = Some(v.clone()), parser::Property::Result(v) => s.result = Some(v.clone()), parser::Property::Round(v) => s.round = Some(v.clone()), parser::Property::Ruleset(v) => s.rules = Some(v.clone()), parser::Property::Source(v) => s.source = Some(v.clone()), parser::Property::TimeLimit(v) => s.time_limit = Some(*v), parser::Property::Overtime(v) => s.overtime = Some(v.clone()), // parser::Property::Data(v) => s.transcriber = Some(v.clone()), _ => {} } } s.children = tree .root .next .iter() .map(GameNode::try_from) .collect::, GameNodeError>>() .map_err(GameError::InvalidGameNode)?; Ok(s) } } #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub enum GameNode { MoveNode(MoveNode), SetupNode(SetupNode), } pub trait Node { /// Provide a pre-order traversal of all of the nodes in the game tree. fn nodes<'a>(&'a self) -> Vec<&'a GameNode> { self.children() .iter() .flat_map(|node| { let mut children = node.nodes(); let mut v = vec![*node]; v.append(&mut children); v }) .collect::>() } fn children(&self) -> Vec<&GameNode>; fn add_child(&mut self, node: GameNode) -> &mut GameNode; } impl GameNode { pub fn id(&self) -> Uuid { match self { GameNode::MoveNode(node) => node.id, GameNode::SetupNode(node) => node.id, } } } impl Node for GameNode { fn children(&self) -> Vec<&GameNode> { match self { GameNode::MoveNode(node) => node.children(), GameNode::SetupNode(node) => node.children(), } } fn nodes(&self) -> Vec<&GameNode> { match self { GameNode::MoveNode(node) => node.nodes(), GameNode::SetupNode(node) => node.nodes(), } } fn add_child(&mut self, new_node: GameNode) -> &mut GameNode { match self { GameNode::MoveNode(node) => node.add_child(new_node), GameNode::SetupNode(node) => node.add_child(new_node), } } } impl TryFrom<&parser::Node> for GameNode { type Error = GameNodeError; fn try_from(n: &parser::Node) -> Result { // I originally wrote this recursively. However, on an ordinary game of a couple hundred // moves, that meant that I was recursing 500 functions, and that exceeded the stack limit. // So, instead, I need to unroll everything to non-recursive form. // // So, I can treat each branch of the tree as a single line. Iterate over that line. I can // only use the MoveNode::try_from and SetupNode::try_from if those functions don't // recurse. Instead, I'm going to process just that node, then return to here and process // the children. let move_node = MoveNode::try_from(n); let setup_node = SetupNode::try_from(n); // I'm much too tired when writing this. I'm still recursing, but I did cut the number of // recursions in half. This helps, but it still doesn't guarantee that I'm going to be able // to parse all possible games. So, still, treat each branch of the game as a single line. // Iterate over that line, don't recurse. Create bookmarks at each branch point, and then // come back to each one. let children = n .next .iter() .map(GameNode::try_from) .collect::, Self::Error>>()?; let node = match (move_node, setup_node) { (Ok(mut node), _) => { node.children = children; Ok(Self::MoveNode(node)) } (Err(_), Ok(mut node)) => { node.children = children; Ok(Self::SetupNode(node)) } (Err(move_err), Err(setup_err)) => { Err(Self::Error::UnsupportedGameNode(move_err, setup_err)) } }?; Ok(node) } } #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct MoveNode { pub id: Uuid, pub color: Color, pub mv: Move, pub children: Vec, pub time_left: Option, pub moves_left: Option, pub name: Option, pub evaluation: Option, pub value: Option, pub comments: Option, pub annotation: Option, pub unknown_props: Vec<(String, String)>, } impl MoveNode { pub fn new(color: Color, mv: Move) -> Self { Self { id: Uuid::new_v4(), color, mv, children: Vec::new(), time_left: None, moves_left: None, name: None, evaluation: None, value: None, comments: None, annotation: None, unknown_props: vec![], } } } impl Node for MoveNode { fn children<'a>(&'a self) -> Vec<&'a GameNode> { self.children.iter().collect::>() } fn add_child(&mut self, node: GameNode) -> &mut GameNode { self.children.push(node); self.children.last_mut().unwrap() } } impl TryFrom<&parser::Node> for MoveNode { type Error = MoveNodeError; fn try_from(n: &parser::Node) -> Result { let s = match n.mv() { Some((color, mv)) => { let mut s = Self::new(color, mv); for prop in n.properties.iter() { match prop { parser::Property::Move((color, mv)) => { if s.color != *color || s.mv != *mv { return Err(Self::Error::ConflictingProperty); } } parser::Property::TimeLeft((color, duration)) => { if s.color != *color { return Err(Self::Error::ConflictingProperty); } if s.time_left.is_some() { return Err(Self::Error::ConflictingProperty); } s.time_left = Some(*duration); } parser::Property::Comment(cmt) => { if s.comments.is_some() { return Err(Self::Error::ConflictingProperty); } s.comments = Some(cmt.clone()); } parser::Property::Evaluation(evaluation) => { if s.evaluation.is_some() { return Err(Self::Error::ConflictingProperty); } s.evaluation = Some(*evaluation) } parser::Property::Annotation(annotation) => { if s.annotation.is_some() { return Err(Self::Error::ConflictingProperty); } s.annotation = Some(*annotation) } parser::Property::Territory(..) => { eprintln!("not processing territory property"); } parser::Property::Unknown(UnknownProperty { ident, value }) => { s.unknown_props.push((ident.clone(), value.clone())); } _ => return Err(Self::Error::IncompatibleProperty(prop.clone())), } } Ok(s) } None => Err(Self::Error::NotAMoveNode), }?; Ok(s) } } #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct SetupNode { id: Uuid, pub positions: Vec, pub children: Vec, } impl SetupNode { pub fn new(positions: Vec) -> Result { let mut board = HashSet::new(); for position in positions.iter() { let point = match position { SetupInstr::Piece((_, point)) => point, SetupInstr::Clear(point) => point, }; if board.contains(point) { return Err(SetupNodeError::ConflictingPosition); } board.insert(point); } Ok(Self { id: Uuid::new_v4(), positions, children: Vec::new(), }) } } impl Node for SetupNode { fn children<'a>(&'a self) -> Vec<&'a GameNode> { self.children.iter().collect::>() } #[allow(dead_code)] fn add_child(&mut self, _node: GameNode) -> &mut GameNode { unimplemented!() } } impl TryFrom<&parser::Node> for SetupNode { type Error = SetupNodeError; fn try_from(n: &parser::Node) -> Result { match n.setup() { Some(elements) => Self::new(elements), None => Err(Self::Error::NotASetupNode), } } } #[allow(dead_code)] pub fn path_to_node(node: &GameNode, id: Uuid) -> Vec<&GameNode> { if node.id() == id { return vec![node]; } for child in node.children() { let mut path = path_to_node(child, id); if path.len() > 1 { path.push(child); return path; } } Vec::new() } #[cfg(test)] mod test { use super::*; use cool_asserts::assert_matches; #[test] fn it_can_create_an_empty_game_tree() { let tree = GameRecord::new( GameType::Go, Size { width: 19, height: 19, }, Player::default(), Player::default(), ); assert_eq!(tree.nodes().len(), 0); } #[test] fn it_can_add_moves_to_a_game() { let mut game = GameRecord::new( GameType::Go, Size { width: 19, height: 19, }, Player::default(), Player::default(), ); let first_move = MoveNode::new(Color::Black, Move::Move("dd".to_owned())); let first_ = game.add_child(GameNode::MoveNode(first_move.clone())); let second_move = MoveNode::new(Color::White, Move::Move("qq".to_owned())); first_.add_child(GameNode::MoveNode(second_move.clone())); let nodes = game.nodes(); assert_eq!(nodes.len(), 2); assert_eq!(nodes[0].id(), first_move.id); assert_eq!(nodes[1].id(), second_move.id); } #[ignore] #[test] fn it_can_set_up_a_game() { unimplemented!() } #[ignore] #[test] fn it_can_load_tree_from_sgf() { unimplemented!() } #[test] fn game_node_can_parse_sgf_move_node() { let n = parser::Node { properties: vec![ parser::Property::Move((Color::White, Move::Move("dp".to_owned()))), parser::Property::TimeLeft((Color::White, Duration::from_secs(176))), parser::Property::Comment("Comments in the game".to_owned()), ], next: vec![], }; assert_matches!(GameNode::try_from(&n), Ok(GameNode::MoveNode(_))); } } #[cfg(test)] mod root_node_tests { #[ignore] #[test] fn it_rejects_move_properties() { unimplemented!() } #[ignore] #[test] fn it_rejects_setup_properties() { unimplemented!() } #[ignore] #[test] fn it_can_parse_a_root_sgf() { unimplemented!() } } #[cfg(test)] mod move_node_tests { use crate::parser::PositionList; use super::*; use cool_asserts::assert_matches; #[test] fn it_can_parse_an_sgf_move_node() { let n = parser::Node { properties: vec![ parser::Property::Move((Color::White, Move::Move("dp".to_owned()))), parser::Property::TimeLeft((Color::White, Duration::from_secs(176))), parser::Property::Comment("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.mv, Move::Move("dp".to_owned())); assert_eq!(node.children, vec![]); assert_eq!(node.time_left, Some(Duration::from_secs(176))); assert_eq!(node.comments, Some("Comments in the game".to_owned())); }); } #[test] fn it_rejects_an_sgf_setup_node() { let n = parser::Node { properties: vec![ parser::Property::Move((Color::White, Move::Move("dp".to_owned()))), parser::Property::TimeLeft((Color::White, Duration::from_secs(176))), parser::Property::SetupBlackStones(PositionList(vec![ "dd".to_owned(), "de".to_owned(), ])), ], next: vec![], }; assert_matches!( MoveNode::try_from(&n), Err(MoveNodeError::IncompatibleProperty(_)) ); } } #[cfg(test)] mod setup_node_tests { use crate::parser::SetupInstr; use super::*; use cool_asserts::assert_matches; #[ignore] #[test] fn it_can_parse_an_sgf_setup_node() { unimplemented!() } #[test] fn it_rejects_conflicting_placement_properties() { assert_matches!( SetupNode::new(vec![ SetupInstr::Piece((Color::Black, "dd".to_owned())), SetupInstr::Piece((Color::Black, "dd".to_owned())), ]), Err(SetupNodeError::ConflictingPosition) ); assert_matches!( SetupNode::new(vec![ SetupInstr::Piece((Color::Black, "dd".to_owned())), SetupInstr::Piece((Color::Black, "ee".to_owned())), SetupInstr::Piece((Color::White, "ee".to_owned())), ]), Err(SetupNodeError::ConflictingPosition) ); } } #[cfg(test)] mod path_test { use super::*; use cool_asserts::assert_matches; use parser::parse_collection; use std::{fs::File, io::Read}; fn with_text(text: &str, f: impl FnOnce(Vec)) { let (_, games) = parse_collection::>(text).unwrap(); let games = games .into_iter() .map(|game| GameRecord::try_from(&game).expect("game to parse")) .collect::>(); f(games); } fn with_file(path: &std::path::Path, f: impl FnOnce(Vec)) { let mut file = File::open(path).unwrap(); let mut text = String::new(); let _ = file.read_to_string(&mut text); with_text(&text, f); } #[test] fn returns_the_mainline_of_a_game_without_branches() { with_file( std::path::Path::new("test_data/2020 USGO DDK, Round 1.sgf"), |games| { let game = &games[0]; let moves = game.mainline(); assert_matches!(moves[0], GameNode::MoveNode(node) => { assert_eq!(node.color, Color::Black); assert_eq!(node.mv, Move::Move("pp".to_owned())); }); assert_matches!(moves[1], GameNode::MoveNode(node) => { assert_eq!(node.color, Color::White); assert_eq!(node.mv, Move::Move("dp".to_owned())); }); assert_matches!(moves[2], GameNode::MoveNode(node) => { assert_eq!(node.color, Color::Black); assert_eq!(node.mv, Move::Move("pd".to_owned())); }); }, ) } #[test] fn returns_the_mainline_of_a_game_with_branches() { with_file(std::path::Path::new("test_data/branch_test.sgf"), |games| { let game = &games[0]; let moves = game.mainline(); assert_matches!(moves[1], GameNode::MoveNode(node) => { assert_eq!(node.color, Color::White); assert_eq!(node.mv, Move::Move("dd".to_owned())); }); assert_matches!(moves[2], GameNode::MoveNode(node) => { assert_eq!(node.color, Color::Black); assert_eq!(node.mv, Move::Move("op".to_owned())); }); assert_matches!(moves[3], GameNode::MoveNode(node) => { assert_eq!(node.color, Color::White); assert_eq!(node.mv, Move::Move("dp".to_owned())); }); }); } #[ignore] #[test] fn returns_empty_list_if_no_game_nodes() { unimplemented!() } #[ignore] #[test] fn returns_empty_list_if_node_not_found() { unimplemented!() } #[ignore] #[test] fn path_excludes_root_node() { unimplemented!() } } #[cfg(test)] mod file_test { use super::*; use crate::Win; use cool_asserts::assert_matches; use parser::parse_collection; use std::{fs::File, io::Read}; fn with_text(text: &str, f: impl FnOnce(Vec)) { let (_, games) = parse_collection::>(text).unwrap(); let games = games .into_iter() .map(|game| GameRecord::try_from(&game).expect("game to parse")) .collect::>(); f(games); } fn with_file(path: &std::path::Path, f: impl FnOnce(Vec)) { let mut file = File::open(path).unwrap(); let mut text = String::new(); let _ = file.read_to_string(&mut text); with_text(&text, f); } /// This test checks against an ordinary game from SGF. It is unannotated and should contain /// only move nodes with no setup nodes. The original source is from a game I played on KGS. #[test] fn it_can_load_an_ordinary_unannotated_game() { with_file( std::path::Path::new("test_data/2020 USGO DDK, Round 1.sgf"), |games| { assert_eq!(games.len(), 1); let game = &games[0]; assert_eq!(game.game_type, GameType::Go); assert_eq!( game.board_size, Size { width: 19, height: 19 } ); assert_eq!( game.black_player, Player { name: Some("savanni".to_owned()), rank: Some("23k".to_owned()), team: None } ); assert_eq!( game.white_player, Player { name: Some("Geckoz".to_owned()), rank: None, team: None } ); assert_eq!(game.app, Some("CGoban:3".to_owned())); assert_eq!(game.annotator, None); assert_eq!(game.copyright, None); assert_eq!( game.dates, vec![Date::Date( chrono::NaiveDate::from_ymd_opt(2020, 8, 5).unwrap() )] ); assert_eq!(game.event, None); assert_eq!(game.game_name, None); assert_eq!(game.extra_info, None); assert_eq!(game.opening_info, None); assert_eq!( game.location, Some("The KGS Go Server at http://www.gokgs.com/".to_owned()) ); assert_eq!(game.result, Some(GameResult::White(Win::Score(17.5)))); assert_eq!(game.round, None); assert_eq!(game.rules, Some("AGA".to_owned())); assert_eq!(game.source, None); assert_eq!(game.time_limit, Some(Duration::from_secs(1800))); assert_eq!(game.overtime, Some("5x30 byo-yomi".to_owned())); assert_eq!(game.transcriber, None); /* Property { ident: "KM".to_owned(), values: vec!["7.50".to_owned()], }, ]; for i in 0..16 { assert_eq!(node.properties[i], expected_properties[i]); } */ let children = game.children(); let node = children.first().unwrap(); assert_matches!(node, GameNode::MoveNode(node) => { assert_eq!(node.color, Color::Black); assert_eq!(node.mv, Move::Move("pp".to_owned())); assert_eq!(node.time_left, Some(Duration::from_secs(1795))); assert_eq!(node.comments, Some("Geckoz [?]: Good game\nsavanni [23k?]: There we go! This UI is... tough.\nsavanni [23k?]: Have fun! Talk to you at the end.\nGeckoz [?]: Yeah, OGS is much better; I'm a UX professional\n".to_owned()) )}); let children = node.children(); let node = children.first().unwrap(); assert_matches!(node, GameNode::MoveNode(node) => { assert_eq!(node.color, Color::White); assert_eq!(node.mv, Move::Move("dp".to_owned())); assert_eq!(node.time_left, Some(Duration::from_secs(1765))); assert_eq!(node.comments, None); }); /* let node = node.next().unwrap(); let expected_properties = vec![ Property { ident: "W".to_owned(), values: vec!["dp".to_owned()], }, Property { ident: "WL".to_owned(), values: vec!["1765.099".to_owned()], }, ]; for i in 0..2 { assert_eq!(node.properties[i], expected_properties[i]); } */ }, ); } }