Compare commits

..

2 Commits

5 changed files with 195 additions and 138 deletions

16
Cargo.lock generated
View File

@ -3687,6 +3687,7 @@ dependencies = [
"cool_asserts", "cool_asserts",
"nom", "nom",
"serde 1.0.193", "serde 1.0.193",
"slab_tree",
"thiserror", "thiserror",
"typeshare", "typeshare",
"uuid 0.8.2", "uuid 0.8.2",
@ -3760,12 +3761,27 @@ dependencies = [
"autocfg 1.1.0", "autocfg 1.1.0",
] ]
[[package]]
name = "slab_tree"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9e8b45abe77b7cab703054a11973cffe164c82c5ff5e211ae5a73af5e42e39f"
dependencies = [
"snowflake",
]
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.11.2" version = "1.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970"
[[package]]
name = "snowflake"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27207bb65232eda1f588cf46db2fee75c0808d557f6b3cf19a75f5d6d7c94df1"
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.4.10" version = "0.4.10"

View File

@ -9,6 +9,7 @@ edition = "2021"
chrono = { version = "0.4", features = [ "serde" ] } chrono = { version = "0.4", features = [ "serde" ] }
nom = { version = "7" } nom = { version = "7" }
serde = { version = "1", features = [ "derive" ] } serde = { version = "1", features = [ "derive" ] }
slab_tree = { version = "0.3" }
thiserror = { version = "1"} thiserror = { version = "1"}
typeshare = { version = "1" } typeshare = { version = "1" }
uuid = { version = "0.8", features = ["v4", "serde"] } uuid = { version = "0.8", features = ["v4", "serde"] }

View File

