From 6fc2487937befcbc9a63718206152cad95f0750c Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sun, 24 Mar 2024 15:50:59 -0400 Subject: [PATCH] Return the mainline of a game that has no branches in it. --- sgf/src/game.rs | 74 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/sgf/src/game.rs b/sgf/src/game.rs index 4a983a2..1f6d0f1 100644 --- a/sgf/src/game.rs +++ b/sgf/src/game.rs @@ -114,6 +114,35 @@ impl GameRecord { 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<'a>(&'a self) -> Vec<&'a GameNode> { + let mut moves: Vec<&GameNode> = vec![]; + + let mut next = self.children.get(0); + 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.get(0), + GameNode::SetupNode(node) => node.children.get(0), + }; + } + + moves + } } impl Node for GameRecord { @@ -666,6 +695,51 @@ mod setup_node_tests { #[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())); + }); + }, + ) + } + #[ignore] #[test] fn returns_empty_list_if_no_game_nodes() {