Compare commits
5 Commits
b9425af234
...
62b8e90c85
Author | SHA1 | Date |
---|---|---|
Savanni D'Gerinel | 62b8e90c85 | |
Savanni D'Gerinel | e630c062c2 | |
Savanni D'Gerinel | f28a64d9a6 | |
Savanni D'Gerinel | d94dd5245c | |
Savanni D'Gerinel | b2ba257cac |
|
@ -233,6 +233,15 @@ version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cool_asserts"
|
||||||
|
version = "2.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ee9f254e53f61e2688d3677fa2cbe4e9b950afd56f48819c98817417cf6b28ec"
|
||||||
|
dependencies = [
|
||||||
|
"indent_write",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "coordinates"
|
name = "coordinates"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -339,7 +348,7 @@ dependencies = [
|
||||||
"geo-types",
|
"geo-types",
|
||||||
"gio",
|
"gio",
|
||||||
"glib",
|
"glib",
|
||||||
"glib-build-tools",
|
"glib-build-tools 0.16.3",
|
||||||
"gtk4",
|
"gtk4",
|
||||||
"ifc",
|
"ifc",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
|
@ -841,6 +850,12 @@ version = "0.16.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "251935cb159350458a627642b0852a7fb8e027e3c5916dc2cebcd70f025de3fc"
|
checksum = "251935cb159350458a627642b0852a7fb8e027e3c5916dc2cebcd70f025de3fc"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "glib-build-tools"
|
||||||
|
version = "0.17.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7a65d79efe318ef2cbbbb37032b125866fd82c34ea44c816132621bbc552e716"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "glib-macros"
|
name = "glib-macros"
|
||||||
version = "0.17.10"
|
version = "0.17.10"
|
||||||
|
@ -900,6 +915,15 @@ dependencies = [
|
||||||
"system-deps",
|
"system-deps",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "grid"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0634107a3a005070dd73e27e74ecb691a94e9e5ba7829f434db7fbf73a6b5c47"
|
||||||
|
dependencies = [
|
||||||
|
"no-std-compat",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gsk4"
|
name = "gsk4"
|
||||||
version = "0.6.3"
|
version = "0.6.3"
|
||||||
|
@ -1048,7 +1072,7 @@ dependencies = [
|
||||||
"coordinates",
|
"coordinates",
|
||||||
"gio",
|
"gio",
|
||||||
"glib",
|
"glib",
|
||||||
"glib-build-tools",
|
"glib-build-tools 0.16.3",
|
||||||
"gtk4",
|
"gtk4",
|
||||||
"image",
|
"image",
|
||||||
]
|
]
|
||||||
|
@ -1185,6 +1209,12 @@ dependencies = [
|
||||||
"tiff",
|
"tiff",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indent_write"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0cfe9645a18782869361d9c8732246be7b410ad4e919d3609ebabdac00ba12c3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "1.9.3"
|
version = "1.9.3"
|
||||||
|
@ -1254,6 +1284,35 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "kifu-core"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"cool_asserts",
|
||||||
|
"grid",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"sgf",
|
||||||
|
"thiserror",
|
||||||
|
"typeshare",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "kifu-gtk"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"cairo-rs",
|
||||||
|
"gio",
|
||||||
|
"glib",
|
||||||
|
"glib-build-tools 0.17.10",
|
||||||
|
"gtk4",
|
||||||
|
"image",
|
||||||
|
"kifu-core",
|
||||||
|
"screenplay",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
|
@ -1419,6 +1478,12 @@ dependencies = [
|
||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "no-std-compat"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nom"
|
name = "nom"
|
||||||
version = "7.1.3"
|
version = "7.1.3"
|
||||||
|
@ -2083,6 +2148,7 @@ name = "sgf"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"cool_asserts",
|
||||||
"nom",
|
"nom",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
|
12
Cargo.toml
12
Cargo.toml
|
@ -1,16 +1,18 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
|
"changeset",
|
||||||
|
"coordinates",
|
||||||
"cyberpunk-splash",
|
"cyberpunk-splash",
|
||||||
"dashboard",
|
"dashboard",
|
||||||
|
"emseries",
|
||||||
|
"flow",
|
||||||
"fluent-ergonomics",
|
"fluent-ergonomics",
|
||||||
"geo-types",
|
"geo-types",
|
||||||
|
"hex-grid",
|
||||||
"ifc",
|
"ifc",
|
||||||
|
"kifu/core",
|
||||||
|
"kifu/gtk",
|
||||||
"memorycache",
|
"memorycache",
|
||||||
"screenplay",
|
"screenplay",
|
||||||
"emseries",
|
|
||||||
"coordinates",
|
|
||||||
"flow",
|
|
||||||
"sgf",
|
"sgf",
|
||||||
"changeset",
|
|
||||||
"hex-grid",
|
|
||||||
]
|
]
|
||||||
|
|
24
build.sh
24
build.sh
|
@ -3,19 +3,21 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
RUST_ALL_TARGETS=(
|
RUST_ALL_TARGETS=(
|
||||||
"dashboard"
|
|
||||||
"ifc"
|
|
||||||
"memorycache"
|
|
||||||
"geo-types"
|
|
||||||
"fluent-ergonomics"
|
|
||||||
"cyberpunk-splash"
|
|
||||||
"screenplay"
|
|
||||||
"emseries"
|
|
||||||
"coordinates"
|
|
||||||
"flow"
|
|
||||||
"sgf"
|
|
||||||
"changeset"
|
"changeset"
|
||||||
|
"coordinates"
|
||||||
|
"cyberpunk-splash"
|
||||||
|
"dashboard"
|
||||||
|
"emseries"
|
||||||
|
"flow"
|
||||||
|
"fluent-ergonomics"
|
||||||
|
"geo-types"
|
||||||
"hex-grid"
|
"hex-grid"
|
||||||
|
"ifc"
|
||||||
|
"kifu-core"
|
||||||
|
"kifu-gtk"
|
||||||
|
"memorycache"
|
||||||
|
"screenplay"
|
||||||
|
"sgf"
|
||||||
)
|
)
|
||||||
|
|
||||||
build_rust_targets() {
|
build_rust_targets() {
|
||||||
|
|
|
@ -7,8 +7,7 @@
|
||||||
;W[fd]
|
;W[fd]
|
||||||
;B[qf]
|
;B[qf]
|
||||||
;W[qh]
|
;W[qh]
|
||||||
(;B[pf]
|
(;B[of]
|
||||||
)(;B[of]
|
|
||||||
;W[nd]
|
;W[nd]
|
||||||
;B[mf]
|
;B[mf]
|
||||||
;W[pk]
|
;W[pk]
|
||||||
|
@ -221,4 +220,5 @@
|
||||||
;B[ai]
|
;B[ai]
|
||||||
;W[qg]
|
;W[qg]
|
||||||
;B[pf]
|
;B[pf]
|
||||||
|
)(;B[pf]
|
||||||
))
|
))
|
|
@ -1,3 +1,5 @@
|
||||||
|
use sgf::{go::Game, parse_sgf};
|
||||||
|
|
||||||
use crate::{BoardError, Color, Size};
|
use crate::{BoardError, Color, Size};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
@ -77,6 +79,41 @@ pub struct Coordinate {
|
||||||
pub row: u8,
|
pub row: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Coordinate {
|
||||||
|
fn from_sgf(s: &str) -> Self {
|
||||||
|
fn parse(s: char) -> u8 {
|
||||||
|
match s {
|
||||||
|
'a' => 0,
|
||||||
|
'b' => 1,
|
||||||
|
'c' => 2,
|
||||||
|
'd' => 3,
|
||||||
|
'e' => 4,
|
||||||
|
'f' => 5,
|
||||||
|
'g' => 6,
|
||||||
|
'h' => 7,
|
||||||
|
'i' => 8,
|
||||||
|
'j' => 9,
|
||||||
|
'k' => 10,
|
||||||
|
'l' => 11,
|
||||||
|
'm' => 12,
|
||||||
|
'n' => 13,
|
||||||
|
'o' => 14,
|
||||||
|
'p' => 15,
|
||||||
|
'q' => 16,
|
||||||
|
'r' => 17,
|
||||||
|
's' => 18,
|
||||||
|
_ => panic!("invalid character in the SGF coordinates"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let s = s.chars().collect::<Vec<char>>();
|
||||||
|
Coordinate {
|
||||||
|
column: parse(s[0]),
|
||||||
|
row: parse(s[1]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Board {
|
impl Board {
|
||||||
pub fn place_stone(mut self, coordinate: Coordinate, color: Color) -> Result<Self, BoardError> {
|
pub fn place_stone(mut self, coordinate: Coordinate, color: Color) -> Result<Self, BoardError> {
|
||||||
if let Some(_) = self.stone(&coordinate) {
|
if let Some(_) = self.stone(&coordinate) {
|
||||||
|
@ -123,17 +160,17 @@ impl Board {
|
||||||
.map(|g| g.color)
|
.map(|g| g.color)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn group(&self, coordinate: &Coordinate) -> Option<&Group> {
|
fn group(&self, coordinate: &Coordinate) -> Option<&Group> {
|
||||||
self.groups
|
self.groups
|
||||||
.iter()
|
.iter()
|
||||||
.find(|g| g.coordinates.contains(coordinate))
|
.find(|g| g.coordinates.contains(coordinate))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_group(&mut self, group: &Group) {
|
fn remove_group(&mut self, group: &Group) {
|
||||||
self.groups.retain(|g| g != group);
|
self.groups.retain(|g| g != group);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn adjacent_groups(&self, group: &Group) -> Vec<Group> {
|
fn adjacent_groups(&self, group: &Group) -> Vec<Group> {
|
||||||
let adjacent_spaces = self.group_halo(group).into_iter();
|
let adjacent_spaces = self.group_halo(group).into_iter();
|
||||||
let mut grps: Vec<Group> = Vec::new();
|
let mut grps: Vec<Group> = Vec::new();
|
||||||
|
|
||||||
|
@ -153,7 +190,7 @@ impl Board {
|
||||||
grps
|
grps
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn group_halo(&self, group: &Group) -> HashSet<Coordinate> {
|
fn group_halo(&self, group: &Group) -> HashSet<Coordinate> {
|
||||||
group
|
group
|
||||||
.coordinates
|
.coordinates
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -162,14 +199,14 @@ impl Board {
|
||||||
.collect::<HashSet<Coordinate>>()
|
.collect::<HashSet<Coordinate>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn liberties(&self, group: &Group) -> usize {
|
fn liberties(&self, group: &Group) -> usize {
|
||||||
self.group_halo(group)
|
self.group_halo(group)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|c| self.stone(&c) == None)
|
.filter(|c| self.stone(&c) == None)
|
||||||
.count()
|
.count()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn adjacencies(&self, coordinate: &Coordinate) -> Vec<Coordinate> {
|
fn adjacencies(&self, coordinate: &Coordinate) -> Vec<Coordinate> {
|
||||||
let mut v = Vec::new();
|
let mut v = Vec::new();
|
||||||
if coordinate.column > 0 {
|
if coordinate.column > 0 {
|
||||||
v.push(Coordinate {
|
v.push(Coordinate {
|
||||||
|
@ -194,7 +231,7 @@ impl Board {
|
||||||
v.into_iter().filter(|c| self.within_board(c)).collect()
|
v.into_iter().filter(|c| self.within_board(c)).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn within_board(&self, coordinate: &Coordinate) -> bool {
|
fn within_board(&self, coordinate: &Coordinate) -> bool {
|
||||||
coordinate.column < self.size.width && coordinate.row < self.size.height
|
coordinate.column < self.size.width && coordinate.row < self.size.height
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -211,9 +248,37 @@ impl Group {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&Game> for Board {
|
||||||
|
type Error = BoardError;
|
||||||
|
fn try_from(record: &Game) -> Result<Self, Self::Error> {
|
||||||
|
let mut board = Board::new();
|
||||||
|
let mut root = Some(&record.root);
|
||||||
|
while let Some(node) = root {
|
||||||
|
match (node.find_prop("B"), node.find_prop("W")) {
|
||||||
|
(Some(prop), _) => {
|
||||||
|
let coordinate = Coordinate::from_sgf(prop.values[0].as_ref());
|
||||||
|
board = board.place_stone(coordinate, Color::Black)?;
|
||||||
|
}
|
||||||
|
(None, Some(prop)) => {
|
||||||
|
let coordinate = Coordinate::from_sgf(prop.values[0].as_ref());
|
||||||
|
board = board.place_stone(coordinate, Color::White)?;
|
||||||
|
}
|
||||||
|
(None, None) => (),
|
||||||
|
};
|
||||||
|
|
||||||
|
root = node.next()
|
||||||
|
}
|
||||||
|
Ok(board)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
use std::{fs::File, io::Read};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use cool_asserts::assert_matches;
|
||||||
|
use sgf::{parse_sgf, Game};
|
||||||
|
|
||||||
/* Two players (Black and White) take turns and Black plays first
|
/* Two players (Black and White) take turns and Black plays first
|
||||||
* Stones are placed on the line intersections and not moved.
|
* Stones are placed on the line intersections and not moved.
|
||||||
|
@ -225,6 +290,25 @@ mod test {
|
||||||
* A stone placed in a suicidal position is legal if it captures other stones first.
|
* A stone placed in a suicidal position is legal if it captures other stones first.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
fn with_text(text: &str, f: impl FnOnce(Vec<sgf::go::Game>)) {
|
||||||
|
let games = parse_sgf(text)
|
||||||
|
.unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|game| match game {
|
||||||
|
Game::Go(g) => Some(g),
|
||||||
|
Game::Unsupported(_) => None,
|
||||||
|
})
|
||||||
|
.collect::<Vec<sgf::go::Game>>();
|
||||||
|
f(games);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_file(path: &std::path::Path, f: impl FnOnce(Vec<sgf::go::Game>)) {
|
||||||
|
let mut file = File::open(path).unwrap();
|
||||||
|
let mut text = String::new();
|
||||||
|
let _ = file.read_to_string(&mut text);
|
||||||
|
with_text(&text, f);
|
||||||
|
}
|
||||||
|
|
||||||
fn with_example_board(test: impl FnOnce(Board)) {
|
fn with_example_board(test: impl FnOnce(Board)) {
|
||||||
let board = Board::from_coordinates(
|
let board = Board::from_coordinates(
|
||||||
vec![
|
vec![
|
||||||
|
@ -630,4 +714,46 @@ mod test {
|
||||||
|
|
||||||
assert_eq!(board, b2);
|
assert_eq!(board, b2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn loads_board_from_sgf() {
|
||||||
|
with_file(
|
||||||
|
std::path::Path::new("fixtures/five_games/2022.10.05.sgf"),
|
||||||
|
|trees| {
|
||||||
|
let game = &trees[0];
|
||||||
|
let board = Board::try_from(game).expect("game to be valid");
|
||||||
|
assert_eq!(
|
||||||
|
board.stone(&Coordinate { column: 14, row: 5 }),
|
||||||
|
Some(Color::Black)
|
||||||
|
);
|
||||||
|
|
||||||
|
/* This game has a fork in the game tree here. This block verifies that we have
|
||||||
|
* reached the move right before the fork. */
|
||||||
|
let mut node = &game.root;
|
||||||
|
for _ in 0..8 {
|
||||||
|
node = node.next().unwrap();
|
||||||
|
}
|
||||||
|
let prop = assert_matches!(node.find_prop("W"), Some(prop) => prop);
|
||||||
|
assert_eq!(
|
||||||
|
prop,
|
||||||
|
sgf::Property {
|
||||||
|
ident: "W".to_owned(),
|
||||||
|
values: vec!["qh".to_owned()]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/* And now we verify that we have gone down the leftmost side of the fork, which is
|
||||||
|
* traditionally the mainline of the game. */
|
||||||
|
let node = node.next().unwrap();
|
||||||
|
let prop = assert_matches!(node.find_prop("B"), Some(prop) => prop);
|
||||||
|
assert_eq!(
|
||||||
|
prop,
|
||||||
|
sgf::Property {
|
||||||
|
ident: "B".to_owned(),
|
||||||
|
values: vec!["of".to_owned()]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,7 +88,7 @@ mod test {
|
||||||
assert_eq!(game.info.black_player, Some("Steve".to_owned()));
|
assert_eq!(game.info.black_player, Some("Steve".to_owned()));
|
||||||
assert_eq!(game.info.white_player, Some("Savanni".to_owned()));
|
assert_eq!(game.info.white_player, Some("Savanni".to_owned()));
|
||||||
assert_eq!(game.info.date, vec![Date::Date(chrono::NaiveDate::from_ymd_opt(2023, 4, 19).unwrap())]);
|
assert_eq!(game.info.date, vec![Date::Date(chrono::NaiveDate::from_ymd_opt(2023, 4, 19).unwrap())]);
|
||||||
assert_eq!(game.info.komi, Some(6.5));
|
// assert_eq!(game.info.komi, Some(6.5));
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,12 +21,12 @@ screenplay = { path = "../../screenplay" }
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
glib-build-tools = "0.17"
|
glib-build-tools = "0.17"
|
||||||
|
|
||||||
[[bin]]
|
# [[bin]]
|
||||||
name = "kifu-gtk"
|
# name = "kifu-gtk"
|
||||||
path = "src/main.rs"
|
# path = "src/main.rs"
|
||||||
|
|
||||||
[[bin]]
|
# [[bin]]
|
||||||
name = "screenplay"
|
# name = "screenplay"
|
||||||
path = "src/bin/screenplay.rs"
|
# path = "src/bin/screenplay.rs"
|
||||||
required-features = [ "screenplay" ]
|
# required-features = [ "screenplay" ]
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
{"Me":{"name":"Savanni","rank":{"Kyu":10}},"DatabasePath":"../core/fixtures/five_games"}
|
{"Me":{"name":"Savanni","rank":{"Kyu":10}},"DatabasePath":"kifu/core/fixtures/five_games"}
|
||||||
|
|
|
@ -11,3 +11,6 @@ nom = { version = "7" }
|
||||||
serde = { version = "1", features = [ "derive" ] }
|
serde = { version = "1", features = [ "derive" ] }
|
||||||
thiserror = { version = "1"}
|
thiserror = { version = "1"}
|
||||||
typeshare = { version = "1" }
|
typeshare = { version = "1" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
cool_asserts = { version = "2" }
|
||||||
|
|
174
sgf/src/go.rs
174
sgf/src/go.rs
|
@ -74,6 +74,7 @@ use crate::{
|
||||||
Error,
|
Error,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::ops::Deref;
|
||||||
use typeshare::typeshare;
|
use typeshare::typeshare;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
@ -84,11 +85,18 @@ pub struct Game {
|
||||||
pub tree: Tree,
|
pub tree: Tree,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Deref for Game {
|
||||||
|
type Target = Tree;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.tree
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl TryFrom<Tree> for Game {
|
impl TryFrom<Tree> for Game {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn try_from(tree: Tree) -> Result<Self, Self::Error> {
|
fn try_from(tree: Tree) -> Result<Self, Self::Error> {
|
||||||
let board_size = match tree.sequence[0].find_prop("SZ") {
|
let board_size = match tree.root.find_prop("SZ") {
|
||||||
Some(prop) => Size::try_from(prop.values[0].as_str())?,
|
Some(prop) => Size::try_from(prop.values[0].as_str())?,
|
||||||
None => Size {
|
None => Size {
|
||||||
width: 19,
|
width: 19,
|
||||||
|
@ -96,35 +104,34 @@ impl TryFrom<Tree> for Game {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let mut info = GameInfo::default();
|
let mut info = GameInfo::default();
|
||||||
info.app_name = tree.sequence[0]
|
info.app_name = tree.root.find_prop("AP").map(|prop| prop.values[0].clone());
|
||||||
.find_prop("AP")
|
info.black_player = tree.root.find_prop("PB").map(|prop| prop.values.join(", "));
|
||||||
.map(|prop| prop.values[0].clone());
|
|
||||||
info.black_player = tree.sequence[0]
|
|
||||||
.find_prop("PB")
|
|
||||||
.map(|prop| prop.values.join(", "));
|
|
||||||
|
|
||||||
info.black_rank = tree.sequence[0]
|
info.black_rank = tree
|
||||||
|
.root
|
||||||
.find_prop("BR")
|
.find_prop("BR")
|
||||||
.and_then(|prop| Rank::try_from(prop.values[0].as_str()).ok());
|
.and_then(|prop| Rank::try_from(prop.values[0].as_str()).ok());
|
||||||
|
|
||||||
info.white_player = tree.sequence[0]
|
info.white_player = tree.root.find_prop("PW").map(|prop| prop.values.join(", "));
|
||||||
.find_prop("PW")
|
|
||||||
.map(|prop| prop.values.join(", "));
|
|
||||||
|
|
||||||
info.white_rank = tree.sequence[0]
|
info.white_rank = tree
|
||||||
|
.root
|
||||||
.find_prop("WR")
|
.find_prop("WR")
|
||||||
.and_then(|prop| Rank::try_from(prop.values[0].as_str()).ok());
|
.and_then(|prop| Rank::try_from(prop.values[0].as_str()).ok());
|
||||||
|
|
||||||
info.result = tree.sequence[0]
|
info.result = tree
|
||||||
|
.root
|
||||||
.find_prop("RE")
|
.find_prop("RE")
|
||||||
.and_then(|prop| GameResult::try_from(prop.values[0].as_str()).ok());
|
.and_then(|prop| GameResult::try_from(prop.values[0].as_str()).ok());
|
||||||
|
|
||||||
info.time_limits = tree.sequence[0]
|
info.time_limits = tree
|
||||||
|
.root
|
||||||
.find_prop("TM")
|
.find_prop("TM")
|
||||||
.and_then(|prop| prop.values[0].parse::<u64>().ok())
|
.and_then(|prop| prop.values[0].parse::<u64>().ok())
|
||||||
.and_then(|seconds| Some(std::time::Duration::from_secs(seconds)));
|
.and_then(|seconds| Some(std::time::Duration::from_secs(seconds)));
|
||||||
|
|
||||||
info.date = tree.sequence[0]
|
info.date = tree
|
||||||
|
.root
|
||||||
.find_prop("DT")
|
.find_prop("DT")
|
||||||
.and_then(|prop| {
|
.and_then(|prop| {
|
||||||
let v = prop
|
let v = prop
|
||||||
|
@ -144,21 +151,13 @@ impl TryFrom<Tree> for Game {
|
||||||
})
|
})
|
||||||
.unwrap_or(vec![]);
|
.unwrap_or(vec![]);
|
||||||
|
|
||||||
info.event = tree.sequence[0]
|
info.event = tree.root.find_prop("EV").map(|prop| prop.values.join(", "));
|
||||||
.find_prop("EV")
|
|
||||||
.map(|prop| prop.values.join(", "));
|
|
||||||
|
|
||||||
info.round = tree.sequence[0]
|
info.round = tree.root.find_prop("RO").map(|prop| prop.values.join(", "));
|
||||||
.find_prop("RO")
|
|
||||||
.map(|prop| prop.values.join(", "));
|
|
||||||
|
|
||||||
info.source = tree.sequence[0]
|
info.source = tree.root.find_prop("SO").map(|prop| prop.values.join(", "));
|
||||||
.find_prop("SO")
|
|
||||||
.map(|prop| prop.values.join(", "));
|
|
||||||
|
|
||||||
info.game_keeper = tree.sequence[0]
|
info.game_keeper = tree.root.find_prop("US").map(|prop| prop.values.join(", "));
|
||||||
.find_prop("US")
|
|
||||||
.map(|prop| prop.values.join(", "));
|
|
||||||
|
|
||||||
Ok(Game {
|
Ok(Game {
|
||||||
board_size,
|
board_size,
|
||||||
|
@ -301,7 +300,7 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
date::Date,
|
date::Date,
|
||||||
tree::{parse_collection, Size},
|
tree::{parse_collection, Property, Size},
|
||||||
};
|
};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
@ -384,4 +383,123 @@ mod tests {
|
||||||
assert_eq!(tree.info.game_keeper, Some("Arno Hollosi".to_owned()));
|
assert_eq!(tree.info.game_keeper, Some("Arno Hollosi".to_owned()));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_presents_the_mainline_of_game_without_branches() {
|
||||||
|
with_file(
|
||||||
|
std::path::Path::new("test_data/2020 USGO DDK, Round 1.sgf"),
|
||||||
|
|trees| {
|
||||||
|
assert_eq!(trees.len(), 1);
|
||||||
|
let tree = &trees[0];
|
||||||
|
|
||||||
|
let node = &tree.root;
|
||||||
|
assert_eq!(node.properties.len(), 16);
|
||||||
|
let expected_properties = vec![
|
||||||
|
Property {
|
||||||
|
ident: "GM".to_owned(),
|
||||||
|
values: vec!["1".to_owned()],
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "FF".to_owned(),
|
||||||
|
values: vec!["4".to_owned()],
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "CA".to_owned(),
|
||||||
|
values: vec!["UTF-8".to_owned()],
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "AP".to_owned(),
|
||||||
|
values: vec!["CGoban:3".to_owned()],
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "ST".to_owned(),
|
||||||
|
values: vec!["2".to_owned()],
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "RU".to_owned(),
|
||||||
|
values: vec!["AGA".to_owned()],
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "SZ".to_owned(),
|
||||||
|
values: vec!["19".to_owned()],
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "KM".to_owned(),
|
||||||
|
values: vec!["7.50".to_owned()],
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "TM".to_owned(),
|
||||||
|
values: vec!["1800".to_owned()],
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "OT".to_owned(),
|
||||||
|
values: vec!["5x30 byo-yomi".to_owned()],
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "PW".to_owned(),
|
||||||
|
values: vec!["Geckoz".to_owned()],
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "PB".to_owned(),
|
||||||
|
values: vec!["savanni".to_owned()],
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "BR".to_owned(),
|
||||||
|
values: vec!["23k".to_owned()],
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "DT".to_owned(),
|
||||||
|
values: vec!["2020-08-05".to_owned()],
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "PC".to_owned(),
|
||||||
|
values: vec!["The KGS Go Server at http://www.gokgs.com/".to_owned()],
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "RE".to_owned(),
|
||||||
|
values: vec!["W+17.50".to_owned()],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for i in 0..16 {
|
||||||
|
assert_eq!(node.properties[i], expected_properties[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let node = node.next().unwrap();
|
||||||
|
let expected_properties = vec![
|
||||||
|
Property {
|
||||||
|
ident: "B".to_owned(),
|
||||||
|
values: vec!["pp".to_owned()],
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "BL".to_owned(),
|
||||||
|
values: vec!["1795.449".to_owned()],
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "C".to_owned(),
|
||||||
|
values: vec!["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()],
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
for i in 0..3 {
|
||||||
|
assert_eq!(node.properties[i], expected_properties[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ pub mod go;
|
||||||
|
|
||||||
mod tree;
|
mod tree;
|
||||||
use tree::parse_collection;
|
use tree::parse_collection;
|
||||||
|
pub use tree::Property;
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
@ -65,7 +66,7 @@ pub fn parse_sgf(input: &str) -> Result<Vec<Game>, Error> {
|
||||||
let (_, trees) = parse_collection::<nom::error::VerboseError<&str>>(input)?;
|
let (_, trees) = parse_collection::<nom::error::VerboseError<&str>>(input)?;
|
||||||
Ok(trees
|
Ok(trees
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|t| match t.sequence[0].find_prop("GM") {
|
.map(|t| match t.root.find_prop("GM") {
|
||||||
Some(prop) if prop.values == vec!["1".to_owned()] => {
|
Some(prop) if prop.values == vec!["1".to_owned()] => {
|
||||||
Game::Go(go::Game::try_from(t).expect("properly structured game tree"))
|
Game::Go(go::Game::try_from(t).expect("properly structured game tree"))
|
||||||
}
|
}
|
||||||
|
|
277
sgf/src/tree.rs
277
sgf/src/tree.rs
|
@ -5,7 +5,6 @@ use nom::{
|
||||||
character::complete::{alpha1, digit1, multispace0, multispace1, none_of},
|
character::complete::{alpha1, digit1, multispace0, multispace1, none_of},
|
||||||
combinator::{opt, value},
|
combinator::{opt, value},
|
||||||
multi::{many0, many1, separated_list1},
|
multi::{many0, many1, separated_list1},
|
||||||
sequence::delimited,
|
|
||||||
IResult,
|
IResult,
|
||||||
};
|
};
|
||||||
use std::num::ParseIntError;
|
use std::num::ParseIntError;
|
||||||
|
@ -54,29 +53,19 @@ impl TryFrom<&str> for Size {
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct Tree {
|
pub struct Tree {
|
||||||
pub sequence: Vec<Node>,
|
pub root: Node,
|
||||||
pub sub_sequences: Vec<Tree>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToString for Tree {
|
impl ToString for Tree {
|
||||||
fn to_string(&self) -> String {
|
fn to_string(&self) -> String {
|
||||||
let sequence = self
|
format!("({})", self.root.to_string())
|
||||||
.sequence
|
|
||||||
.iter()
|
|
||||||
.map(|node| node.to_string())
|
|
||||||
.collect::<String>();
|
|
||||||
let subsequences = self
|
|
||||||
.sub_sequences
|
|
||||||
.iter()
|
|
||||||
.map(|seq| seq.to_string())
|
|
||||||
.collect::<String>();
|
|
||||||
format!("({}{})", sequence, subsequences)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct Node {
|
pub struct Node {
|
||||||
pub properties: Vec<Property>,
|
pub properties: Vec<Property>,
|
||||||
|
pub next: Vec<Node>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToString for Node {
|
impl ToString for Node {
|
||||||
|
@ -86,7 +75,21 @@ impl ToString for Node {
|
||||||
.iter()
|
.iter()
|
||||||
.map(|prop| prop.to_string())
|
.map(|prop| prop.to_string())
|
||||||
.collect::<String>();
|
.collect::<String>();
|
||||||
format!(";{}", props)
|
|
||||||
|
let next = if self.next.len() == 1 {
|
||||||
|
self.next
|
||||||
|
.iter()
|
||||||
|
.map(|node| node.to_string())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join("")
|
||||||
|
} else {
|
||||||
|
self.next
|
||||||
|
.iter()
|
||||||
|
.map(|node| format!("({})", node.to_string()))
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join("")
|
||||||
|
};
|
||||||
|
format!(";{}{}", props, next)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,6 +100,10 @@ impl Node {
|
||||||
.find(|prop| prop.ident == ident)
|
.find(|prop| prop.ident == ident)
|
||||||
.cloned()
|
.cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn next<'a>(&'a self) -> Option<&'a Node> {
|
||||||
|
self.next.get(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
@ -119,40 +126,40 @@ impl ToString for Property {
|
||||||
pub fn parse_collection<'a, E: nom::error::ParseError<&'a str>>(
|
pub fn parse_collection<'a, E: nom::error::ParseError<&'a str>>(
|
||||||
input: &'a str,
|
input: &'a str,
|
||||||
) -> IResult<&'a str, Vec<Tree>, E> {
|
) -> IResult<&'a str, Vec<Tree>, E> {
|
||||||
separated_list1(multispace1, parse_tree)(input)
|
let (input, roots) = separated_list1(multispace1, parse_tree)(input)?;
|
||||||
|
let trees = roots
|
||||||
|
.into_iter()
|
||||||
|
.map(|root| Tree { root })
|
||||||
|
.collect::<Vec<Tree>>();
|
||||||
|
|
||||||
|
Ok((input, trees))
|
||||||
}
|
}
|
||||||
|
|
||||||
// note: must preserve unknown properties
|
// note: must preserve unknown properties
|
||||||
// note: must fix or preserve illegally formatted game-info properties
|
// note: must fix or preserve illegally formatted game-info properties
|
||||||
// note: must correct or delete illegally foramtted properties, but display a warning
|
// note: must correct or delete illegally foramtted properties, but display a warning
|
||||||
fn parse_tree<'a, E: nom::error::ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, Tree, E> {
|
fn parse_tree<'a, E: nom::error::ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, Node, E> {
|
||||||
let (input, _) = multispace0(input)?;
|
let (input, _) = multispace0(input)?;
|
||||||
delimited(tag("("), parse_sequence, tag(")"))(input)
|
let (input, _) = tag("(")(input)?;
|
||||||
}
|
let (input, node) = parse_node(input)?;
|
||||||
|
let (input, _) = multispace0(input)?;
|
||||||
|
let (input, _) = tag(")")(input)?;
|
||||||
|
|
||||||
fn parse_sequence<'a, E: nom::error::ParseError<&'a str>>(
|
Ok((input, node))
|
||||||
input: &'a str,
|
|
||||||
) -> IResult<&'a str, Tree, E> {
|
|
||||||
let (input, _) = multispace0(input)?;
|
|
||||||
let (input, nodes) = many1(parse_node)(input)?;
|
|
||||||
let (input, _) = multispace0(input)?;
|
|
||||||
let (input, sub_sequences) = many0(parse_tree)(input)?;
|
|
||||||
let (input, _) = multispace0(input)?;
|
|
||||||
|
|
||||||
Ok((
|
|
||||||
input,
|
|
||||||
Tree {
|
|
||||||
sequence: nodes,
|
|
||||||
sub_sequences,
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_node<'a, E: nom::error::ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, Node, E> {
|
fn parse_node<'a, E: nom::error::ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, Node, E> {
|
||||||
let (input, _) = multispace0(input)?;
|
let (input, _) = multispace0(input)?;
|
||||||
let (input, _) = tag(";")(input)?;
|
let (input, _) = opt(tag(";"))(input)?;
|
||||||
let (input, properties) = many1(parse_property)(input)?;
|
let (input, properties) = many1(parse_property)(input)?;
|
||||||
Ok((input, Node { properties }))
|
|
||||||
|
let (input, next) = opt(parse_node)(input)?;
|
||||||
|
let (input, mut next_seq) = many0(parse_tree)(input)?;
|
||||||
|
|
||||||
|
let mut next = next.map(|n| vec![n]).unwrap_or(vec![]);
|
||||||
|
next.append(&mut next_seq);
|
||||||
|
|
||||||
|
Ok((input, Node { properties, next }))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_property<'a, E: nom::error::ParseError<&'a str>>(
|
fn parse_property<'a, E: nom::error::ParseError<&'a str>>(
|
||||||
|
@ -219,8 +226,6 @@ pub fn parse_size<'a, E: nom::error::ParseError<&'a str>>(
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use std::{fs::File, io::Read};
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
const EXAMPLE: &'static str = "(;FF[4]C[root](;C[a];C[b](;C[c])
|
const EXAMPLE: &'static str = "(;FF[4]C[root](;C[a];C[b](;C[c])
|
||||||
|
@ -259,7 +264,8 @@ mod test {
|
||||||
properties: vec![Property {
|
properties: vec![Property {
|
||||||
ident: "B".to_owned(),
|
ident: "B".to_owned(),
|
||||||
values: vec!["ab".to_owned()]
|
values: vec!["ab".to_owned()]
|
||||||
}]
|
}],
|
||||||
|
next: vec![]
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -273,6 +279,25 @@ mod test {
|
||||||
properties: vec![Property {
|
properties: vec![Property {
|
||||||
ident: "B".to_owned(),
|
ident: "B".to_owned(),
|
||||||
values: vec!["ab".to_owned()]
|
values: vec!["ab".to_owned()]
|
||||||
|
}],
|
||||||
|
next: vec![Node {
|
||||||
|
properties: vec![Property {
|
||||||
|
ident: "W".to_owned(),
|
||||||
|
values: vec!["dp".to_owned()]
|
||||||
|
}],
|
||||||
|
next: vec![Node {
|
||||||
|
properties: vec![
|
||||||
|
Property {
|
||||||
|
ident: "B".to_owned(),
|
||||||
|
values: vec!["pq".to_owned()]
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "C".to_owned(),
|
||||||
|
values: vec!["some comments".to_owned()]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
next: vec![],
|
||||||
|
}]
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -286,21 +311,17 @@ mod test {
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
sequence,
|
sequence,
|
||||||
Tree {
|
|
||||||
sequence: vec![
|
|
||||||
Node {
|
Node {
|
||||||
properties: vec![Property {
|
properties: vec![Property {
|
||||||
ident: "B".to_owned(),
|
ident: "B".to_owned(),
|
||||||
values: vec!["ab".to_owned()]
|
values: vec!["ab".to_owned()]
|
||||||
}]
|
}],
|
||||||
},
|
next: vec![Node {
|
||||||
Node {
|
|
||||||
properties: vec![Property {
|
properties: vec![Property {
|
||||||
ident: "W".to_owned(),
|
ident: "W".to_owned(),
|
||||||
values: vec!["dp".to_owned()]
|
values: vec!["dp".to_owned()]
|
||||||
}]
|
}],
|
||||||
},
|
next: vec![Node {
|
||||||
Node {
|
|
||||||
properties: vec![
|
properties: vec![
|
||||||
Property {
|
Property {
|
||||||
ident: "B".to_owned(),
|
ident: "B".to_owned(),
|
||||||
|
@ -310,114 +331,158 @@ mod test {
|
||||||
ident: "C".to_owned(),
|
ident: "C".to_owned(),
|
||||||
values: vec!["some comments".to_owned()]
|
values: vec!["some comments".to_owned()]
|
||||||
}
|
}
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
sub_sequences: vec![],
|
next: vec![],
|
||||||
}
|
}]
|
||||||
|
}],
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn it_can_parse_a_sequence_with_subsequences() {
|
fn it_can_parse_a_branching_sequence() {
|
||||||
let text = "(;C[a];C[b](;C[c])(;C[d];C[e]))";
|
let text = "(;C[a];C[b](;C[c])(;C[d];C[e]))";
|
||||||
let (_, sequence) = parse_tree::<nom::error::VerboseError<&str>>(text).unwrap();
|
let (_, tree) = parse_tree::<nom::error::VerboseError<&str>>(text).unwrap();
|
||||||
|
|
||||||
let main_sequence = vec![
|
let expected = Node {
|
||||||
Node {
|
|
||||||
properties: vec![Property {
|
properties: vec![Property {
|
||||||
ident: "C".to_owned(),
|
ident: "C".to_owned(),
|
||||||
values: vec!["a".to_owned()],
|
values: vec!["a".to_owned()],
|
||||||
}],
|
}],
|
||||||
},
|
next: vec![Node {
|
||||||
Node {
|
|
||||||
properties: vec![Property {
|
properties: vec![Property {
|
||||||
ident: "C".to_owned(),
|
ident: "C".to_owned(),
|
||||||
values: vec!["b".to_owned()],
|
values: vec!["b".to_owned()],
|
||||||
}],
|
}],
|
||||||
},
|
next: vec![
|
||||||
];
|
Node {
|
||||||
let subsequence_1 = Tree {
|
|
||||||
sequence: vec![Node {
|
|
||||||
properties: vec![Property {
|
properties: vec![Property {
|
||||||
ident: "C".to_owned(),
|
ident: "C".to_owned(),
|
||||||
values: vec!["c".to_owned()],
|
values: vec!["c".to_owned()],
|
||||||
}],
|
}],
|
||||||
}],
|
next: vec![],
|
||||||
sub_sequences: vec![],
|
},
|
||||||
};
|
|
||||||
let subsequence_2 = Tree {
|
|
||||||
sequence: vec![
|
|
||||||
Node {
|
Node {
|
||||||
properties: vec![Property {
|
properties: vec![Property {
|
||||||
ident: "C".to_owned(),
|
ident: "C".to_owned(),
|
||||||
values: vec!["d".to_owned()],
|
values: vec!["d".to_owned()],
|
||||||
}],
|
}],
|
||||||
},
|
next: vec![Node {
|
||||||
Node {
|
|
||||||
properties: vec![Property {
|
properties: vec![Property {
|
||||||
ident: "C".to_owned(),
|
ident: "C".to_owned(),
|
||||||
values: vec!["e".to_owned()],
|
values: vec!["e".to_owned()],
|
||||||
}],
|
}],
|
||||||
|
next: vec![],
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
sub_sequences: vec![],
|
}],
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(tree, expected);
|
||||||
sequence,
|
|
||||||
Tree {
|
|
||||||
sequence: main_sequence,
|
|
||||||
sub_sequences: vec![subsequence_1, subsequence_2],
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn it_can_parse_example_1() {
|
fn it_can_parse_example_1() {
|
||||||
let (_, ex_tree) = parse_tree::<nom::error::VerboseError<&str>>(EXAMPLE).unwrap();
|
let (_, tree) = parse_tree::<nom::error::VerboseError<&str>>(EXAMPLE).unwrap();
|
||||||
assert_eq!(ex_tree.sequence.len(), 1);
|
|
||||||
|
|
||||||
assert_eq!(ex_tree.sequence[0].properties.len(), 2);
|
let j = Node {
|
||||||
assert_eq!(
|
properties: vec![Property {
|
||||||
ex_tree.sequence[0].properties[0],
|
ident: "C".to_owned(),
|
||||||
|
values: vec!["j".to_owned()],
|
||||||
|
}],
|
||||||
|
next: vec![],
|
||||||
|
};
|
||||||
|
let i = Node {
|
||||||
|
properties: vec![Property {
|
||||||
|
ident: "C".to_owned(),
|
||||||
|
values: vec!["i".to_owned()],
|
||||||
|
}],
|
||||||
|
next: vec![],
|
||||||
|
};
|
||||||
|
let h = Node {
|
||||||
|
properties: vec![Property {
|
||||||
|
ident: "C".to_owned(),
|
||||||
|
values: vec!["h".to_owned()],
|
||||||
|
}],
|
||||||
|
next: vec![i],
|
||||||
|
};
|
||||||
|
let g = Node {
|
||||||
|
properties: vec![Property {
|
||||||
|
ident: "C".to_owned(),
|
||||||
|
values: vec!["g".to_owned()],
|
||||||
|
}],
|
||||||
|
next: vec![h],
|
||||||
|
};
|
||||||
|
let f = Node {
|
||||||
|
properties: vec![Property {
|
||||||
|
ident: "C".to_owned(),
|
||||||
|
values: vec!["f".to_owned()],
|
||||||
|
}],
|
||||||
|
next: vec![g, j],
|
||||||
|
};
|
||||||
|
let e = Node {
|
||||||
|
properties: vec![Property {
|
||||||
|
ident: "C".to_owned(),
|
||||||
|
values: vec!["e".to_owned()],
|
||||||
|
}],
|
||||||
|
next: vec![],
|
||||||
|
};
|
||||||
|
let d = Node {
|
||||||
|
properties: vec![Property {
|
||||||
|
ident: "C".to_owned(),
|
||||||
|
values: vec!["d".to_owned()],
|
||||||
|
}],
|
||||||
|
next: vec![e],
|
||||||
|
};
|
||||||
|
let c = Node {
|
||||||
|
properties: vec![Property {
|
||||||
|
ident: "C".to_owned(),
|
||||||
|
values: vec!["c".to_owned()],
|
||||||
|
}],
|
||||||
|
next: vec![],
|
||||||
|
};
|
||||||
|
let b = Node {
|
||||||
|
properties: vec![Property {
|
||||||
|
ident: "C".to_owned(),
|
||||||
|
values: vec!["b".to_owned()],
|
||||||
|
}],
|
||||||
|
next: vec![c, d],
|
||||||
|
};
|
||||||
|
let a = Node {
|
||||||
|
properties: vec![Property {
|
||||||
|
ident: "C".to_owned(),
|
||||||
|
values: vec!["a".to_owned()],
|
||||||
|
}],
|
||||||
|
next: vec![b],
|
||||||
|
};
|
||||||
|
let expected = Node {
|
||||||
|
properties: vec![
|
||||||
Property {
|
Property {
|
||||||
ident: "FF".to_owned(),
|
ident: "FF".to_owned(),
|
||||||
values: vec!["4".to_owned()]
|
values: vec!["4".to_owned()],
|
||||||
}
|
},
|
||||||
);
|
Property {
|
||||||
assert_eq!(ex_tree.sub_sequences.len(), 2);
|
ident: "C".to_owned(),
|
||||||
|
values: vec!["root".to_owned()],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
next: vec![a, f],
|
||||||
|
};
|
||||||
|
|
||||||
assert_eq!(ex_tree.sub_sequences[0].sequence.len(), 2);
|
assert_eq!(tree, expected);
|
||||||
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]
|
#[test]
|
||||||
fn it_can_regenerate_the_tree() {
|
fn it_can_regenerate_the_tree() {
|
||||||
let (_, tree1) = parse_tree::<nom::error::VerboseError<&str>>(EXAMPLE).unwrap();
|
let (_, tree1) = parse_tree::<nom::error::VerboseError<&str>>(EXAMPLE).unwrap();
|
||||||
|
let tree1 = Tree { root: tree1 };
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tree1.to_string(),
|
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])))"
|
"(;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::<nom::error::VerboseError<&str>>(&tree1.to_string()).unwrap();
|
let (_, tree2) = parse_tree::<nom::error::VerboseError<&str>>(&tree1.to_string()).unwrap();
|
||||||
assert_eq!(tree1, tree2);
|
assert_eq!(tree1, Tree { root: tree2 });
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
Loading…
Reference in New Issue