@ -3,7 +3,11 @@ use crate::{
Color, Date, GameResult, GameType, Color, Date, GameResult, GameType,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{collections::HashSet, time::Duration}; use slab_tree::{NodeId, NodeMut, NodeRef, Tree};
use std::{
collections::{HashSet, VecDeque},
time::Duration,
};
use uuid::Uuid; use uuid::Uuid;
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
@ -32,7 +36,7 @@ pub enum SetupNodeError {
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum GameNodeError { pub enum GameNodeError {
UnsupportedGameNode(MoveNodeError, SetupNodeError), UnsupportedGameNode(MoveNodeError, SetupNodeError, parser::Node),
ConflictingProperty, ConflictingProperty,
ConflictingPosition, ConflictingPosition,
} }
@ -52,7 +56,6 @@ pub struct Player {
/// syntax issues, the result of the GameRecord is to have a fully-understood game. However, this /// 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 /// doesn't (yet?) go quite to the level of apply the game type (i.e., this is Go, Chess, Yinsh, or
/// whatever). /// whatever).
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct GameRecord { pub struct GameRecord {
pub game_type: GameType, pub game_type: GameType,
@ -78,7 +81,7 @@ pub struct GameRecord {
pub overtime: Option<String>, pub overtime: Option<String>,
pub transcriber: Option<String>, pub transcriber: Option<String>,
pub children: Vec<GameNode>, pub trees: Vec<Tree<GameNode>>,
} }
impl GameRecord { impl GameRecord {
@ -111,55 +114,41 @@ impl GameRecord {
overtime: None, overtime: None,
transcriber: None, transcriber: None,
children: vec![], trees: vec![],
} }
} }
pub fn nodes(&self) -> Vec<&GameNode> {
self.iter().collect()
}
pub fn iter(&self) -> impl Iterator<Item = &'_ GameNode> {
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 /// 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 /// was actually played out, and by convention consists of the first node in each list of
/// children. /// children.
pub fn mainline(&self) -> Vec<&GameNode> { pub fn mainline(&self) -> Option<impl Iterator<Item = &'_ GameNode>> {
let mut moves: Vec<&GameNode> = vec![]; println!("number of trees: {}", self.trees.len());
if self.trees.len() > 0 {
let mut next = self.children.first(); Some(MainlineIter {
while let Some(node) = next { next: self.trees[0].root(),
// Given that I know that I have a node, and I know that I'm going to push a reference tree: &self.trees[0],
// 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. } else {
// None
// 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 { impl TryFrom<parser::Tree> for GameRecord {
fn children<'a>(&'a self) -> Vec<&'a GameNode> {
self.children.iter().collect::<Vec<&'a GameNode>>()
}
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; type Error = GameError;
fn try_from(tree: &parser::Tree) -> Result<Self, Self::Error> { fn try_from(tree: parser::Tree) -> Result<Self, Self::Error> {
let mut ty = None; let mut ty = None;
let mut size = None; let mut size = None;
let mut black_player = Player { let mut black_player = Player {
@ -234,6 +223,7 @@ impl TryFrom<&parser::Tree> for GameRecord {
} }
} }
/*
s.children = tree s.children = tree
.root .root
.next .next
@ -241,35 +231,100 @@ impl TryFrom<&parser::Tree> for GameRecord {
.map(GameNode::try_from) .map(GameNode::try_from)
.collect::<Result<Vec<GameNode>, GameNodeError>>() .collect::<Result<Vec<GameNode>, GameNodeError>>()
.map_err(GameError::InvalidGameNode)?; .map_err(GameError::InvalidGameNode)?;
*/
s.trees = tree.root.next.into_iter()
.map(recursive_tree_to_slab_tree)
.collect::<Result<Vec<Tree<GameNode>>, GameError>>()?;
Ok(s) Ok(s)
} }
} }
fn recursive_tree_to_slab_tree(node: parser::Node) -> Result<Tree<GameNode>, GameError> {
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<GameNode> = 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(slab)
}
pub struct TreeIter<'a> {
queue: VecDeque<NodeRef<'a, &'a GameNode>>,
}
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<Self::Item> {
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 MainlineIter<'a> {
next: Option<NodeRef<'a, GameNode>>,
tree: &'a Tree<GameNode>,
}
impl<'a> Iterator for MainlineIter<'a> {
type Item = &'a GameNode;
fn next(&mut self) -> Option<Self::Item> {
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)] #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub enum GameNode { pub enum GameNode {
MoveNode(MoveNode), MoveNode(MoveNode),
SetupNode(SetupNode), 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::<Vec<&'a GameNode>>()
}
fn children(&self) -> Vec<&GameNode>;
fn add_child(&mut self, node: GameNode) -> &mut GameNode;
}
impl GameNode { impl GameNode {
pub fn id(&self) -> Uuid { pub fn id(&self) -> Uuid {
match self { match self {
@ -279,33 +334,10 @@ impl GameNode {
} }
} }
impl Node for GameNode { impl TryFrom<parser::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; type Error = GameNodeError;
fn try_from(n: &parser::Node) -> Result<Self, Self::Error> { fn try_from(n: parser::Node) -> Result<Self, Self::Error> {
// I originally wrote this recursively. However, on an ordinary game of a couple hundred // 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. // 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, instead, I need to unroll everything to non-recursive form.
@ -314,35 +346,29 @@ impl TryFrom<&parser::Node> for GameNode {
// only use the MoveNode::try_from and SetupNode::try_from if those functions don't // 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 // recurse. Instead, I'm going to process just that node, then return to here and process
// the children. // the children.
let move_node = MoveNode::try_from(n); let move_node = MoveNode::try_from(n.clone());
let setup_node = SetupNode::try_from(n); let setup_node = SetupNode::try_from(n.clone());
// I'm much too tired when writing this. I'm still recursing, but I did cut the number of // 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 // 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. // 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 // Iterate over that line, don't recurse. Create bookmarks at each branch point, and then
// come back to each one. // come back to each one.
/*
let children = n let children = n
.next .next
.iter() .iter()
.map(GameNode::try_from) .map(GameNode::try_from)
.collect::<Result<Vec<Self>, Self::Error>>()?; .collect::<Result<Vec<Self>, Self::Error>>()?;
*/
let node = match (move_node, setup_node) { match (move_node, setup_node) {
(Ok(mut node), _) => { (Ok(mut node), _) => Ok(Self::MoveNode(node)),
node.children = children; (Err(_), Ok(mut node)) => Ok(Self::SetupNode(node)),
Ok(Self::MoveNode(node))
}
(Err(_), Ok(mut node)) => {
node.children = children;
Ok(Self::SetupNode(node))
}
(Err(move_err), Err(setup_err)) => { (Err(move_err), Err(setup_err)) => {
Err(Self::Error::UnsupportedGameNode(move_err, setup_err)) Err(Self::Error::UnsupportedGameNode(move_err, setup_err, n))
}
} }
}?;
Ok(node)
} }
} }
@ -351,7 +377,6 @@ pub struct MoveNode {
pub id: Uuid, pub id: Uuid,
pub color: Color, pub color: Color,
pub mv: Move, pub mv: Move,
pub children: Vec<GameNode>,
pub time_left: Option<Duration>, pub time_left: Option<Duration>,
pub moves_left: Option<usize>, pub moves_left: Option<usize>,
@ -369,7 +394,6 @@ impl MoveNode {
id: Uuid::new_v4(), id: Uuid::new_v4(),
color, color,
mv, mv,
children: Vec::new(),
time_left: None, time_left: None,
moves_left: None, moves_left: None,
@ -383,21 +407,10 @@ impl MoveNode {
} }
} }
impl Node for MoveNode { impl TryFrom<parser::Node> for MoveNode {
fn children<'a>(&'a self) -> Vec<&'a GameNode> {
self.children.iter().collect::<Vec<&'a GameNode>>()
}
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; type Error = MoveNodeError;
fn try_from(n: &parser::Node) -> Result<Self, Self::Error> { fn try_from(n: parser::Node) -> Result<Self, Self::Error> {
let s = match n.mv() { let s = match n.mv() {
Some((color, mv)) => { Some((color, mv)) => {
let mut s = Self::new(color, mv); let mut s = Self::new(color, mv);
@ -460,7 +473,6 @@ pub struct SetupNode {
id: Uuid, id: Uuid,
pub positions: Vec<parser::SetupInstr>, pub positions: Vec<parser::SetupInstr>,
pub children: Vec<GameNode>,
} }
impl SetupNode { impl SetupNode {
@ -480,26 +492,14 @@ impl SetupNode {
Ok(Self { Ok(Self {
id: Uuid::new_v4(), id: Uuid::new_v4(),
positions, positions,
children: Vec::new(),
}) })
} }
} }
impl Node for SetupNode { impl TryFrom<parser::Node> for SetupNode {
fn children<'a>(&'a self) -> Vec<&'a GameNode> {
self.children.iter().collect::<Vec<&'a GameNode>>()
}
#[allow(dead_code)]
fn add_child(&mut self, _node: GameNode) -> &mut GameNode {
unimplemented!()
}
}
impl TryFrom<&parser::Node> for SetupNode {
type Error = SetupNodeError; type Error = SetupNodeError;
fn try_from(n: &parser::Node) -> Result<Self, Self::Error> { fn try_from(n: parser::Node) -> Result<Self, Self::Error> {
match n.setup() { match n.setup() {
Some(elements) => Self::new(elements), Some(elements) => Self::new(elements),
None => Err(Self::Error::NotASetupNode), None => Err(Self::Error::NotASetupNode),
@ -507,6 +507,7 @@ impl TryFrom<&parser::Node> for SetupNode {
} }
} }
/*
#[allow(dead_code)] #[allow(dead_code)]
pub fn path_to_node(node: &GameNode, id: Uuid) -> Vec<&GameNode> { pub fn path_to_node(node: &GameNode, id: Uuid) -> Vec<&GameNode> {
if node.id() == id { if node.id() == id {
@ -523,6 +524,7 @@ pub fn path_to_node(node: &GameNode, id: Uuid) -> Vec<&GameNode> {
Vec::new() Vec::new()
} }
*/
#[cfg(test)] #[cfg(test)]
mod test { mod test {
@ -555,15 +557,19 @@ mod test {
Player::default(), Player::default(),
); );
/*
let first_move = MoveNode::new(Color::Black, Move::Move("dd".to_owned())); let first_move = MoveNode::new(Color::Black, Move::Move("dd".to_owned()));
let first_ = game.add_child(GameNode::MoveNode(first_move.clone())); let first_ = game.add_child(GameNode::MoveNode(first_move.clone()));
let second_move = MoveNode::new(Color::White, Move::Move("qq".to_owned())); let second_move = MoveNode::new(Color::White, Move::Move("qq".to_owned()));
first_.add_child(GameNode::MoveNode(second_move.clone())); first_.add_child(GameNode::MoveNode(second_move.clone()));
*/
/*
let nodes = game.nodes(); let nodes = game.nodes();
assert_eq!(nodes.len(), 2); assert_eq!(nodes.len(), 2);
assert_eq!(nodes[0].id(), first_move.id); assert_eq!(nodes[0].id(), first_move.id);
assert_eq!(nodes[1].id(), second_move.id); assert_eq!(nodes[1].id(), second_move.id);
*/
} }
#[ignore] #[ignore]
@ -588,7 +594,7 @@ mod test {
], ],
next: vec![], next: vec![],
}; };
assert_matches!(GameNode::try_from(&n), Ok(GameNode::MoveNode(_))); assert_matches!(GameNode::try_from(n), Ok(GameNode::MoveNode(_)));
} }
} }
@ -630,10 +636,10 @@ mod move_node_tests {
], ],
next: vec![], next: vec![],
}; };
assert_matches!(MoveNode::try_from(&n), Ok(node) => { assert_matches!(MoveNode::try_from(n), Ok(node) => {
assert_eq!(node.color, Color::White); assert_eq!(node.color, Color::White);
assert_eq!(node.mv, Move::Move("dp".to_owned())); assert_eq!(node.mv, Move::Move("dp".to_owned()));
assert_eq!(node.children, vec![]); // assert_eq!(node.children, vec![]);
assert_eq!(node.time_left, Some(Duration::from_secs(176))); assert_eq!(node.time_left, Some(Duration::from_secs(176)));
assert_eq!(node.comments, Some("Comments in the game".to_owned())); assert_eq!(node.comments, Some("Comments in the game".to_owned()));
}); });
@ -653,7 +659,7 @@ mod move_node_tests {
next: vec![], next: vec![],
}; };
assert_matches!( assert_matches!(
MoveNode::try_from(&n), MoveNode::try_from(n),
Err(MoveNodeError::IncompatibleProperty(_)) Err(MoveNodeError::IncompatibleProperty(_))
); );
} }
@ -703,7 +709,7 @@ mod path_test {
let (_, games) = parse_collection::<nom::error::VerboseError<&str>>(text).unwrap(); let (_, games) = parse_collection::<nom::error::VerboseError<&str>>(text).unwrap();
let games = games let games = games
.into_iter() .into_iter()
.map(|game| GameRecord::try_from(&game).expect("game to parse")) .map(|game| GameRecord::try_from(game).expect("game to parse"))
.collect::<Vec<GameRecord>>(); .collect::<Vec<GameRecord>>();
f(games); f(games);
} }
@ -722,7 +728,10 @@ mod path_test {
|games| { |games| {
let game = &games[0]; let game = &games[0];
let moves = game.mainline(); let moves = game
.mainline()
.expect("there should be a mainline in this file")
.collect::<Vec<&GameNode>>();
assert_matches!(moves[0], GameNode::MoveNode(node) => { assert_matches!(moves[0], GameNode::MoveNode(node) => {
assert_eq!(node.color, Color::Black); assert_eq!(node.color, Color::Black);
assert_eq!(node.mv, Move::Move("pp".to_owned())); assert_eq!(node.mv, Move::Move("pp".to_owned()));
@ -744,7 +753,10 @@ mod path_test {
with_file(std::path::Path::new("test_data/branch_test.sgf"), |games| { with_file(std::path::Path::new("test_data/branch_test.sgf"), |games| {
let game = &games[0]; let game = &games[0];
let moves = game.mainline(); let moves = game
.mainline()
.expect("there should be a mainline in this file")
.collect::<Vec<&GameNode>>();
assert_matches!(moves[1], GameNode::MoveNode(node) => { assert_matches!(moves[1], GameNode::MoveNode(node) => {
assert_eq!(node.color, Color::White); assert_eq!(node.color, Color::White);
assert_eq!(node.mv, Move::Move("dd".to_owned())); assert_eq!(node.mv, Move::Move("dd".to_owned()));
@ -791,7 +803,7 @@ mod file_test {
let (_, games) = parse_collection::<nom::error::VerboseError<&str>>(text).unwrap(); let (_, games) = parse_collection::<nom::error::VerboseError<&str>>(text).unwrap();
let games = games let games = games
.into_iter() .into_iter()
.map(|game| GameRecord::try_from(&game).expect("game to parse")) .map(|game| GameRecord::try_from(game).expect("game to parse"))
.collect::<Vec<GameRecord>>(); .collect::<Vec<GameRecord>>();
f(games); f(games);
} }
@ -875,6 +887,7 @@ mod file_test {
} }
*/ */
/*
let children = game.children(); let children = game.children();
let node = children.first().unwrap(); let node = children.first().unwrap();
assert_matches!(node, GameNode::MoveNode(node) => { assert_matches!(node, GameNode::MoveNode(node) => {
@ -892,6 +905,7 @@ mod file_test {
assert_eq!(node.time_left, Some(Duration::from_secs(1765))); assert_eq!(node.time_left, Some(Duration::from_secs(1765)));
assert_eq!(node.comments, None); assert_eq!(node.comments, None);
}); });
*/
/* /*
let node = node.next().unwrap(); let node = node.next().unwrap();
let expected_properties = vec![ let expected_properties = vec![
@ -911,4 +925,29 @@ mod file_test {
}, },
); );
} }
#[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()));
});
});
}
} }

View File

@ -73,7 +73,7 @@ pub fn parse_sgf(input: &str) -> Result<Vec<Result<GameRecord, game::GameError>>
let (_, games) = parse_collection::<nom::error::VerboseError<&str>>(input)?; let (_, games) = parse_collection::<nom::error::VerboseError<&str>>(input)?;
let games = games let games = games
.into_iter() .into_iter()
.map(|game| GameRecord::try_from(&game)) .map(|game| GameRecord::try_from(game))
.collect::<Vec<Result<GameRecord, game::GameError>>>(); .collect::<Vec<Result<GameRecord, game::GameError>>>();
Ok(games) Ok(games)

View File

@ -0,0 +1 @@
(;GM[1]FF[4]CA[UTF-8]AP[Sabaki:0.52.2]KM[7.5]SZ[19]DT[2024-04-19](;B[pd](;W[qc];B[qd];W[pc];B[oc];W[ob];B[nc];W[nb];B[mc];W[rd];B[re];W[rc];B[qf])(;W[qf];B[nc];W[rd];B[qc];W[pi]))(;B[pc];W[qe];B[oe];W[pg];B[ld];W[qj]))