Set up a per-game interpretation layer atop the SGF, and bind all games together in a Game
data structure #49
230
sgf/src/go.rs
230
sgf/src/go.rs
|
@ -69,15 +69,108 @@
|
||||||
// VW
|
// VW
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
date::{self, parse_date_field, Date},
|
date::{parse_date_field, Date},
|
||||||
tree::{parse_collection, Size},
|
tree::{Size, Tree},
|
||||||
Error, VerboseNomError,
|
Error,
|
||||||
};
|
};
|
||||||
use nom::error::VerboseError;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use typeshare::typeshare;
|
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)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[typeshare]
|
#[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)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct GameInfo {
|
pub struct GameInfo {
|
||||||
pub annotator: Option<String>,
|
pub annotator: Option<String>,
|
||||||
|
@ -193,12 +276,6 @@ pub enum Win {
|
||||||
Time,
|
Time,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub enum GameType {
|
|
||||||
Go,
|
|
||||||
Unsupported,
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
enum PropType {
|
enum PropType {
|
||||||
Move,
|
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)]
|
#[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;
|
||||||
|
|
||||||
|
@ -327,12 +313,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);
|
||||||
|
@ -346,7 +336,6 @@ mod tests {
|
||||||
let tree = &trees[0];
|
let tree = &trees[0];
|
||||||
assert_eq!(tree.file_format, 4);
|
assert_eq!(tree.file_format, 4);
|
||||||
assert_eq!(tree.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 {
|
||||||
|
@ -362,7 +351,6 @@ mod tests {
|
||||||
let tree = &trees[0];
|
let tree = &trees[0];
|
||||||
assert_eq!(tree.file_format, 4);
|
assert_eq!(tree.file_format, 4);
|
||||||
assert_eq!(tree.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 {
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
mod date;
|
mod date;
|
||||||
pub use date::Date;
|
pub use date::Date;
|
||||||
|
|
||||||
mod go;
|
pub mod go;
|
||||||
pub use go::{GameTree, GameType, Rank};
|
|
||||||
|
|
||||||
mod tree;
|
mod tree;
|
||||||
use tree::parse_collection;
|
use tree::parse_collection;
|
||||||
|
@ -67,7 +66,9 @@ pub fn parse_sgf(input: &str) -> Result<Vec<Game>, Error> {
|
||||||
Ok(trees
|
Ok(trees
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|t| match t.sequence[0].find_prop("GM") {
|
.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),
|
_ => Game::Unsupported(t),
|
||||||
})
|
})
|
||||||
.collect::<Vec<Game>>())
|
.collect::<Vec<Game>>())
|
||||||
|
|
Loading…
Reference in New Issue