Set up a per-game interpretation layer atop the SGF, and bind all games together in a Game
data structure #49
12
Makefile
12
Makefile
|
@ -1,4 +1,10 @@
|
||||||
|
|
||||||
|
all: test bin
|
||||||
|
|
||||||
|
test: kifu-core/test-oneshot sgf/test-oneshot
|
||||||
|
|
||||||
|
bin: kifu-gtk
|
||||||
|
|
||||||
changeset-dev:
|
changeset-dev:
|
||||||
cd changeset && make dev
|
cd changeset && make dev
|
||||||
|
|
||||||
|
@ -58,3 +64,9 @@ kifu-pwa/dev:
|
||||||
|
|
||||||
kifu-pwa/server:
|
kifu-pwa/server:
|
||||||
pushd kifu/pwa && make server
|
pushd kifu/pwa && make server
|
||||||
|
|
||||||
|
sgf/test:
|
||||||
|
pushd sgf && make test
|
||||||
|
|
||||||
|
sgf/test-oneshot:
|
||||||
|
pushd sgf && make test-oneshot
|
||||||
|
|
|
@ -1,47 +0,0 @@
|
||||||
mod date;
|
|
||||||
pub use date::Date;
|
|
||||||
|
|
||||||
mod go;
|
|
||||||
pub use go::{parse_sgf, GameTree, GameType, Rank};
|
|
||||||
|
|
||||||
mod tree;
|
|
||||||
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
pub enum Warning {}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Error)]
|
|
||||||
pub enum ParseError {
|
|
||||||
#[error("An unknown error was found")]
|
|
||||||
NomError(nom::error::Error<String>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<nom::error::Error<&str>> for ParseError {
|
|
||||||
fn from(err: nom::error::Error<&str>) -> Self {
|
|
||||||
Self::NomError(nom::error::Error {
|
|
||||||
input: err.input.to_owned(),
|
|
||||||
code: err.code.clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
impl From<(&str, VerboseErrorKind)> for
|
|
||||||
|
|
||||||
impl From<nom::error::VerboseError<&str>> for ParseError {
|
|
||||||
fn from(err: nom::error::VerboseError<&str>) -> Self {
|
|
||||||
Self::NomErrors(
|
|
||||||
err.errors
|
|
||||||
.into_iter()
|
|
||||||
.map(|err| ParseError::from(err))
|
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
/*
|
|
||||||
Self::NomError(nom::error::Error {
|
|
||||||
input: err.input.to_owned(),
|
|
||||||
code: err.code.clone(),
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
|
@ -120,17 +120,6 @@ dependencies = [
|
||||||
"syn 2.0.12",
|
"syn 2.0.12",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "go-sgf"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"chrono",
|
|
||||||
"nom",
|
|
||||||
"serde",
|
|
||||||
"thiserror",
|
|
||||||
"typeshare",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "grid"
|
name = "grid"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
@ -191,10 +180,10 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"cool_asserts",
|
"cool_asserts",
|
||||||
"go-sgf",
|
|
||||||
"grid",
|
"grid",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sgf",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"typeshare",
|
"typeshare",
|
||||||
]
|
]
|
||||||
|
@ -337,6 +326,17 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sgf"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"nom",
|
||||||
|
"serde",
|
||||||
|
"thiserror",
|
||||||
|
"typeshare",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.109"
|
version = "1.0.109"
|
||||||
|
|
|
@ -7,7 +7,7 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chrono = { version = "0.4" }
|
chrono = { version = "0.4" }
|
||||||
go-sgf = { path = "../../go-sgf" }
|
sgf = { path = "../../sgf" }
|
||||||
grid = { version = "0.9" }
|
grid = { version = "0.9" }
|
||||||
serde_json = { version = "1" }
|
serde_json = { version = "1" }
|
||||||
serde = { version = "1", features = [ "derive" ] }
|
serde = { version = "1", features = [ "derive" ] }
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use std::{ffi::OsStr, io::Read, os::unix::ffi::OsStrExt, path::PathBuf};
|
use std::{ffi::OsStr, io::Read, os::unix::ffi::OsStrExt, path::PathBuf};
|
||||||
|
|
||||||
use go_sgf::{parse_sgf, GameTree};
|
use sgf::{go, parse_sgf, Game};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
|
@ -20,12 +20,12 @@ impl From<std::io::Error> for Error {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Database {
|
pub struct Database {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
games: Vec<GameTree>,
|
games: Vec<go::Game>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Database {
|
impl Database {
|
||||||
pub fn open_path(path: PathBuf) -> Result<Database, Error> {
|
pub fn open_path(path: PathBuf) -> Result<Database, Error> {
|
||||||
let mut games: Vec<GameTree> = Vec::new();
|
let mut games: Vec<go::Game> = Vec::new();
|
||||||
|
|
||||||
let extension = PathBuf::from("sgf").into_os_string();
|
let extension = PathBuf::from("sgf").into_os_string();
|
||||||
|
|
||||||
|
@ -39,8 +39,12 @@ impl Database {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.read_to_string(&mut buffer)
|
.read_to_string(&mut buffer)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let sgf = parse_sgf(&buffer).unwrap();
|
for sgf in parse_sgf(&buffer).unwrap() {
|
||||||
games.extend(sgf);
|
match sgf {
|
||||||
|
Game::Go(game) => games.push(game),
|
||||||
|
Game::Unsupported(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => println!("failed entry: {:?}", err),
|
Err(err) => println!("failed entry: {:?}", err),
|
||||||
|
@ -54,7 +58,7 @@ impl Database {
|
||||||
self.games.len()
|
self.games.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn all_games(&self) -> impl Iterator<Item = &GameTree> {
|
pub fn all_games(&self) -> impl Iterator<Item = &go::Game> {
|
||||||
self.games.iter()
|
self.games.iter()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,7 +67,7 @@ impl Database {
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use cool_asserts::assert_matches;
|
use cool_asserts::assert_matches;
|
||||||
use go_sgf::{Date, GameType};
|
use sgf::Date;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn it_reads_empty_database() {
|
fn it_reads_empty_database() {
|
||||||
|
@ -77,9 +81,7 @@ mod test {
|
||||||
let db =
|
let db =
|
||||||
Database::open_path(PathBuf::from("fixtures/five_games/")).expect("database to open");
|
Database::open_path(PathBuf::from("fixtures/five_games/")).expect("database to open");
|
||||||
assert_eq!(db.all_games().count(), 5);
|
assert_eq!(db.all_games().count(), 5);
|
||||||
for game in db.all_games() {
|
for game in db.all_games() {}
|
||||||
assert_eq!(game.game_type, GameType::Go);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_matches!(db.all_games().find(|g| g.info.black_player == Some("Steve".to_owned())),
|
assert_matches!(db.all_games().find(|g| g.info.black_player == Some("Steve".to_owned())),
|
||||||
Some(game) => {
|
Some(game) => {
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
use go_sgf::{Date, GameTree, Rank};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sgf::{
|
||||||
|
go::{Game, Rank},
|
||||||
|
Date,
|
||||||
|
};
|
||||||
use typeshare::typeshare;
|
use typeshare::typeshare;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
@ -13,7 +16,7 @@ pub struct GamePreviewElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GamePreviewElement {
|
impl GamePreviewElement {
|
||||||
pub fn new(game: &GameTree) -> GamePreviewElement {
|
pub fn new(game: &Game) -> GamePreviewElement {
|
||||||
GamePreviewElement {
|
GamePreviewElement {
|
||||||
date: game.info.date.clone(),
|
date: game.info.date.clone(),
|
||||||
black_player: game
|
black_player: game
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::ui::{Action, GamePreviewElement};
|
use crate::ui::{Action, GamePreviewElement};
|
||||||
use go_sgf::GameTree;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sgf::go::Game;
|
||||||
use typeshare::typeshare;
|
use typeshare::typeshare;
|
||||||
|
|
||||||
fn rank_strings() -> Vec<String> {
|
fn rank_strings() -> Vec<String> {
|
||||||
|
@ -56,7 +56,7 @@ pub struct HomeView {
|
||||||
pub start_game: Action<()>,
|
pub start_game: Action<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn home<'a>(games: impl Iterator<Item = &'a GameTree>) -> HomeView {
|
pub fn home<'a>(games: impl Iterator<Item = &'a Game>) -> HomeView {
|
||||||
let black_player = PlayerElement::Hotseat(HotseatPlayerElement {
|
let black_player = PlayerElement::Hotseat(HotseatPlayerElement {
|
||||||
placeholder: Some("black player".to_owned()),
|
placeholder: Some("black player".to_owned()),
|
||||||
default_rank: None,
|
default_rank: None,
|
||||||
|
|
|
@ -545,17 +545,6 @@ dependencies = [
|
||||||
"system-deps",
|
"system-deps",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "go-sgf"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"chrono",
|
|
||||||
"nom",
|
|
||||||
"serde",
|
|
||||||
"thiserror",
|
|
||||||
"typeshare",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gobject-sys"
|
name = "gobject-sys"
|
||||||
version = "0.17.4"
|
version = "0.17.4"
|
||||||
|
@ -799,10 +788,10 @@ name = "kifu-core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"go-sgf",
|
|
||||||
"grid",
|
"grid",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sgf",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"typeshare",
|
"typeshare",
|
||||||
]
|
]
|
||||||
|
@ -1237,6 +1226,17 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sgf"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"nom",
|
||||||
|
"serde",
|
||||||
|
"thiserror",
|
||||||
|
"typeshare",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "signal-hook-registry"
|
||||||
version = "1.4.1"
|
version = "1.4.1"
|
||||||
|
|
|
@ -63,17 +63,6 @@ version = "0.8.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
|
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "go-sgf"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"chrono",
|
|
||||||
"nom",
|
|
||||||
"serde",
|
|
||||||
"thiserror",
|
|
||||||
"typeshare",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iana-time-zone"
|
name = "iana-time-zone"
|
||||||
version = "0.1.57"
|
version = "0.1.57"
|
||||||
|
@ -216,6 +205,17 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sgf"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"nom",
|
||||||
|
"serde",
|
||||||
|
"thiserror",
|
||||||
|
"typeshare",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.109"
|
version = "1.0.109"
|
|
@ -1,5 +1,5 @@
|
||||||
[package]
|
[package]
|
||||||
name = "go-sgf"
|
name = "sgf"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
|
@ -69,33 +69,103 @@
|
||||||
// VW
|
// VW
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
date::{self, parse_date_field, Date},
|
date::{parse_date_field, Date},
|
||||||
tree::{parse_collection, ParseSizeError, Size},
|
tree::{Size, Tree},
|
||||||
|
Error,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use typeshare::typeshare;
|
use typeshare::typeshare;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Error<'a> {
|
pub struct Game {
|
||||||
InvalidField,
|
pub board_size: Size,
|
||||||
InvalidBoardSize,
|
pub info: GameInfo,
|
||||||
Incomplete,
|
|
||||||
InvalidSgf(nom::error::VerboseError<&'a str>),
|
pub tree: Tree,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> From<nom::Err<nom::error::VerboseError<&'a str>>> for Error<'a> {
|
impl TryFrom<Tree> for Game {
|
||||||
fn from(err: nom::Err<nom::error::VerboseError<&'a str>>) -> Self {
|
type Error = Error;
|
||||||
match err {
|
|
||||||
nom::Err::Incomplete(_) => Error::Incomplete,
|
|
||||||
nom::Err::Error(e) => Error::InvalidSgf(e.clone()),
|
|
||||||
nom::Err::Failure(e) => Error::InvalidSgf(e.clone()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> From<ParseSizeError> for Error<'a> {
|
fn try_from(tree: Tree) -> Result<Self, Self::Error> {
|
||||||
fn from(_: ParseSizeError) -> Self {
|
let board_size = match tree.sequence[0].find_prop("SZ") {
|
||||||
Self::InvalidBoardSize
|
Some(prop) => Size::try_from(prop.values[0].as_str())?,
|
||||||
|
None => Size {
|
||||||
|
width: 19,
|
||||||
|
height: 19,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let mut info = GameInfo::default();
|
||||||
|
info.app_name = tree.sequence[0]
|
||||||
|
.find_prop("AP")
|
||||||
|
.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]
|
||||||
|
.find_prop("BR")
|
||||||
|
.and_then(|prop| Rank::try_from(prop.values[0].as_str()).ok());
|
||||||
|
|
||||||
|
info.white_player = tree.sequence[0]
|
||||||
|
.find_prop("PW")
|
||||||
|
.map(|prop| prop.values.join(", "));
|
||||||
|
|
||||||
|
info.white_rank = tree.sequence[0]
|
||||||
|
.find_prop("WR")
|
||||||
|
.and_then(|prop| Rank::try_from(prop.values[0].as_str()).ok());
|
||||||
|
|
||||||
|
info.result = tree.sequence[0]
|
||||||
|
.find_prop("RE")
|
||||||
|
.and_then(|prop| GameResult::try_from(prop.values[0].as_str()).ok());
|
||||||
|
|
||||||
|
info.time_limits = tree.sequence[0]
|
||||||
|
.find_prop("TM")
|
||||||
|
.and_then(|prop| prop.values[0].parse::<u64>().ok())
|
||||||
|
.and_then(|seconds| Some(std::time::Duration::from_secs(seconds)));
|
||||||
|
|
||||||
|
info.date = tree.sequence[0]
|
||||||
|
.find_prop("DT")
|
||||||
|
.and_then(|prop| {
|
||||||
|
let v = prop
|
||||||
|
.values
|
||||||
|
.iter()
|
||||||
|
.map(|val| parse_date_field(val))
|
||||||
|
.fold(Ok(vec![]), |acc, v| match (acc, v) {
|
||||||
|
(Ok(mut acc), Ok(mut values)) => {
|
||||||
|
acc.append(&mut values);
|
||||||
|
Ok(acc)
|
||||||
|
}
|
||||||
|
(Ok(_), Err(err)) => Err(err),
|
||||||
|
(Err(err), _) => Err(err),
|
||||||
|
})
|
||||||
|
.ok()?;
|
||||||
|
Some(v)
|
||||||
|
})
|
||||||
|
.unwrap_or(vec![]);
|
||||||
|
|
||||||
|
info.event = tree.sequence[0]
|
||||||
|
.find_prop("EV")
|
||||||
|
.map(|prop| prop.values.join(", "));
|
||||||
|
|
||||||
|
info.round = tree.sequence[0]
|
||||||
|
.find_prop("RO")
|
||||||
|
.map(|prop| prop.values.join(", "));
|
||||||
|
|
||||||
|
info.source = tree.sequence[0]
|
||||||
|
.find_prop("SO")
|
||||||
|
.map(|prop| prop.values.join(", "));
|
||||||
|
|
||||||
|
info.game_keeper = tree.sequence[0]
|
||||||
|
.find_prop("US")
|
||||||
|
.map(|prop| prop.values.join(", "));
|
||||||
|
|
||||||
|
Ok(Game {
|
||||||
|
board_size,
|
||||||
|
info,
|
||||||
|
|
||||||
|
tree,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,18 +197,9 @@ impl ToString for Rank {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct GameTree {
|
|
||||||
pub file_format: i8,
|
|
||||||
pub app_name: Option<String>,
|
|
||||||
pub game_type: GameType,
|
|
||||||
pub board_size: Size,
|
|
||||||
pub info: GameInfo,
|
|
||||||
// pub text: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct GameInfo {
|
pub struct GameInfo {
|
||||||
|
pub app_name: Option<String>,
|
||||||
pub annotator: Option<String>,
|
pub annotator: Option<String>,
|
||||||
pub copyright: Option<String>,
|
pub copyright: Option<String>,
|
||||||
pub event: Option<String>,
|
pub event: Option<String>,
|
||||||
|
@ -213,12 +274,6 @@ pub enum Win {
|
||||||
Time,
|
Time,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub enum GameType {
|
|
||||||
Go,
|
|
||||||
Unsupported,
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
enum PropType {
|
enum PropType {
|
||||||
Move,
|
Move,
|
||||||
|
@ -241,104 +296,13 @@ enum PropValue {
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
pub fn parse_sgf<'a>(input: &'a str) -> Result<Vec<GameTree>, Error<'a>> {
|
|
||||||
let (_, trees) = parse_collection::<nom::error::VerboseError<&'a str>>(input)?;
|
|
||||||
|
|
||||||
let games = trees
|
|
||||||
.into_iter()
|
|
||||||
.map(|tree| {
|
|
||||||
let file_format = match tree.sequence[0].find_prop("FF") {
|
|
||||||
Some(prop) => prop.values[0].parse::<i8>().unwrap(),
|
|
||||||
None => 4,
|
|
||||||
};
|
|
||||||
let app_name = tree.sequence[0]
|
|
||||||
.find_prop("AP")
|
|
||||||
.map(|prop| prop.values[0].clone());
|
|
||||||
let board_size = match tree.sequence[0].find_prop("SZ") {
|
|
||||||
Some(prop) => Size::try_from(prop.values[0].as_str())?,
|
|
||||||
None => Size {
|
|
||||||
width: 19,
|
|
||||||
height: 19,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
let mut info = GameInfo::default();
|
|
||||||
info.black_player = tree.sequence[0]
|
|
||||||
.find_prop("PB")
|
|
||||||
.map(|prop| prop.values.join(", "));
|
|
||||||
|
|
||||||
info.black_rank = tree.sequence[0]
|
|
||||||
.find_prop("BR")
|
|
||||||
.and_then(|prop| Rank::try_from(prop.values[0].as_str()).ok());
|
|
||||||
|
|
||||||
info.white_player = tree.sequence[0]
|
|
||||||
.find_prop("PW")
|
|
||||||
.map(|prop| prop.values.join(", "));
|
|
||||||
|
|
||||||
info.white_rank = tree.sequence[0]
|
|
||||||
.find_prop("WR")
|
|
||||||
.and_then(|prop| Rank::try_from(prop.values[0].as_str()).ok());
|
|
||||||
|
|
||||||
info.result = tree.sequence[0]
|
|
||||||
.find_prop("RE")
|
|
||||||
.and_then(|prop| GameResult::try_from(prop.values[0].as_str()).ok());
|
|
||||||
|
|
||||||
info.time_limits = tree.sequence[0]
|
|
||||||
.find_prop("TM")
|
|
||||||
.and_then(|prop| prop.values[0].parse::<u64>().ok())
|
|
||||||
.and_then(|seconds| Some(std::time::Duration::from_secs(seconds)));
|
|
||||||
|
|
||||||
info.date = tree.sequence[0]
|
|
||||||
.find_prop("DT")
|
|
||||||
.and_then(|prop| {
|
|
||||||
let v = prop
|
|
||||||
.values
|
|
||||||
.iter()
|
|
||||||
.map(|val| parse_date_field(val))
|
|
||||||
.fold(Ok(vec![]), |acc, v| match (acc, v) {
|
|
||||||
(Ok(mut acc), Ok(mut values)) => {
|
|
||||||
acc.append(&mut values);
|
|
||||||
Ok(acc)
|
|
||||||
}
|
|
||||||
(Ok(_), Err(err)) => Err(err),
|
|
||||||
(Err(err), _) => Err(err),
|
|
||||||
})
|
|
||||||
.ok()?;
|
|
||||||
Some(v)
|
|
||||||
})
|
|
||||||
.unwrap_or(vec![]);
|
|
||||||
|
|
||||||
info.event = tree.sequence[0]
|
|
||||||
.find_prop("EV")
|
|
||||||
.map(|prop| prop.values.join(", "));
|
|
||||||
|
|
||||||
info.round = tree.sequence[0]
|
|
||||||
.find_prop("RO")
|
|
||||||
.map(|prop| prop.values.join(", "));
|
|
||||||
|
|
||||||
info.source = tree.sequence[0]
|
|
||||||
.find_prop("SO")
|
|
||||||
.map(|prop| prop.values.join(", "));
|
|
||||||
|
|
||||||
info.game_keeper = tree.sequence[0]
|
|
||||||
.find_prop("US")
|
|
||||||
.map(|prop| prop.values.join(", "));
|
|
||||||
|
|
||||||
Ok(GameTree {
|
|
||||||
file_format,
|
|
||||||
app_name,
|
|
||||||
game_type: GameType::Go,
|
|
||||||
board_size,
|
|
||||||
info,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect::<Result<Vec<GameTree>, Error>>()?;
|
|
||||||
Ok(games)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{date::Date, tree::Size};
|
use crate::{
|
||||||
|
date::Date,
|
||||||
|
tree::{parse_collection, Size},
|
||||||
|
};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
|
||||||
|
@ -347,12 +311,16 @@ mod tests {
|
||||||
(;C[f](;C[g];C[h];C[i])
|
(;C[f](;C[g];C[h];C[i])
|
||||||
(;C[j])))";
|
(;C[j])))";
|
||||||
|
|
||||||
fn with_text(text: &str, f: impl FnOnce(Vec<GameTree>)) {
|
fn with_text(text: &str, f: impl FnOnce(Vec<Game>)) {
|
||||||
let games = parse_sgf(text).unwrap();
|
let (_, games) = parse_collection::<nom::error::VerboseError<&str>>(text).unwrap();
|
||||||
|
let games = games
|
||||||
|
.into_iter()
|
||||||
|
.map(|game| Game::try_from(game).expect("game to parse"))
|
||||||
|
.collect::<Vec<Game>>();
|
||||||
f(games);
|
f(games);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn with_file(path: &std::path::Path, f: impl FnOnce(Vec<GameTree>)) {
|
fn with_file(path: &std::path::Path, f: impl FnOnce(Vec<Game>)) {
|
||||||
let mut file = File::open(path).unwrap();
|
let mut file = File::open(path).unwrap();
|
||||||
let mut text = String::new();
|
let mut text = String::new();
|
||||||
let _ = file.read_to_string(&mut text);
|
let _ = file.read_to_string(&mut text);
|
||||||
|
@ -364,9 +332,7 @@ mod tests {
|
||||||
with_text(EXAMPLE, |trees| {
|
with_text(EXAMPLE, |trees| {
|
||||||
assert_eq!(trees.len(), 1);
|
assert_eq!(trees.len(), 1);
|
||||||
let tree = &trees[0];
|
let tree = &trees[0];
|
||||||
assert_eq!(tree.file_format, 4);
|
assert_eq!(tree.info.app_name, None);
|
||||||
assert_eq!(tree.app_name, None);
|
|
||||||
assert_eq!(tree.game_type, GameType::Go);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tree.board_size,
|
tree.board_size,
|
||||||
Size {
|
Size {
|
||||||
|
@ -380,9 +346,7 @@ mod tests {
|
||||||
with_file(std::path::Path::new("test_data/print1.sgf"), |trees| {
|
with_file(std::path::Path::new("test_data/print1.sgf"), |trees| {
|
||||||
assert_eq!(trees.len(), 1);
|
assert_eq!(trees.len(), 1);
|
||||||
let tree = &trees[0];
|
let tree = &trees[0];
|
||||||
assert_eq!(tree.file_format, 4);
|
assert_eq!(tree.info.app_name, None);
|
||||||
assert_eq!(tree.app_name, None);
|
|
||||||
assert_eq!(tree.game_type, GameType::Go);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tree.board_size,
|
tree.board_size,
|
||||||
Size {
|
Size {
|
|
@ -0,0 +1,96 @@
|
||||||
|
mod date;
|
||||||
|
pub use date::Date;
|
||||||
|
|
||||||
|
pub mod go;
|
||||||
|
|
||||||
|
mod tree;
|
||||||
|
use tree::parse_collection;
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
InvalidField,
|
||||||
|
InvalidBoardSize,
|
||||||
|
Incomplete,
|
||||||
|
InvalidSgf(VerboseNomError),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct VerboseNomError(nom::error::VerboseError<String>);
|
||||||
|
|
||||||
|
impl From<nom::error::VerboseError<&str>> for VerboseNomError {
|
||||||
|
fn from(err: nom::error::VerboseError<&str>) -> Self {
|
||||||
|
VerboseNomError(nom::error::VerboseError {
|
||||||
|
errors: err
|
||||||
|
.errors
|
||||||
|
.into_iter()
|
||||||
|
.map(|err| (err.0.to_owned(), err.1))
|
||||||
|
.collect(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<nom::Err<nom::error::VerboseError<&str>>> for Error {
|
||||||
|
fn from(err: nom::Err<nom::error::VerboseError<&str>>) -> Self {
|
||||||
|
match err {
|
||||||
|
nom::Err::Incomplete(_) => Error::Incomplete,
|
||||||
|
nom::Err::Error(e) => Error::InvalidSgf(VerboseNomError::from(e)),
|
||||||
|
nom::Err::Failure(e) => Error::InvalidSgf(VerboseNomError::from(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Error)]
|
||||||
|
pub enum ParseError {
|
||||||
|
#[error("An unknown error was found")]
|
||||||
|
NomError(nom::error::Error<String>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<nom::error::Error<&str>> for ParseError {
|
||||||
|
fn from(err: nom::error::Error<&str>) -> Self {
|
||||||
|
Self::NomError(nom::error::Error {
|
||||||
|
input: err.input.to_owned(),
|
||||||
|
code: err.code.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Game {
|
||||||
|
Go(go::Game),
|
||||||
|
Unsupported(tree::Tree),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_sgf(input: &str) -> Result<Vec<Game>, Error> {
|
||||||
|
let (_, trees) = parse_collection::<nom::error::VerboseError<&str>>(input)?;
|
||||||
|
Ok(trees
|
||||||
|
.into_iter()
|
||||||
|
.map(|t| match t.sequence[0].find_prop("GM") {
|
||||||
|
Some(prop) if prop.values == vec!["1".to_owned()] => {
|
||||||
|
Game::Go(go::Game::try_from(t).expect("properly structured game tree"))
|
||||||
|
}
|
||||||
|
_ => Game::Unsupported(t),
|
||||||
|
})
|
||||||
|
.collect::<Vec<Game>>())
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
impl From<(&str, VerboseErrorKind)> for
|
||||||
|
|
||||||
|
impl From<nom::error::VerboseError<&str>> for ParseError {
|
||||||
|
fn from(err: nom::error::VerboseError<&str>) -> Self {
|
||||||
|
Self::NomErrors(
|
||||||
|
err.errors
|
||||||
|
.into_iter()
|
||||||
|
.map(|err| ParseError::from(err))
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
/*
|
||||||
|
Self::NomError(nom::error::Error {
|
||||||
|
input: err.input.to_owned(),
|
||||||
|
code: err.code.clone(),
|
||||||
|
})
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::Error;
|
||||||
use nom::{
|
use nom::{
|
||||||
branch::alt,
|
branch::alt,
|
||||||
bytes::complete::{escaped_transform, tag},
|
bytes::complete::{escaped_transform, tag},
|
||||||
|
@ -9,6 +10,12 @@ use nom::{
|
||||||
};
|
};
|
||||||
use std::num::ParseIntError;
|
use std::num::ParseIntError;
|
||||||
|
|
||||||
|
impl From<ParseSizeError> for Error {
|
||||||
|
fn from(_: ParseSizeError) -> Self {
|
||||||
|
Self::InvalidBoardSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ParseSizeError {
|
pub enum ParseSizeError {
|
||||||
ParseIntError(ParseIntError),
|
ParseIntError(ParseIntError),
|
||||||
|
@ -45,7 +52,7 @@ impl TryFrom<&str> for Size {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct Tree {
|
pub struct Tree {
|
||||||
pub sequence: Vec<Node>,
|
pub sequence: Vec<Node>,
|
||||||
pub sub_sequences: Vec<Tree>,
|
pub sub_sequences: Vec<Tree>,
|
||||||
|
@ -67,7 +74,7 @@ impl ToString for Tree {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct Node {
|
pub struct Node {
|
||||||
pub properties: Vec<Property>,
|
pub properties: Vec<Property>,
|
||||||
}
|
}
|
Loading…
Reference in New Issue