2023-07-21 03:22:00 +00:00
// https://red-bean.com/sgf/
2023-07-03 14:26:20 +00:00
// https://red-bean.com/sgf/user_guide/index.html
// https://red-bean.com/sgf/sgf4.html
// todo: support collections in a file
// Properties to support. Remove each one as it gets support.
// B
// KO
// MN
// W
// AB
// AE
// AW
// PL
// C
// DM
// GB
// GW
// HO
// N
// UC
// V
// BM
// DO
// IT
// TE
// AR
// CR
// DD
// LB
// LN
// MA
// SL
// SQ
// TR
// AP
// CA
// FF
// GM
// ST
// SZ
// AN
// BR
// BT
// CP
// DT
// EV
// GN
// GC
// ON
// OT
// PB
// PC
// PW
// RE
// RO
// RU
// SO
// TM
// US
// WR
// WT
// BL
// OB
// OW
// WL
// FG
// PM
// VW
2023-07-22 03:48:05 +00:00
use crate ::{
2023-07-27 00:24:05 +00:00
date ::{ parse_date_field , Date } ,
tree ::{ Size , Tree } ,
Error ,
2023-07-22 03:48:05 +00:00
} ;
2023-07-26 01:08:22 +00:00
use serde ::{ Deserialize , Serialize } ;
2023-08-11 15:03:53 +00:00
use std ::ops ::Deref ;
2023-07-26 01:08:22 +00:00
use typeshare ::typeshare ;
2023-07-03 14:26:20 +00:00
2023-07-27 00:29:12 +00:00
#[ derive(Clone, Debug) ]
2023-07-27 00:24:05 +00:00
pub struct Game {
pub board_size : Size ,
pub info : GameInfo ,
2023-07-27 00:53:21 +00:00
pub tree : Tree ,
2023-07-27 00:24:05 +00:00
}
2023-08-11 15:03:53 +00:00
impl Deref for Game {
type Target = Tree ;
fn deref ( & self ) -> & Self ::Target {
& self . tree
}
}
2023-07-27 00:24:05 +00:00
impl TryFrom < Tree > for Game {
type Error = Error ;
2023-10-05 15:41:00 +00:00
#[ allow(clippy::field_reassign_with_default) ]
2023-07-27 00:24:05 +00:00
fn try_from ( tree : Tree ) -> Result < Self , Self ::Error > {
2023-08-11 15:03:53 +00:00
let board_size = match tree . root . find_prop ( " SZ " ) {
2023-07-27 00:24:05 +00:00
Some ( prop ) = > Size ::try_from ( prop . values [ 0 ] . as_str ( ) ) ? ,
None = > Size {
width : 19 ,
height : 19 ,
} ,
} ;
let mut info = GameInfo ::default ( ) ;
2023-08-11 15:03:53 +00:00
info . app_name = tree . root . find_prop ( " AP " ) . map ( | prop | prop . values [ 0 ] . clone ( ) ) ;
2023-08-20 16:53:14 +00:00
2023-08-11 15:03:53 +00:00
info . game_name = tree . root . find_prop ( " GN " ) . map ( | prop | prop . values [ 0 ] . clone ( ) ) ;
2023-08-20 16:53:14 +00:00
2023-08-11 15:03:53 +00:00
info . black_player = tree . root . find_prop ( " PB " ) . map ( | prop | prop . values . join ( " , " ) ) ;
2023-07-27 00:24:05 +00:00
2023-08-11 15:03:53 +00:00
info . black_rank = tree
. root
2023-07-27 00:24:05 +00:00
. find_prop ( " BR " )
. and_then ( | prop | Rank ::try_from ( prop . values [ 0 ] . as_str ( ) ) . ok ( ) ) ;
2023-08-11 15:03:53 +00:00
info . white_player = tree . root . find_prop ( " PW " ) . map ( | prop | prop . values . join ( " , " ) ) ;
2023-07-27 00:24:05 +00:00
2023-08-11 15:03:53 +00:00
info . white_rank = tree
. root
2023-07-27 00:24:05 +00:00
. find_prop ( " WR " )
. and_then ( | prop | Rank ::try_from ( prop . values [ 0 ] . as_str ( ) ) . ok ( ) ) ;
2023-08-11 15:03:53 +00:00
info . result = tree
. root
2023-07-27 00:24:05 +00:00
. find_prop ( " RE " )
. and_then ( | prop | GameResult ::try_from ( prop . values [ 0 ] . as_str ( ) ) . ok ( ) ) ;
2023-08-11 15:03:53 +00:00
info . time_limits = tree
. root
2023-07-27 00:24:05 +00:00
. find_prop ( " TM " )
. and_then ( | prop | prop . values [ 0 ] . parse ::< u64 > ( ) . ok ( ) )
2023-10-05 15:41:00 +00:00
. map ( std ::time ::Duration ::from_secs ) ;
2023-07-27 00:24:05 +00:00
2023-08-11 15:03:53 +00:00
info . date = tree
. root
2023-07-27 00:24:05 +00:00
. 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! [ ] ) ;
2023-08-11 15:03:53 +00:00
info . event = tree . root . find_prop ( " EV " ) . map ( | prop | prop . values . join ( " , " ) ) ;
2023-07-27 00:24:05 +00:00
2023-08-11 15:03:53 +00:00
info . round = tree . root . find_prop ( " RO " ) . map ( | prop | prop . values . join ( " , " ) ) ;
2023-07-27 00:24:05 +00:00
2023-08-11 15:03:53 +00:00
info . source = tree . root . find_prop ( " SO " ) . map ( | prop | prop . values . join ( " , " ) ) ;
2023-07-27 00:24:05 +00:00
2023-08-11 15:03:53 +00:00
info . game_keeper = tree . root . find_prop ( " US " ) . map ( | prop | prop . values . join ( " , " ) ) ;
2023-07-27 00:24:05 +00:00
Ok ( Game {
board_size ,
info ,
2023-07-27 00:53:21 +00:00
tree ,
2023-07-27 00:24:05 +00:00
} )
}
}
2023-07-03 14:26:20 +00:00
2023-07-26 01:08:22 +00:00
#[ derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize) ]
#[ typeshare ]
2023-07-21 03:22:00 +00:00
pub enum Rank {
Kyu ( u8 ) ,
Dan ( u8 ) ,
Pro ( u8 ) ,
}
impl TryFrom < & str > for Rank {
type Error = String ;
fn try_from ( r : & str ) -> Result < Rank , Self ::Error > {
2023-10-05 15:41:00 +00:00
let parts = r . split ( ' ' ) . map ( | s | s . to_owned ( ) ) . collect ::< Vec < String > > ( ) ;
2023-07-21 03:22:00 +00:00
let cnt = parts [ 0 ] . parse ::< u8 > ( ) . map_err ( | err | format! ( " {:?} " , err ) ) ? ;
match parts [ 1 ] . to_ascii_lowercase ( ) . as_str ( ) {
" kyu " = > Ok ( Rank ::Kyu ( cnt ) ) ,
" dan " = > Ok ( Rank ::Dan ( cnt ) ) ,
" pro " = > Ok ( Rank ::Pro ( cnt ) ) ,
_ = > Err ( " unparsable " . to_owned ( ) ) ,
}
}
}
2023-07-26 03:27:36 +00:00
impl ToString for Rank {
fn to_string ( & self ) -> String {
unimplemented! ( )
}
}
2023-07-21 03:22:00 +00:00
#[ derive(Clone, Debug, Default) ]
2023-07-03 14:26:20 +00:00
pub struct GameInfo {
2023-07-27 00:53:21 +00:00
pub app_name : Option < String > ,
2023-07-03 14:26:20 +00:00
pub annotator : Option < String > ,
pub copyright : Option < String > ,
pub event : Option < String > ,
// Games can be played across multiple days, even multiple years. The format specifies
// shortcuts.
2023-07-22 03:48:05 +00:00
pub date : Vec < Date > ,
2023-07-03 14:26:20 +00:00
pub location : Option < String > ,
// special rules for the round-number and type
pub round : Option < String > ,
pub ruleset : Option < String > ,
pub source : Option < String > ,
pub time_limits : Option < std ::time ::Duration > ,
pub game_keeper : Option < String > ,
2023-07-21 14:47:30 +00:00
pub komi : Option < f32 > ,
2023-07-03 14:26:20 +00:00
pub game_name : Option < String > ,
pub game_comments : Option < String > ,
pub black_player : Option < String > ,
2023-07-21 03:22:00 +00:00
pub black_rank : Option < Rank > ,
2023-07-03 14:26:20 +00:00
pub black_team : Option < String > ,
pub white_player : Option < String > ,
2023-07-21 03:22:00 +00:00
pub white_rank : Option < Rank > ,
2023-07-03 14:26:20 +00:00
pub white_team : Option < String > ,
pub opening : Option < String > ,
pub overtime : Option < String > ,
pub result : Option < GameResult > ,
}
2023-07-22 03:48:05 +00:00
/*
2023-07-03 14:26:20 +00:00
enum PropType {
Move ,
Setup ,
Root ,
GameInfo ,
}
enum PropValue {
Empty ,
Number ,
Real ,
Double ,
Color ,
SimpleText ,
Text ,
Point ,
Move ,
Stone ,
}
2023-07-22 03:48:05 +00:00
* /
2023-07-03 14:26:20 +00:00
#[ cfg(test) ]
mod tests {
use super ::* ;
2023-07-27 00:24:05 +00:00
use crate ::{
date ::Date ,
2023-08-11 15:03:53 +00:00
tree ::{ parse_collection , Property , Size } ,
2023-07-27 00:24:05 +00:00
} ;
2023-07-03 14:26:20 +00:00
use std ::fs ::File ;
use std ::io ::Read ;
const EXAMPLE : & 'static str = " (;FF[4]C[root](;C[a];C[b](;C[c])
( ; C [ d ] ; C [ e ] ) )
( ; C [ f ] ( ; C [ g ] ; C [ h ] ; C [ i ] )
( ; C [ j ] ) ) ) " ;
2023-07-27 00:24:05 +00:00
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 > > ( ) ;
2023-07-03 14:26:20 +00:00
f ( games ) ;
}
2023-07-27 00:24:05 +00:00
fn with_file ( path : & std ::path ::Path , f : impl FnOnce ( Vec < Game > ) ) {
2023-07-03 14:26:20 +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 ) ;
}
#[ test ]
fn it_parses_game_root ( ) {
with_text ( EXAMPLE , | trees | {
assert_eq! ( trees . len ( ) , 1 ) ;
let tree = & trees [ 0 ] ;
2023-07-27 00:53:21 +00:00
assert_eq! ( tree . info . app_name , None ) ;
2023-07-03 14:26:20 +00:00
assert_eq! (
tree . board_size ,
Size {
width : 19 ,
height : 19
}
) ;
// assert_eq!(tree.text, EXAMPLE.to_owned());
} ) ;
with_file ( std ::path ::Path ::new ( " test_data/print1.sgf " ) , | trees | {
assert_eq! ( trees . len ( ) , 1 ) ;
let tree = & trees [ 0 ] ;
2023-07-27 00:53:21 +00:00
assert_eq! ( tree . info . app_name , None ) ;
2023-07-03 14:26:20 +00:00
assert_eq! (
tree . board_size ,
Size {
width : 19 ,
height : 19
}
) ;
} ) ;
}
2023-07-21 03:22:00 +00:00
#[ test ]
fn it_parses_game_info ( ) {
with_file ( std ::path ::Path ::new ( " test_data/print1.sgf " ) , | trees | {
assert_eq! ( trees . len ( ) , 1 ) ;
let tree = & trees [ 0 ] ;
assert_eq! ( tree . info . black_player , Some ( " Takemiya Masaki " . to_owned ( ) ) ) ;
assert_eq! ( tree . info . black_rank , Some ( Rank ::Dan ( 9 ) ) ) ;
assert_eq! ( tree . info . white_player , Some ( " Cho Chikun " . to_owned ( ) ) ) ;
assert_eq! ( tree . info . white_rank , Some ( Rank ::Dan ( 9 ) ) ) ;
assert_eq! ( tree . info . result , Some ( GameResult ::White ( Win ::Resignation ) ) ) ;
assert_eq! (
tree . info . time_limits ,
Some ( std ::time ::Duration ::from_secs ( 28800 ) )
) ;
assert_eq! (
2023-07-21 14:47:30 +00:00
tree . info . date ,
2023-07-21 03:22:00 +00:00
vec! [
2023-07-22 03:48:05 +00:00
Date ::Date ( chrono ::NaiveDate ::from_ymd_opt ( 1996 , 10 , 18 ) . unwrap ( ) ) ,
Date ::Date ( chrono ::NaiveDate ::from_ymd_opt ( 1996 , 10 , 19 ) . unwrap ( ) ) ,
2023-07-21 03:22:00 +00:00
]
) ;
assert_eq! ( tree . info . event , Some ( " 21st Meijin " . to_owned ( ) ) ) ;
2023-07-22 04:15:17 +00:00
assert_eq! ( tree . info . round , Some ( " 2 (final) " . to_owned ( ) ) ) ;
2023-07-21 03:22:00 +00:00
assert_eq! ( tree . info . source , Some ( " Go World #78 " . to_owned ( ) ) ) ;
assert_eq! ( tree . info . game_keeper , Some ( " Arno Hollosi " . to_owned ( ) ) ) ;
} ) ;
}
2023-08-11 15:03:53 +00:00
#[ test ]
fn it_presents_the_mainline_of_game_without_branches ( ) {
with_file (
std ::path ::Path ::new ( " test_data/2020 USGO DDK, Round 1.sgf " ) ,
| trees | {
assert_eq! ( trees . len ( ) , 1 ) ;
let tree = & trees [ 0 ] ;
let node = & tree . root ;
assert_eq! ( node . properties . len ( ) , 16 ) ;
let expected_properties = vec! [
Property {
ident : " GM " . to_owned ( ) ,
values : vec ! [ " 1 " . to_owned ( ) ] ,
} ,
Property {
ident : " FF " . to_owned ( ) ,
values : vec ! [ " 4 " . to_owned ( ) ] ,
} ,
Property {
ident : " CA " . to_owned ( ) ,
values : vec ! [ " UTF-8 " . to_owned ( ) ] ,
} ,
Property {
ident : " AP " . to_owned ( ) ,
values : vec ! [ " CGoban:3 " . to_owned ( ) ] ,
} ,
Property {
ident : " ST " . to_owned ( ) ,
values : vec ! [ " 2 " . to_owned ( ) ] ,
} ,
Property {
ident : " RU " . to_owned ( ) ,
values : vec ! [ " AGA " . to_owned ( ) ] ,
} ,
Property {
ident : " SZ " . to_owned ( ) ,
values : vec ! [ " 19 " . to_owned ( ) ] ,
} ,
Property {
ident : " KM " . to_owned ( ) ,
values : vec ! [ " 7.50 " . to_owned ( ) ] ,
} ,
Property {
ident : " TM " . to_owned ( ) ,
values : vec ! [ " 1800 " . to_owned ( ) ] ,
} ,
Property {
ident : " OT " . to_owned ( ) ,
values : vec ! [ " 5x30 byo-yomi " . to_owned ( ) ] ,
} ,
Property {
ident : " PW " . to_owned ( ) ,
values : vec ! [ " Geckoz " . to_owned ( ) ] ,
} ,
Property {
ident : " PB " . to_owned ( ) ,
values : vec ! [ " savanni " . to_owned ( ) ] ,
} ,
Property {
ident : " BR " . to_owned ( ) ,
values : vec ! [ " 23k " . to_owned ( ) ] ,
} ,
Property {
ident : " DT " . to_owned ( ) ,
values : vec ! [ " 2020-08-05 " . to_owned ( ) ] ,
} ,
Property {
ident : " PC " . to_owned ( ) ,
values : vec ! [ " The KGS Go Server at http://www.gokgs.com/ " . to_owned ( ) ] ,
} ,
Property {
ident : " RE " . to_owned ( ) ,
values : vec ! [ " W+17.50 " . to_owned ( ) ] ,
} ,
] ;
for i in 0 .. 16 {
assert_eq! ( node . properties [ i ] , expected_properties [ i ] ) ;
}
let node = node . next ( ) . unwrap ( ) ;
let expected_properties = vec! [
Property {
ident : " B " . to_owned ( ) ,
values : vec ! [ " pp " . to_owned ( ) ] ,
} ,
Property {
ident : " BL " . to_owned ( ) ,
values : vec ! [ " 1795.449 " . to_owned ( ) ] ,
} ,
Property {
ident : " C " . to_owned ( ) ,
values : vec ! [ " 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 ( ) ] ,
}
] ;
for i in 0 .. 3 {
assert_eq! ( node . properties [ i ] , expected_properties [ i ] ) ;
}
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 ] ) ;
}
} ,
) ;
}
2023-07-03 14:26:20 +00:00
}