use crate::{ parser::{self, Annotation, Evaluation, Move, SetupInstr, Size, UnknownProperty}, Color, Date, GameResult, GameType, }; use serde::{Deserialize, Serialize}; use nary_tree::{NodeId, NodeMut, NodeRef, Tree}; use std::{ collections::{HashMap, HashSet, VecDeque}, fmt, fmt::Debug, ops::{Deref, DerefMut}, 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, parser::Node), 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)] 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 trees: 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, trees: vec![], } } pub fn nodes(&self) -> Vec<&GameNode> { self.iter().collect() } pub fn iter(&self) -> impl Iterator { self.trees .iter() .flat_map(|tree| tree.root().unwrap().traverse_pre_order()) .map(|nr| nr.data()) } /// 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) -> Option> { if !self.trees.is_empty() { Some(MainlineIter { next: self.trees[0].root(), tree: &self.trees[0], }) } else { None } } } impl TryFrom 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)?; */ s.trees = tree .root .next .into_iter() .map(recursive_tree_to_slab_tree) .collect::, GameError>>()?; Ok(s) } } fn recursive_tree_to_slab_tree(node: parser::Node) -> Result { let mut slab = Tree::new(); let mut nodes: VecDeque<(NodeId, parser::Node)> = VecDeque::new(); let root_id = slab.set_root(GameNode::try_from(node.clone()).map_err(GameError::InvalidGameNode)?); nodes.push_back((root_id, node)); // I need to keep track of the current parent, and I need to keep on digging deeper into the // tree. Given that I have the root, I can then easily find out all of the children. // // So, maybe I take the list of children. Assign each one of them to a place in the slab tree. // Then push the child *and* its ID into a dequeue. So long as the dequeue is not empty, I want // to pop a node and its ID from the dequeue. The retrieve the NodeMut for it and work on the // node's children. while let Some((node_id, node)) = nodes.pop_front() { let mut game_node: NodeMut = slab .get_mut(node_id) .expect("invalid node_id when retrieving nodes from the game"); // I have a node that is in the tree. Now run across all of its children, adding each one // to the tree and pushing them into the deque along with their IDs. for child in node.next { let slab_child = game_node .append(GameNode::try_from(child.clone()).map_err(GameError::InvalidGameNode)?); nodes.push_back((slab_child.node_id(), child)); } } Ok(GameTree(slab)) } #[derive(Default)] pub struct TreeIter<'a> { queue: VecDeque>, } /* impl<'a> Default for TreeIter<'a> { fn default() -> Self { TreeIter { queue: VecDeque::default(), } } } */ impl<'a> Iterator for TreeIter<'a> { type Item = &'a GameNode; fn next(&mut self) -> Option { let retval = self.queue.pop_front(); if let Some(ref retval) = retval { retval .children() .for_each(|node| self.queue.push_back(node)); } retval.map(|rv| *rv.data()) } } pub struct GameTree(Tree); impl Default for GameTree { fn default() -> Self { Self(Tree::new()) } } impl Clone for GameTree { fn clone(&self) -> Self { match self.0.root() { None => Self(Tree::new()), Some(source_root_node) => { let mut dest = Tree::new(); let dest_root_id = dest.set_root(source_root_node.data().clone()); // In order to add a node to the new tree, I need to know the ID of the parent in // the source tree and the ID of the parent in the destination tree. So I want a // lookup table that maps source IDs to destination IDs. But is that sufficient? // Perhaps I can just keep a mapping from a source noderef to a destination ID. // I don't think I can keep more than one mutable destination node. let mut mapping: HashMap = HashMap::new(); mapping.insert(source_root_node.node_id(), dest_root_id); for source_node in source_root_node.traverse_level_order() { match source_node.parent() { None => {} Some(parent) => { let source_node_parent_id = parent.node_id(); let target_node_parent_id = mapping.get(&source_node_parent_id).expect("node should have been added to the source to dest mapping when being cloned"); let mut parent = dest.get_mut(*target_node_parent_id).expect( "destination parent node to exist before reaching potential children", ); let dest_id = parent.append(source_node.data().clone()).node_id(); mapping.insert(source_node.node_id(), dest_id); } } } Self(dest) } } } } impl Debug for GameTree { fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { self.write_formatted(f) } } impl Deref for GameTree { type Target = Tree; fn deref(&self) -> &Self::Target { &self.0 } } impl DerefMut for GameTree { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } impl PartialEq for GameTree { fn eq(&self, other: &Self) -> bool { // Get pre-order iterators over both trees, zip them, and ensure that the data contents are // the same between them let left_root = self.root(); let right_root = other.root(); match (left_root, right_root) { (Some(left_root), Some(right_root)) => { for (left_node, right_node) in std::iter::zip( left_root.traverse_pre_order(), right_root.traverse_pre_order(), ) { if left_node.data() != right_node.data() { return false; } } } (None, None) => return true, _ => return false, } true } } pub struct MainlineIter<'a> { next: Option>, tree: &'a Tree, } impl<'a> Iterator for MainlineIter<'a> { type Item = &'a GameNode; fn next(&mut self) -> Option { if let Some(next) = self.next.take() { let ret = self.tree.get(next.node_id())?; self.next = next .first_child() .and_then(|child| self.tree.get(child.node_id())); Some(ret.data()) } else { None } } } #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub enum GameNode { MoveNode(MoveNode), SetupNode(SetupNode), } impl fmt::Display for GameNode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { match self { GameNode::MoveNode(_) => write!(f, "MoveNode"), GameNode::SetupNode(_) => write!(f, "SetupNode"), } } } impl GameNode { pub fn id(&self) -> Uuid { match self { GameNode::MoveNode(node) => node.id, GameNode::SetupNode(node) => node.id, } } } impl TryFrom for GameNode { type Error = GameNodeError; fn try_from(n: parser::Node) -> Result { let move_node = MoveNode::try_from(n.clone()); let setup_node = SetupNode::try_from(n.clone()); match (move_node, setup_node) { (Ok(node), _) => Ok(Self::MoveNode(node)), (Err(_), Ok(node)) => Ok(Self::SetupNode(node)), (Err(move_err), Err(setup_err)) => { Err(Self::Error::UnsupportedGameNode(move_err, setup_err, n)) } } } } #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct MoveNode { pub id: Uuid, pub color: Color, pub mv: Move, 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, time_left: None, moves_left: None, name: None, evaluation: None, value: None, comments: None, annotation: None, unknown_props: vec![], } } } impl TryFrom 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, } 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, }) } } impl TryFrom 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() .expect("there should be a mainline in this file") .collect::>(); 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() .expect("there should be a mainline in this file") .collect::>(); 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]); } */ }, ); } #[test] fn it_can_load_a_file_with_multiple_roots() { with_file(std::path::Path::new("test_data/multi-tree.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.trees.len(), 2); assert_matches!(game.trees[0].root().unwrap().data(), GameNode::MoveNode(node) => { assert_eq!(node.color, Color::Black); assert_eq!(node.mv, Move::Move("pd".to_owned())); }); assert_matches!(game.trees[1].root().unwrap().data(), GameNode::MoveNode(node) => { assert_eq!(node.color, Color::Black); assert_eq!(node.mv, Move::Move("pc".to_owned())); }); }); } #[test] fn it_can_copy_a_game_record() { with_file(std::path::Path::new("test_data/multi-tree.sgf"), |games| { let dest = games.clone(); assert_eq!(games.len(), dest.len()); assert_eq!(games[0], dest[0]); }); } }