Set up a second layer to the SGF parser #226

Merged
savanni merged 4 commits from kifu/game-repr into main 2024-03-22 03:14:04 +00:00
1 changed files with 44 additions and 15 deletions
Showing only changes of commit bd6d5b62e3 - Show all commits

View File

@ -43,6 +43,14 @@ pub struct Player {
pub team: Option<String>, pub team: Option<String>,
} }
/// This represents the more semantic version of the game parser. Where the `parser` crate pulls
/// out a raw set of nodes, this structure is guaranteed to be a well-formed game. Getting to this
/// level, the interpreter will reject any games that have setup properties and move properties
/// mixed in a single node. If there are other semantic problems, the interpreter will reject
/// those, as well. Where the function of the parser is to understand and correct fundamental
/// syntax issues, the result of the Game is to have a fully-understood game. However, this doesn't
/// (yet?) go quite to the level of apply the game type (i.e., this is Go, Chess, Yinsh, or
/// whatever).
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct Game { pub struct Game {
game_type: GameType, game_type: GameType,
@ -267,17 +275,45 @@ impl Node for GameNode {
impl TryFrom<&parser::Node> for GameNode { impl TryFrom<&parser::Node> for GameNode {
type Error = GameNodeError; type Error = GameNodeError;
fn try_from(n: &parser::Node) -> Result<Self, Self::Error> { fn try_from(n: &parser::Node) -> Result<Self, Self::Error> {
// I originally wrote this recursively. However, on an ordinary game of a couple hundred
// moves, that meant that I was recursing 500 functions, and that exceeded the stack limit.
// So, instead, I need to unroll everything to non-recursive form.
//
// So, I can treat each branch of the tree as a single line. Iterate over that line. I can
// only use the MoveNode::try_from and SetupNode::try_from if those functions don't
// recurse. Instead, I'm going to process just that node, then return to here and process
// the children.
let move_node = MoveNode::try_from(n); let move_node = MoveNode::try_from(n);
let setup_node = SetupNode::try_from(n); let setup_node = SetupNode::try_from(n);
match (move_node, setup_node) { // I'm much too tired when writing this. I'm still recursing, but I did cut the number of
(Ok(node), _) => Ok(Self::MoveNode(node)), // recursions in half. This helps, but it still doesn't guarantee that I'm going to be able
(Err(_), Ok(node)) => Ok(Self::SetupNode(node)), // to parse all possible games. So, still, treat each branch of the game as a single line.
// Iterate over that line, don't recurse. Create bookmarks at each branch point, and then
// come back to each one.
let children = n
.next
.iter()
.map(|n| GameNode::try_from(n))
.collect::<Result<Vec<Self>, Self::Error>>()?;
let node = match (move_node, setup_node) {
(Ok(mut node), _) => {
node.children = children;
Ok(Self::MoveNode(node))
}
(Err(_), Ok(mut node)) => {
node.children = children;
Ok(Self::SetupNode(node))
}
(Err(move_err), Err(setup_err)) => { (Err(move_err), Err(setup_err)) => {
Err(Self::Error::UnsupportedGameNode(move_err, setup_err)) Err(Self::Error::UnsupportedGameNode(move_err, setup_err))
} }
} }?;
Ok(node)
} }
} }
@ -333,7 +369,7 @@ impl TryFrom<&parser::Node> for MoveNode {
type Error = MoveNodeError; type Error = MoveNodeError;
fn try_from(n: &parser::Node) -> Result<Self, Self::Error> { fn try_from(n: &parser::Node) -> Result<Self, Self::Error> {
let mut s = match n.mv() { let s = match n.mv() {
Some((color, mv)) => { Some((color, mv)) => {
let mut s = Self::new(color, mv); let mut s = Self::new(color, mv);
@ -383,14 +419,6 @@ impl TryFrom<&parser::Node> for MoveNode {
None => Err(Self::Error::NotAMoveNode), None => Err(Self::Error::NotAMoveNode),
}?; }?;
s.children = n
.next
.iter()
.map(|node| {
GameNode::try_from(node).map_err(|err| Self::Error::ChildError(Box::new(err)))
})
.collect::<Result<Vec<GameNode>, MoveNodeError>>()?;
Ok(s) Ok(s)
} }
} }
@ -677,9 +705,10 @@ mod file_test {
with_text(&text, f); with_text(&text, f);
} }
#[ignore] /// This test checks against an ordinary game from SGF. It is unannotated and should contain
/// only move nodes with no setup nodes. The original source is from a game I played on KGS.
#[test] #[test]
fn it_can_load_basic_game_records() { fn it_can_load_an_ordinary_unannotated_game() {
with_file( with_file(
std::path::Path::new("test_data/2020 USGO DDK, Round 1.sgf"), std::path::Path::new("test_data/2020 USGO DDK, Round 1.sgf"),
|games| { |games| {