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:
cd changeset && make dev
@ -58,3 +64,9 @@ kifu-pwa/dev:
kifu-pwa/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",
]
[[package]]
name = "go-sgf"
version = "0.1.0"
dependencies = [
"chrono",
"nom",
"serde",
"thiserror",
"typeshare",
]
[[package]]
name = "grid"
version = "0.9.0"
@ -191,10 +180,10 @@ version = "0.1.0"
dependencies = [
"chrono",
"cool_asserts",
"go-sgf",
"grid",
"serde",
"serde_json",
"sgf",
"thiserror",
"typeshare",
]
@ -337,6 +326,17 @@ dependencies = [
"serde",
]
[[package]]
name = "sgf"
version = "0.1.0"
dependencies = [
"chrono",
"nom",
"serde",
"thiserror",
"typeshare",
]
[[package]]
name = "syn"
version = "1.0.109"

View File

@ -7,7 +7,7 @@ edition = "2021"
[dependencies]
chrono = { version = "0.4" }
go-sgf = { path = "../../go-sgf" }
sgf = { path = "../../sgf" }
grid = { version = "0.9" }
serde_json = { version = "1" }
serde = { version = "1", features = [ "derive" ] }

View File

@ -1,6 +1,6 @@
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;
#[derive(Error, Debug)]
@ -20,12 +20,12 @@ impl From<std::io::Error> for Error {
#[derive(Debug)]
pub struct Database {
path: PathBuf,
games: Vec<GameTree>,
games: Vec<go::Game>,
}
impl Database {
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();
@ -39,8 +39,12 @@ impl Database {
.unwrap()
.read_to_string(&mut buffer)
.unwrap();
let sgf = parse_sgf(&buffer).unwrap();
games.extend(sgf);
for sgf in parse_sgf(&buffer).unwrap() {
match sgf {
Game::Go(game) => games.push(game),
Game::Unsupported(_) => {}
}
}
}
}
Err(err) => println!("failed entry: {:?}", err),
@ -54,7 +58,7 @@ impl Database {
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()
}
}
@ -63,7 +67,7 @@ impl Database {
mod test {
use super::*;
use cool_asserts::assert_matches;
use go_sgf::{Date, GameType};
use sgf::Date;
#[test]
fn it_reads_empty_database() {
@ -77,9 +81,7 @@ mod test {
let db =
Database::open_path(PathBuf::from("fixtures/five_games/")).expect("database to open");
assert_eq!(db.all_games().count(), 5);
for game in db.all_games() {
assert_eq!(game.game_type, GameType::Go);
}
for game in db.all_games() {}
assert_matches!(db.all_games().find(|g| g.info.black_player == Some("Steve".to_owned())),
Some(game) => {

View File

@ -1,5 +1,8 @@
use go_sgf::{Date, GameTree, Rank};
use serde::{Deserialize, Serialize};
use sgf::{
go::{Game, Rank},
Date,
};
use typeshare::typeshare;
#[derive(Clone, Debug, Deserialize, Serialize)]
@ -13,7 +16,7 @@ pub struct GamePreviewElement {
}
impl GamePreviewElement {
pub fn new(game: &GameTree) -> GamePreviewElement {
pub fn new(game: &Game) -> GamePreviewElement {
GamePreviewElement {
date: game.info.date.clone(),
black_player: game

View File

@ -1,6 +1,6 @@
use crate::ui::{Action, GamePreviewElement};
use go_sgf::GameTree;
use serde::{Deserialize, Serialize};
use sgf::go::Game;
use typeshare::typeshare;
fn rank_strings() -> Vec<String> {
@ -56,7 +56,7 @@ pub struct HomeView {
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 {
placeholder: Some("black player".to_owned()),
default_rank: None,

24
kifu/gtk/Cargo.lock generated
View File

@ -545,17 +545,6 @@ dependencies = [
"system-deps",
]
[[package]]
name = "go-sgf"
version = "0.1.0"
dependencies = [
"chrono",
"nom",
"serde",
"thiserror",
"typeshare",
]
[[package]]
name = "gobject-sys"
version = "0.17.4"
@ -799,10 +788,10 @@ name = "kifu-core"
version = "0.1.0"
dependencies = [
"chrono",
"go-sgf",
"grid",
"serde",
"serde_json",
"sgf",
"thiserror",
"typeshare",
]
@ -1237,6 +1226,17 @@ dependencies = [
"serde",
]
[[package]]
name = "sgf"
version = "0.1.0"
dependencies = [
"chrono",
"nom",
"serde",
"thiserror",
"typeshare",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.1"

View File

@ -63,17 +63,6 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
[[package]]
name = "go-sgf"
version = "0.1.0"
dependencies = [
"chrono",
"nom",
"serde",
"thiserror",
"typeshare",
]
[[package]]
name = "iana-time-zone"
version = "0.1.57"
@ -216,6 +205,17 @@ dependencies = [
"serde",
]
[[package]]
name = "sgf"
version = "0.1.0"
dependencies = [
"chrono",
"nom",
"serde",
"thiserror",
"typeshare",
]
[[package]]
name = "syn"
version = "1.0.109"

View File

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

View File

@ -69,33 +69,103 @@
// VW
use crate::{
date::{self, parse_date_field, Date},
tree::{parse_collection, ParseSizeError, Size},
date::{parse_date_field, Date},
tree::{Size, Tree},
Error,
};
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
#[derive(Debug)]
pub enum Error<'a> {
InvalidField,
InvalidBoardSize,
Incomplete,
InvalidSgf(nom::error::VerboseError<&'a str>),
#[derive(Clone, Debug)]
pub struct Game {
pub board_size: Size,
pub info: GameInfo,
pub tree: Tree,
}
impl<'a> From<nom::Err<nom::error::VerboseError<&'a str>>> for Error<'a> {
fn from(err: nom::Err<nom::error::VerboseError<&'a str>>) -> Self {
match err {
nom::Err::Incomplete(_) => Error::Incomplete,
nom::Err::Error(e) => Error::InvalidSgf(e.clone()),
nom::Err::Failure(e) => Error::InvalidSgf(e.clone()),
}
}
}
impl TryFrom<Tree> for Game {
type Error = Error;
impl<'a> From<ParseSizeError> for Error<'a> {
fn from(_: ParseSizeError) -> Self {
Self::InvalidBoardSize
fn try_from(tree: Tree) -> Result<Self, Self::Error> {
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.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)]
pub struct GameInfo {
pub app_name: Option<String>,
pub annotator: Option<String>,
pub copyright: Option<String>,
pub event: Option<String>,
@ -213,12 +274,6 @@ pub enum Win {
Time,
}
#[derive(Clone, Debug, PartialEq)]
pub enum GameType {
Go,
Unsupported,
}
/*
enum PropType {
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)]
mod tests {
use super::*;
use crate::{date::Date, tree::Size};
use crate::{
date::Date,
tree::{parse_collection, Size},
};
use std::fs::File;
use std::io::Read;
@ -347,12 +311,16 @@ mod tests {
(;C[f](;C[g];C[h];C[i])
(;C[j])))";
fn with_text(text: &str, f: impl FnOnce(Vec<GameTree>)) {
let games = parse_sgf(text).unwrap();
fn with_text(text: &str, f: impl FnOnce(Vec<Game>)) {
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);
}
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 text = String::new();
let _ = file.read_to_string(&mut text);
@ -364,9 +332,7 @@ mod tests {
with_text(EXAMPLE, |trees| {
assert_eq!(trees.len(), 1);
let tree = &trees[0];
assert_eq!(tree.file_format, 4);
assert_eq!(tree.app_name, None);
assert_eq!(tree.game_type, GameType::Go);
assert_eq!(tree.info.app_name, None);
assert_eq!(
tree.board_size,
Size {
@ -380,9 +346,7 @@ mod tests {
with_file(std::path::Path::new("test_data/print1.sgf"), |trees| {
assert_eq!(trees.len(), 1);
let tree = &trees[0];
assert_eq!(tree.file_format, 4);
assert_eq!(tree.app_name, None);
assert_eq!(tree.game_type, GameType::Go);
assert_eq!(tree.info.app_name, None);
assert_eq!(
tree.board_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::{
branch::alt,
bytes::complete::{escaped_transform, tag},
@ -9,6 +10,12 @@ use nom::{
};
use std::num::ParseIntError;
impl From<ParseSizeError> for Error {
fn from(_: ParseSizeError) -> Self {
Self::InvalidBoardSize
}
}
#[derive(Debug)]
pub enum ParseSizeError {
ParseIntError(ParseIntError),
@ -45,7 +52,7 @@ impl TryFrom<&str> for Size {
}
}
#[derive(Debug, PartialEq)]
#[derive(Clone, Debug, PartialEq)]
pub struct Tree {
pub sequence: Vec<Node>,
pub sub_sequences: Vec<Tree>,
@ -67,7 +74,7 @@ impl ToString for Tree {
}
}
#[derive(Debug, PartialEq)]
#[derive(Clone, Debug, PartialEq)]
pub struct Node {
pub properties: Vec<Property>,
}