2023-10-19 13:57:02 +00:00
use crate ::{
parser ::{ self , Annotation , Evaluation , Move , SetupInstr , Size , UnknownProperty } ,
Color , Date , GameResult , GameType ,
} ;
2024-03-22 03:23:47 +00:00
use serde ::{ Deserialize , Serialize } ;
2024-04-19 12:03:40 +00:00
use slab_tree ::{ NodeRef , Tree } ;
use std ::{
collections ::{ HashSet , VecDeque } ,
time ::Duration ,
} ;
2023-10-19 13:57:02 +00:00
use uuid ::Uuid ;
#[ derive(Clone, Debug, PartialEq) ]
pub enum GameError {
InvalidGame ,
RequiredPropertiesMissing ,
InvalidGameNode ( GameNodeError ) ,
}
#[ derive(Clone, Debug, PartialEq) ]
pub enum MoveNodeError {
IncompatibleProperty ( parser ::Property ) ,
ConflictingProperty ,
NotAMoveNode ,
ChildError ( Box < GameNodeError > ) ,
}
#[ derive(Clone, Debug, PartialEq) ]
pub enum SetupNodeError {
IncompatibleProperty ( parser ::Property ) ,
ConflictingProperty ,
ConflictingPosition ,
NotASetupNode ,
ChildError ( Box < GameNodeError > ) ,
}
#[ derive(Clone, Debug, PartialEq) ]
pub enum GameNodeError {
UnsupportedGameNode ( MoveNodeError , SetupNodeError ) ,
ConflictingProperty ,
ConflictingPosition ,
}
2024-03-22 03:23:47 +00:00
#[ derive(Clone, Debug, PartialEq, Default, Deserialize, Serialize) ]
2023-10-19 13:57:02 +00:00
pub struct Player {
pub name : Option < String > ,
pub rank : Option < String > ,
pub team : Option < String > ,
}
2023-10-20 04:36:03 +00:00
/// 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
2024-03-23 17:40:06 +00:00
/// syntax issues, the result of the GameRecord 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
2023-10-20 04:36:03 +00:00
/// whatever).
2024-03-23 17:40:06 +00:00
pub struct GameRecord {
2024-03-22 03:12:30 +00:00
pub game_type : GameType ,
2023-10-19 13:57:02 +00:00
// TODO: board size is not necessary in all games. Hive has no defined board size.
2024-03-22 03:12:30 +00:00
pub board_size : Size ,
pub black_player : Player ,
pub white_player : Player ,
2023-10-19 13:57:02 +00:00
2024-03-22 03:12:30 +00:00
pub app : Option < String > ,
pub annotator : Option < String > ,
pub copyright : Option < String > ,
pub dates : Vec < Date > ,
pub event : Option < String > ,
pub game_name : Option < String > ,
pub extra_info : Option < String > ,
pub opening_info : Option < String > ,
pub location : Option < String > ,
pub result : Option < GameResult > ,
pub round : Option < String > ,
pub rules : Option < String > ,
pub source : Option < String > ,
pub time_limit : Option < Duration > ,
pub overtime : Option < String > ,
pub transcriber : Option < String > ,
2023-10-19 13:57:02 +00:00
2024-04-19 12:03:40 +00:00
pub trees : Vec < Tree < GameNode > > ,
2023-10-19 13:57:02 +00:00
}
2024-03-23 17:40:06 +00:00
impl GameRecord {
2023-10-19 13:57:02 +00:00
pub fn new (
game_type : GameType ,
board_size : Size ,
black_player : Player ,
white_player : Player ,
) -> Self {
Self {
game_type ,
board_size ,
black_player ,
white_player ,
app : None ,
annotator : None ,
copyright : None ,
dates : vec ! [ ] ,
event : None ,
game_name : None ,
extra_info : None ,
opening_info : None ,
location : None ,
result : None ,
round : None ,
rules : None ,
source : None ,
time_limit : None ,
overtime : None ,
transcriber : None ,
2024-04-19 12:03:40 +00:00
trees : vec ! [ ] ,
2023-10-19 13:57:02 +00:00
}
}
2024-03-24 19:50:59 +00:00
2024-04-19 12:03:40 +00:00
pub fn nodes ( & self ) -> Vec < & GameNode > {
self . iter ( ) . collect ( )
2024-03-24 19:50:59 +00:00
}
2023-10-19 13:57:02 +00:00
2024-04-19 12:03:40 +00:00
pub fn iter ( & self ) -> impl Iterator < Item = & '_ GameNode > {
self . trees
. iter ( )
. flat_map ( | tree | tree . root ( ) . unwrap ( ) . traverse_pre_order ( ) )
. map ( | nr | nr . data ( ) )
2023-10-19 13:57:02 +00:00
}
2024-04-19 12:03:40 +00:00
/// Generate a list of moves which constitute the main line of the game. This is the game as it
/// was actually played out, and by convention consists of the first node in each list of
/// children.
pub fn mainline ( & self ) -> Option < impl Iterator < Item = & '_ GameNode > > {
println! ( " number of trees: {} " , self . trees . len ( ) ) ;
if self . trees . len ( ) > 0 {
Some ( MainlineIter {
next : self . trees [ 0 ] . root ( ) ,
tree : & self . trees [ 0 ] ,
} )
} else {
None
}
2023-10-19 13:57:02 +00:00
}
}
2024-03-23 17:40:06 +00:00
impl TryFrom < & parser ::Tree > for GameRecord {
2023-10-19 13:57:02 +00:00
type Error = GameError ;
fn try_from ( tree : & parser ::Tree ) -> Result < Self , Self ::Error > {
let mut ty = None ;
let mut size = None ;
let mut black_player = Player {
name : None ,
rank : None ,
team : None ,
} ;
let mut white_player = Player {
name : None ,
rank : None ,
team : None ,
} ;
for prop in tree . root . properties . iter ( ) {
match prop {
parser ::Property ::GameType ( ty_ ) = > ty = Some ( ty_ . clone ( ) ) ,
parser ::Property ::BoardSize ( size_ ) = > size = Some ( size_ . clone ( ) ) ,
parser ::Property ::BlackPlayer ( name ) = > {
black_player . name = Some ( name . clone ( ) ) ;
}
parser ::Property ::WhitePlayer ( name ) = > {
white_player . name = Some ( name . clone ( ) ) ;
}
parser ::Property ::BlackRank ( rank ) = > {
black_player . rank = Some ( rank . clone ( ) ) ;
}
parser ::Property ::WhiteRank ( rank ) = > {
white_player . rank = Some ( rank . clone ( ) ) ;
}
parser ::Property ::BlackTeam ( team ) = > {
black_player . team = Some ( team . clone ( ) ) ;
}
parser ::Property ::WhiteTeam ( team ) = > {
white_player . team = Some ( team . clone ( ) ) ;
}
_ = > { }
}
}
let mut s = match ( ty , size ) {
( Some ( ty ) , Some ( size ) ) = > Ok ( Self ::new ( ty , size , black_player , white_player ) ) ,
_ = > Err ( Self ::Error ::RequiredPropertiesMissing ) ,
} ? ;
for prop in tree . root . properties . iter ( ) {
match prop {
parser ::Property ::GameType ( _ )
| parser ::Property ::BoardSize ( _ )
| parser ::Property ::BlackPlayer ( _ )
| parser ::Property ::WhitePlayer ( _ )
| parser ::Property ::BlackRank ( _ )
| parser ::Property ::WhiteRank ( _ )
| parser ::Property ::BlackTeam ( _ )
| parser ::Property ::WhiteTeam ( _ ) = > { }
parser ::Property ::Application ( v ) = > s . app = Some ( v . clone ( ) ) ,
parser ::Property ::Annotator ( v ) = > s . annotator = Some ( v . clone ( ) ) ,
parser ::Property ::Copyright ( v ) = > s . copyright = Some ( v . clone ( ) ) ,
parser ::Property ::EventDates ( v ) = > s . dates = v . clone ( ) ,
parser ::Property ::EventName ( v ) = > s . event = Some ( v . clone ( ) ) ,
parser ::Property ::GameName ( v ) = > s . game_name = Some ( v . clone ( ) ) ,
parser ::Property ::ExtraGameInformation ( v ) = > s . extra_info = Some ( v . clone ( ) ) ,
parser ::Property ::GameOpening ( v ) = > s . opening_info = Some ( v . clone ( ) ) ,
parser ::Property ::GameLocation ( v ) = > s . location = Some ( v . clone ( ) ) ,
parser ::Property ::Result ( v ) = > s . result = Some ( v . clone ( ) ) ,
parser ::Property ::Round ( v ) = > s . round = Some ( v . clone ( ) ) ,
parser ::Property ::Ruleset ( v ) = > s . rules = Some ( v . clone ( ) ) ,
parser ::Property ::Source ( v ) = > s . source = Some ( v . clone ( ) ) ,
2024-03-26 12:46:54 +00:00
parser ::Property ::TimeLimit ( v ) = > s . time_limit = Some ( * v ) ,
2023-10-19 13:57:02 +00:00
parser ::Property ::Overtime ( v ) = > s . overtime = Some ( v . clone ( ) ) ,
// parser::Property::Data(v) => s.transcriber = Some(v.clone()),
_ = > { }
}
}
2024-04-19 12:03:40 +00:00
/*
2023-10-19 13:57:02 +00:00
s . children = tree
. root
. next
. iter ( )
2024-03-26 12:46:54 +00:00
. map ( GameNode ::try_from )
2023-10-19 13:57:02 +00:00
. collect ::< Result < Vec < GameNode > , GameNodeError > > ( )
. map_err ( GameError ::InvalidGameNode ) ? ;
2024-04-19 12:03:40 +00:00
* /
s . trees = vec! [ ] ;
2023-10-19 13:57:02 +00:00
Ok ( s )
}
}
2024-04-19 12:03:40 +00:00
pub struct TreeIter < ' a > {
queue : VecDeque < NodeRef < ' a , & ' a GameNode > > ,
2023-10-19 13:57:02 +00:00
}
2024-04-19 12:03:40 +00:00
impl < ' a > Default for TreeIter < ' a > {
fn default ( ) -> Self {
TreeIter {
queue : VecDeque ::default ( ) ,
}
2023-10-19 13:57:02 +00:00
}
}
2024-04-19 12:03:40 +00:00
impl < ' a > Iterator for TreeIter < ' a > {
type Item = & ' a GameNode ;
fn next ( & mut self ) -> Option < Self ::Item > {
let retval = self . queue . pop_front ( ) ;
if let Some ( ref retval ) = retval {
retval
. children ( )
. for_each ( | node | self . queue . push_back ( node ) ) ;
2023-10-19 13:57:02 +00:00
}
2024-04-19 12:03:40 +00:00
retval . map ( | rv | * rv . data ( ) )
2023-10-19 13:57:02 +00:00
}
}
2024-04-19 12:03:40 +00:00
pub struct MainlineIter < ' a > {
next : Option < NodeRef < ' a , GameNode > > ,
tree : & ' a Tree < GameNode > ,
}
2023-10-19 13:57:02 +00:00
2024-04-19 12:03:40 +00:00
impl < ' a > Iterator for MainlineIter < ' a > {
type Item = & ' a GameNode ;
fn next ( & mut self ) -> Option < Self ::Item > {
if let Some ( next ) = self . next . take ( ) {
let ret = self . tree . get ( next . node_id ( ) ) ? ;
self . next = next
. first_child ( )
. and_then ( | child | self . tree . get ( child . node_id ( ) ) ) ;
Some ( ret . data ( ) )
} else {
None
2023-10-19 13:57:02 +00:00
}
}
2024-04-19 12:03:40 +00:00
}
#[ derive(Clone, Debug, PartialEq, Deserialize, Serialize) ]
pub enum GameNode {
MoveNode ( MoveNode ) ,
SetupNode ( SetupNode ) ,
}
2023-10-19 13:57:02 +00:00
2024-04-19 12:03:40 +00:00
impl GameNode {
pub fn id ( & self ) -> Uuid {
2023-10-19 13:57:02 +00:00
match self {
2024-04-19 12:03:40 +00:00
GameNode ::MoveNode ( node ) = > node . id ,
GameNode ::SetupNode ( node ) = > node . id ,
2023-10-19 13:57:02 +00:00
}
}
}
impl TryFrom < & parser ::Node > for GameNode {
type Error = GameNodeError ;
2023-10-20 04:36:03 +00:00
2023-10-19 13:57:02 +00:00
fn try_from ( n : & parser ::Node ) -> Result < Self , Self ::Error > {
2023-10-20 04:36:03 +00:00
// 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.
2023-10-19 13:57:02 +00:00
let move_node = MoveNode ::try_from ( n ) ;
let setup_node = SetupNode ::try_from ( n ) ;
2023-10-20 04:36:03 +00:00
// I'm much too tired when writing this. I'm still recursing, but I did cut the number of
// recursions in half. This helps, but it still doesn't guarantee that I'm going to be able
// 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.
2024-04-19 12:03:40 +00:00
/*
2023-10-20 04:36:03 +00:00
let children = n
. next
. iter ( )
2024-03-26 12:46:54 +00:00
. map ( GameNode ::try_from )
2023-10-20 04:36:03 +00:00
. collect ::< Result < Vec < Self > , Self ::Error > > ( ) ? ;
2024-04-19 12:03:40 +00:00
* /
2023-10-20 04:36:03 +00:00
2024-04-19 12:03:40 +00:00
match ( move_node , setup_node ) {
( Ok ( mut node ) , _ ) = > Ok ( Self ::MoveNode ( node ) ) ,
( Err ( _ ) , Ok ( mut node ) ) = > Ok ( Self ::SetupNode ( node ) ) ,
2023-10-19 13:57:02 +00:00
( Err ( move_err ) , Err ( setup_err ) ) = > {
Err ( Self ::Error ::UnsupportedGameNode ( move_err , setup_err ) )
}
2024-04-19 12:03:40 +00:00
}
2023-10-19 13:57:02 +00:00
}
}
2024-03-22 03:23:47 +00:00
#[ derive(Clone, Debug, PartialEq, Deserialize, Serialize) ]
2023-10-19 13:57:02 +00:00
pub struct MoveNode {
2024-03-24 15:03:40 +00:00
pub id : Uuid ,
pub color : Color ,
pub mv : Move ,
pub time_left : Option < Duration > ,
pub moves_left : Option < usize > ,
pub name : Option < String > ,
pub evaluation : Option < Evaluation > ,
pub value : Option < f64 > ,
pub comments : Option < String > ,
pub annotation : Option < Annotation > ,
pub unknown_props : Vec < ( String , String ) > ,
2023-10-19 13:57:02 +00:00
}
impl MoveNode {
pub fn new ( color : Color , mv : Move ) -> Self {
Self {
id : Uuid ::new_v4 ( ) ,
color ,
mv ,
time_left : None ,
moves_left : None ,
name : None ,
evaluation : None ,
value : None ,
comments : None ,
annotation : None ,
unknown_props : vec ! [ ] ,
}
}
}
impl TryFrom < & parser ::Node > for MoveNode {
type Error = MoveNodeError ;
fn try_from ( n : & parser ::Node ) -> Result < Self , Self ::Error > {
2023-10-20 04:36:03 +00:00
let s = match n . mv ( ) {
2023-10-19 13:57:02 +00:00
Some ( ( color , mv ) ) = > {
let mut s = Self ::new ( color , mv ) ;
for prop in n . properties . iter ( ) {
match prop {
parser ::Property ::Move ( ( color , mv ) ) = > {
if s . color ! = * color | | s . mv ! = * mv {
return Err ( Self ::Error ::ConflictingProperty ) ;
}
}
parser ::Property ::TimeLeft ( ( color , duration ) ) = > {
if s . color ! = * color {
return Err ( Self ::Error ::ConflictingProperty ) ;
}
if s . time_left . is_some ( ) {
return Err ( Self ::Error ::ConflictingProperty ) ;
}
2024-03-26 12:46:54 +00:00
s . time_left = Some ( * duration ) ;
2023-10-19 13:57:02 +00:00
}
parser ::Property ::Comment ( cmt ) = > {
if s . comments . is_some ( ) {
return Err ( Self ::Error ::ConflictingProperty ) ;
}
s . comments = Some ( cmt . clone ( ) ) ;
}
parser ::Property ::Evaluation ( evaluation ) = > {
if s . evaluation . is_some ( ) {
return Err ( Self ::Error ::ConflictingProperty ) ;
}
s . evaluation = Some ( * evaluation )
}
parser ::Property ::Annotation ( annotation ) = > {
if s . annotation . is_some ( ) {
return Err ( Self ::Error ::ConflictingProperty ) ;
}
s . annotation = Some ( * annotation )
}
2024-03-22 02:48:53 +00:00
parser ::Property ::Territory ( .. ) = > {
eprintln! ( " not processing territory property " ) ;
}
2023-10-19 13:57:02 +00:00
parser ::Property ::Unknown ( UnknownProperty { ident , value } ) = > {
s . unknown_props . push ( ( ident . clone ( ) , value . clone ( ) ) ) ;
}
_ = > return Err ( Self ::Error ::IncompatibleProperty ( prop . clone ( ) ) ) ,
}
}
Ok ( s )
}
None = > Err ( Self ::Error ::NotAMoveNode ) ,
} ? ;
Ok ( s )
}
}
2024-03-22 03:23:47 +00:00
#[ derive(Clone, Debug, PartialEq, Deserialize, Serialize) ]
2023-10-19 13:57:02 +00:00
pub struct SetupNode {
id : Uuid ,
2024-03-25 22:11:03 +00:00
pub positions : Vec < parser ::SetupInstr > ,
2023-10-19 13:57:02 +00:00
}
impl SetupNode {
pub fn new ( positions : Vec < parser ::SetupInstr > ) -> Result < Self , SetupNodeError > {
let mut board = HashSet ::new ( ) ;
for position in positions . iter ( ) {
let point = match position {
SetupInstr ::Piece ( ( _ , point ) ) = > point ,
SetupInstr ::Clear ( point ) = > point ,
} ;
if board . contains ( point ) {
return Err ( SetupNodeError ::ConflictingPosition ) ;
}
board . insert ( point ) ;
}
Ok ( Self {
id : Uuid ::new_v4 ( ) ,
positions ,
} )
}
}
impl TryFrom < & parser ::Node > for SetupNode {
type Error = SetupNodeError ;
fn try_from ( n : & parser ::Node ) -> Result < Self , Self ::Error > {
match n . setup ( ) {
Some ( elements ) = > Self ::new ( elements ) ,
None = > Err ( Self ::Error ::NotASetupNode ) ,
}
}
}
2024-04-19 12:03:40 +00:00
/*
2023-10-19 13:57:02 +00:00
#[ allow(dead_code) ]
2024-03-26 12:46:54 +00:00
pub fn path_to_node ( node : & GameNode , id : Uuid ) -> Vec < & GameNode > {
2023-10-19 13:57:02 +00:00
if node . id ( ) = = id {
return vec! [ node ] ;
}
for child in node . children ( ) {
let mut path = path_to_node ( child , id ) ;
if path . len ( ) > 1 {
path . push ( child ) ;
return path ;
}
}
Vec ::new ( )
}
2024-04-19 12:03:40 +00:00
* /
2023-10-19 13:57:02 +00:00
#[ cfg(test) ]
mod test {
use super ::* ;
use cool_asserts ::assert_matches ;
#[ test ]
fn it_can_create_an_empty_game_tree ( ) {
2024-03-23 17:40:06 +00:00
let tree = GameRecord ::new (
2023-10-19 13:57:02 +00:00
GameType ::Go ,
Size {
width : 19 ,
height : 19 ,
} ,
Player ::default ( ) ,
Player ::default ( ) ,
) ;
assert_eq! ( tree . nodes ( ) . len ( ) , 0 ) ;
}
#[ test ]
fn it_can_add_moves_to_a_game ( ) {
2024-03-23 17:40:06 +00:00
let mut game = GameRecord ::new (
2023-10-19 13:57:02 +00:00
GameType ::Go ,
Size {
width : 19 ,
height : 19 ,
} ,
Player ::default ( ) ,
Player ::default ( ) ,
) ;
2024-04-19 12:03:40 +00:00
/*
2023-10-19 13:57:02 +00:00
let first_move = MoveNode ::new ( Color ::Black , Move ::Move ( " dd " . to_owned ( ) ) ) ;
let first_ = game . add_child ( GameNode ::MoveNode ( first_move . clone ( ) ) ) ;
let second_move = MoveNode ::new ( Color ::White , Move ::Move ( " qq " . to_owned ( ) ) ) ;
first_ . add_child ( GameNode ::MoveNode ( second_move . clone ( ) ) ) ;
2024-04-19 12:03:40 +00:00
* /
2023-10-19 13:57:02 +00:00
2024-04-19 12:03:40 +00:00
/*
2023-10-19 13:57:02 +00:00
let nodes = game . nodes ( ) ;
assert_eq! ( nodes . len ( ) , 2 ) ;
assert_eq! ( nodes [ 0 ] . id ( ) , first_move . id ) ;
assert_eq! ( nodes [ 1 ] . id ( ) , second_move . id ) ;
2024-04-19 12:03:40 +00:00
* /
2023-10-19 13:57:02 +00:00
}
#[ ignore ]
#[ test ]
fn it_can_set_up_a_game ( ) {
unimplemented! ( )
}
#[ ignore ]
#[ test ]
fn it_can_load_tree_from_sgf ( ) {
unimplemented! ( )
}
#[ test ]
fn game_node_can_parse_sgf_move_node ( ) {
let n = parser ::Node {
properties : vec ! [
parser ::Property ::Move ( ( Color ::White , Move ::Move ( " dp " . to_owned ( ) ) ) ) ,
parser ::Property ::TimeLeft ( ( Color ::White , Duration ::from_secs ( 176 ) ) ) ,
parser ::Property ::Comment ( " Comments in the game " . to_owned ( ) ) ,
] ,
next : vec ! [ ] ,
} ;
assert_matches! ( GameNode ::try_from ( & n ) , Ok ( GameNode ::MoveNode ( _ ) ) ) ;
}
}
#[ cfg(test) ]
mod root_node_tests {
#[ ignore ]
#[ test ]
fn it_rejects_move_properties ( ) {
unimplemented! ( )
}
#[ ignore ]
#[ test ]
fn it_rejects_setup_properties ( ) {
unimplemented! ( )
}
#[ ignore ]
#[ test ]
fn it_can_parse_a_root_sgf ( ) {
unimplemented! ( )
}
}
#[ cfg(test) ]
mod move_node_tests {
use crate ::parser ::PositionList ;
use super ::* ;
use cool_asserts ::assert_matches ;
#[ test ]
fn it_can_parse_an_sgf_move_node ( ) {
let n = parser ::Node {
properties : vec ! [
parser ::Property ::Move ( ( Color ::White , Move ::Move ( " dp " . to_owned ( ) ) ) ) ,
parser ::Property ::TimeLeft ( ( Color ::White , Duration ::from_secs ( 176 ) ) ) ,
parser ::Property ::Comment ( " Comments in the game " . to_owned ( ) ) ,
] ,
next : vec ! [ ] ,
} ;
assert_matches! ( MoveNode ::try_from ( & n ) , Ok ( node ) = > {
assert_eq! ( node . color , Color ::White ) ;
assert_eq! ( node . mv , Move ::Move ( " dp " . to_owned ( ) ) ) ;
2024-04-19 12:03:40 +00:00
// assert_eq!(node.children, vec![]);
2023-10-19 13:57:02 +00:00
assert_eq! ( node . time_left , Some ( Duration ::from_secs ( 176 ) ) ) ;
assert_eq! ( node . comments , Some ( " Comments in the game " . to_owned ( ) ) ) ;
} ) ;
}
#[ test ]
fn it_rejects_an_sgf_setup_node ( ) {
let n = parser ::Node {
properties : vec ! [
parser ::Property ::Move ( ( Color ::White , Move ::Move ( " dp " . to_owned ( ) ) ) ) ,
parser ::Property ::TimeLeft ( ( Color ::White , Duration ::from_secs ( 176 ) ) ) ,
parser ::Property ::SetupBlackStones ( PositionList ( vec! [
" dd " . to_owned ( ) ,
" de " . to_owned ( ) ,
] ) ) ,
] ,
next : vec ! [ ] ,
} ;
assert_matches! (
MoveNode ::try_from ( & n ) ,
Err ( MoveNodeError ::IncompatibleProperty ( _ ) )
) ;
}
}
#[ cfg(test) ]
mod setup_node_tests {
use crate ::parser ::SetupInstr ;
use super ::* ;
use cool_asserts ::assert_matches ;
#[ ignore ]
#[ test ]
fn it_can_parse_an_sgf_setup_node ( ) {
unimplemented! ( )
}
#[ test ]
fn it_rejects_conflicting_placement_properties ( ) {
assert_matches! (
SetupNode ::new ( vec! [
SetupInstr ::Piece ( ( Color ::Black , " dd " . to_owned ( ) ) ) ,
SetupInstr ::Piece ( ( Color ::Black , " dd " . to_owned ( ) ) ) ,
] ) ,
Err ( SetupNodeError ::ConflictingPosition )
) ;
assert_matches! (
SetupNode ::new ( vec! [
SetupInstr ::Piece ( ( Color ::Black , " dd " . to_owned ( ) ) ) ,
SetupInstr ::Piece ( ( Color ::Black , " ee " . to_owned ( ) ) ) ,
SetupInstr ::Piece ( ( Color ::White , " ee " . to_owned ( ) ) ) ,
] ) ,
Err ( SetupNodeError ::ConflictingPosition )
) ;
}
}
#[ cfg(test) ]
mod path_test {
2024-03-24 19:50:59 +00:00
use super ::* ;
use cool_asserts ::assert_matches ;
use parser ::parse_collection ;
use std ::{ fs ::File , io ::Read } ;
fn with_text ( text : & str , f : impl FnOnce ( Vec < GameRecord > ) ) {
let ( _ , games ) = parse_collection ::< nom ::error ::VerboseError < & str > > ( text ) . unwrap ( ) ;
let games = games
. into_iter ( )
. map ( | game | GameRecord ::try_from ( & game ) . expect ( " game to parse " ) )
. collect ::< Vec < GameRecord > > ( ) ;
f ( games ) ;
}
fn with_file ( path : & std ::path ::Path , f : impl FnOnce ( Vec < GameRecord > ) ) {
let mut file = File ::open ( path ) . unwrap ( ) ;
let mut text = String ::new ( ) ;
let _ = file . read_to_string ( & mut text ) ;
with_text ( & text , f ) ;
}
#[ test ]
fn returns_the_mainline_of_a_game_without_branches ( ) {
with_file (
std ::path ::Path ::new ( " test_data/2020 USGO DDK, Round 1.sgf " ) ,
| games | {
let game = & games [ 0 ] ;
2024-04-19 12:03:40 +00:00
let moves = game
. mainline ( )
. expect ( " there should be a mainline in this file " )
. collect ::< Vec < & GameNode > > ( ) ;
2024-03-24 19:50:59 +00:00
assert_matches! ( moves [ 0 ] , GameNode ::MoveNode ( node ) = > {
assert_eq! ( node . color , Color ::Black ) ;
assert_eq! ( node . mv , Move ::Move ( " pp " . to_owned ( ) ) ) ;
} ) ;
assert_matches! ( moves [ 1 ] , GameNode ::MoveNode ( node ) = > {
assert_eq! ( node . color , Color ::White ) ;
assert_eq! ( node . mv , Move ::Move ( " dp " . to_owned ( ) ) ) ;
} ) ;
assert_matches! ( moves [ 2 ] , GameNode ::MoveNode ( node ) = > {
assert_eq! ( node . color , Color ::Black ) ;
assert_eq! ( node . mv , Move ::Move ( " pd " . to_owned ( ) ) ) ;
} ) ;
} ,
)
}
2024-03-24 20:04:24 +00:00
#[ test ]
fn returns_the_mainline_of_a_game_with_branches ( ) {
with_file ( std ::path ::Path ::new ( " test_data/branch_test.sgf " ) , | games | {
let game = & games [ 0 ] ;
2024-04-19 12:03:40 +00:00
let moves = game
. mainline ( )
. expect ( " there should be a mainline in this file " )
. collect ::< Vec < & GameNode > > ( ) ;
2024-03-24 20:04:24 +00:00
assert_matches! ( moves [ 1 ] , GameNode ::MoveNode ( node ) = > {
assert_eq! ( node . color , Color ::White ) ;
assert_eq! ( node . mv , Move ::Move ( " dd " . to_owned ( ) ) ) ;
} ) ;
assert_matches! ( moves [ 2 ] , GameNode ::MoveNode ( node ) = > {
assert_eq! ( node . color , Color ::Black ) ;
assert_eq! ( node . mv , Move ::Move ( " op " . to_owned ( ) ) ) ;
} ) ;
assert_matches! ( moves [ 3 ] , GameNode ::MoveNode ( node ) = > {
assert_eq! ( node . color , Color ::White ) ;
assert_eq! ( node . mv , Move ::Move ( " dp " . to_owned ( ) ) ) ;
} ) ;
} ) ;
}
2023-10-19 13:57:02 +00:00
#[ ignore ]
#[ test ]
fn returns_empty_list_if_no_game_nodes ( ) {
unimplemented! ( )
}
#[ ignore ]
#[ test ]
fn returns_empty_list_if_node_not_found ( ) {
unimplemented! ( )
}
#[ ignore ]
#[ test ]
fn path_excludes_root_node ( ) {
unimplemented! ( )
}
}
#[ cfg(test) ]
mod file_test {
use super ::* ;
use crate ::Win ;
use cool_asserts ::assert_matches ;
use parser ::parse_collection ;
use std ::{ fs ::File , io ::Read } ;
2024-03-23 17:40:06 +00:00
fn with_text ( text : & str , f : impl FnOnce ( Vec < GameRecord > ) ) {
2023-10-19 13:57:02 +00:00
let ( _ , games ) = parse_collection ::< nom ::error ::VerboseError < & str > > ( text ) . unwrap ( ) ;
let games = games
. into_iter ( )
2024-03-23 17:40:06 +00:00
. map ( | game | GameRecord ::try_from ( & game ) . expect ( " game to parse " ) )
. collect ::< Vec < GameRecord > > ( ) ;
2023-10-19 13:57:02 +00:00
f ( games ) ;
}
2024-03-23 17:40:06 +00:00
fn with_file ( path : & std ::path ::Path , f : impl FnOnce ( Vec < GameRecord > ) ) {
2023-10-19 13:57:02 +00:00
let mut file = File ::open ( path ) . unwrap ( ) ;
let mut text = String ::new ( ) ;
let _ = file . read_to_string ( & mut text ) ;
with_text ( & text , f ) ;
}
2023-10-20 04:36:03 +00:00
/// 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.
2023-10-19 13:57:02 +00:00
#[ test ]
2023-10-20 04:36:03 +00:00
fn it_can_load_an_ordinary_unannotated_game ( ) {
2023-10-19 13:57:02 +00:00
with_file (
std ::path ::Path ::new ( " test_data/2020 USGO DDK, Round 1.sgf " ) ,
| games | {
assert_eq! ( games . len ( ) , 1 ) ;
let game = & games [ 0 ] ;
assert_eq! ( game . game_type , GameType ::Go ) ;
assert_eq! (
game . board_size ,
Size {
width : 19 ,
height : 19
}
) ;
assert_eq! (
game . black_player ,
Player {
name : Some ( " savanni " . to_owned ( ) ) ,
rank : Some ( " 23k " . to_owned ( ) ) ,
team : None
}
) ;
assert_eq! (
game . white_player ,
Player {
name : Some ( " Geckoz " . to_owned ( ) ) ,
rank : None ,
team : None
}
) ;
assert_eq! ( game . app , Some ( " CGoban:3 " . to_owned ( ) ) ) ;
assert_eq! ( game . annotator , None ) ;
assert_eq! ( game . copyright , None ) ;
assert_eq! (
game . dates ,
vec! [ Date ::Date (
chrono ::NaiveDate ::from_ymd_opt ( 2020 , 8 , 5 ) . unwrap ( )
) ]
) ;
assert_eq! ( game . event , None ) ;
assert_eq! ( game . game_name , None ) ;
assert_eq! ( game . extra_info , None ) ;
assert_eq! ( game . opening_info , None ) ;
assert_eq! (
game . location ,
Some ( " The KGS Go Server at http://www.gokgs.com/ " . to_owned ( ) )
) ;
assert_eq! ( game . result , Some ( GameResult ::White ( Win ::Score ( 17.5 ) ) ) ) ;
assert_eq! ( game . round , None ) ;
assert_eq! ( game . rules , Some ( " AGA " . to_owned ( ) ) ) ;
assert_eq! ( game . source , None ) ;
assert_eq! ( game . time_limit , Some ( Duration ::from_secs ( 1800 ) ) ) ;
assert_eq! ( game . overtime , Some ( " 5x30 byo-yomi " . to_owned ( ) ) ) ;
assert_eq! ( game . transcriber , None ) ;
/*
Property {
ident : " KM " . to_owned ( ) ,
values : vec ! [ " 7.50 " . to_owned ( ) ] ,
} ,
] ;
for i in 0 .. 16 {
assert_eq! ( node . properties [ i ] , expected_properties [ i ] ) ;
}
* /
2024-04-19 12:03:40 +00:00
/*
2023-10-19 13:57:02 +00:00
let children = game . children ( ) ;
let node = children . first ( ) . unwrap ( ) ;
assert_matches! ( node , GameNode ::MoveNode ( node ) = > {
assert_eq! ( node . color , Color ::Black ) ;
assert_eq! ( node . mv , Move ::Move ( " pp " . to_owned ( ) ) ) ;
assert_eq! ( node . time_left , Some ( Duration ::from_secs ( 1795 ) ) ) ;
assert_eq! ( node . comments , Some ( " Geckoz [?]: Good game \n savanni [23k?]: There we go! This UI is... tough. \n savanni [23k?]: Have fun! Talk to you at the end. \n Geckoz [?]: Yeah, OGS is much better; I'm a UX professional \n " . to_owned ( ) )
) } ) ;
let children = node . children ( ) ;
let node = children . first ( ) . unwrap ( ) ;
assert_matches! ( node , GameNode ::MoveNode ( node ) = > {
assert_eq! ( node . color , Color ::White ) ;
assert_eq! ( node . mv , Move ::Move ( " dp " . to_owned ( ) ) ) ;
assert_eq! ( node . time_left , Some ( Duration ::from_secs ( 1765 ) ) ) ;
assert_eq! ( node . comments , None ) ;
} ) ;
2024-04-19 12:03:40 +00:00
* /
2023-10-19 13:57:02 +00:00
/*
let node = node . next ( ) . unwrap ( ) ;
let expected_properties = vec! [
Property {
ident : " W " . to_owned ( ) ,
values : vec ! [ " dp " . to_owned ( ) ] ,
} ,
Property {
ident : " WL " . to_owned ( ) ,
values : vec ! [ " 1765.099 " . to_owned ( ) ] ,
} ,
] ;
for i in 0 .. 2 {
assert_eq! ( node . properties [ i ] , expected_properties [ i ] ) ;
}
* /
} ,
) ;
}
}