Set up a per-game interpretation layer atop the SGF, and bind all games together in a Game data structure #49

Merged
savanni merged 6 commits from feature/sgf-interpretation-layer into main 2023-07-27 22:20:24 +00:00
25 changed files with 277 additions and 240 deletions

View File

@ -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

View File

@ -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(),
})
*/
}
}
*/

24
kifu/core/Cargo.lock generated
View File

@ -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"

View File

@ -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" ] }

View File

@ -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) => {

View File

@ -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

View File

@ -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,

24
kifu/gtk/Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -1,5 +1,5 @@
[package] [package]
name = "go-sgf" name = "sgf"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"

View File

@ -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 {

96
sgf/src/lib.rs Normal file
View File

@ -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(),
})
*/
}
}
*/

View File

@ -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>,
} }