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
2 changed files with 113 additions and 124 deletions
Showing only changes of commit d79598eaa3 - Show all commits

View File

@ -69,15 +69,108 @@
// VW
use crate::{
date::{self, parse_date_field, Date},
tree::{parse_collection, Size},
Error, VerboseNomError,
date::{parse_date_field, Date},
tree::{Size, Tree},
Error,
};
use nom::error::VerboseError;
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
pub struct Game {}
pub struct Game {
pub file_format: i8,
pub app_name: Option<String>,
pub board_size: Size,
pub info: GameInfo,
}
impl TryFrom<Tree> for Game {
type Error = Error;
fn try_from(tree: Tree) -> Result<Self, Self::Error> {
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(Game {
file_format,
app_name,
board_size,
info,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[typeshare]
@ -107,16 +200,6 @@ 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 annotator: Option<String>,
@ -193,12 +276,6 @@ pub enum Win {
Time,
}
#[derive(Clone, Debug, PartialEq)]
pub enum GameType {
Go,
Unsupported,
}
/*
enum PropType {
Move,
@ -221,104 +298,13 @@ enum PropValue {
}
*/
pub fn parse_sgf(input: &str) -> Result<Vec<GameTree>, Error> {
let (_, trees) = parse_collection::<nom::error::VerboseError<&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;
@ -327,12 +313,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);
@ -346,7 +336,6 @@ mod tests {
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.board_size,
Size {
@ -362,7 +351,6 @@ mod tests {
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.board_size,
Size {

View File

@ -1,8 +1,7 @@
mod date;
pub use date::Date;
mod go;
pub use go::{GameTree, GameType, Rank};
pub mod go;
mod tree;
use tree::parse_collection;
@ -67,7 +66,9 @@ pub fn parse_sgf(input: &str) -> Result<Vec<Game>, Error> {
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 {}),
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>>())