From bc1f4395e5a73d206e5573bde9519679155fb345 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Thu, 22 Jun 2023 23:58:16 -0400 Subject: [PATCH] Start parsing data into a GameTree --- go-sgf/src/lib.rs | 150 ++++++++++++++++++-------------- go-sgf/test_data/ff4_ex.sgf | 165 ++++++++++++++++++++++++++++++++++++ go-sgf/test_data/print1.sgf | 35 ++++++++ go-sgf/test_data/print2.sgf | 50 +++++++++++ 4 files changed, 337 insertions(+), 63 deletions(-) create mode 100644 go-sgf/test_data/ff4_ex.sgf create mode 100644 go-sgf/test_data/print1.sgf create mode 100644 go-sgf/test_data/print2.sgf diff --git a/go-sgf/src/lib.rs b/go-sgf/src/lib.rs index 2d5dd6f..bc24212 100644 --- a/go-sgf/src/lib.rs +++ b/go-sgf/src/lib.rs @@ -69,11 +69,11 @@ use nom::{ bytes::complete::{tag, take_until}, - character::complete::{alpha1, anychar, multispace0}, + character::complete::{alpha1, anychar, digit1, multispace0}, combinator::eof, - multi::{many0, many1, many_till}, + multi::{many0, many1, many_till, separated_list1}, sequence::{delimited, terminated}, - IResult, Parser, + Finish, IResult, Parser, }; use thiserror::Error; @@ -85,6 +85,12 @@ pub enum ParseError { UnknownError, } +impl From> for ParseError { + fn from(_: nom::error::Error<&str>) -> Self { + Self::UnknownError + } +} + // todo: support ST root node #[derive(Debug)] pub struct GameTree { @@ -181,6 +187,37 @@ enum PropValue { Stone, } +pub fn parse_sgf(input: &str) -> Result { + let (_, tree) = parse_tree(input).finish()?; + + let file_format = match tree.sequence[0].find_prop("FF") { + Some(prop) => prop.values[0].parse::().unwrap(), + None => 4, + }; + let app = tree.sequence[0] + .find_prop("AP") + .map(|prop| prop.values[0].clone()); + let board_size = match tree.sequence[0].find_prop("SZ") { + Some(prop) => { + let (_, size) = parse_size(prop.values[0].as_str()).finish()?; + size + } + None => Size { + width: 19, + height: 19, + }, + }; + + Ok(GameTree { + file_format, + + app, + game_type: GameType::Go, + board_size, + text: input.to_owned(), + }) +} + #[derive(Debug, PartialEq)] struct Tree { sequence: Vec, @@ -219,7 +256,16 @@ impl ToString for Node { } } -#[derive(Debug, PartialEq)] +impl Node { + fn find_prop(&self, ident: &str) -> Option { + self.properties + .iter() + .find(|prop| prop.ident == ident) + .cloned() + } +} + +#[derive(Clone, Debug, PartialEq)] struct Property { ident: String, values: Vec, @@ -239,13 +285,6 @@ impl ToString for Property { // note: must preserve unknown properties // note: must fix or preserve illegally formatted game-info properties // note: must correct or delete illegally foramtted properties, but display a warning -/* -pub fn parse_sgf(input: &str) -> Result<(GameTree, Vec), ParseError> { - let (_, gameinfo) = parse_gametree(input).unwrap(); - Ok((gameinfo, vec![])) -} -*/ - fn parse_tree(input: &str) -> IResult<&str, Tree> { println!("parse_tree: {}", input); let (input, _) = multispace0(input)?; @@ -277,6 +316,7 @@ fn parse_node(input: &str) -> IResult<&str, Node> { fn parse_property(input: &str) -> IResult<&str, Property> { println!("parse_property: {}", input); + let (input, _) = multispace0(input)?; let (input, ident) = alpha1(input)?; let (input, values) = many1(delimited(tag("["), take_until("]"), tag("]")))(input)?; @@ -293,49 +333,30 @@ fn parse_property(input: &str) -> IResult<&str, Property> { )) } -/* -fn parse_gametree(input: &str) -> IResult<&str, GameTree> { - let (input, _) = tag("(;")(input)?; - let (input, properties) = many1(parse_property)(input)?; - let (input, _) = tag(")")(input)?; - println!("properties: {:?}", properties); - Ok((input, unimplemented!())) -} - -*/ - -pub fn add(left: usize, right: usize) -> usize { - left + right +fn parse_size(input: &str) -> IResult<&str, Size> { + let (input, dimensions) = separated_list1(tag(":"), digit1)(input)?; + let (width, height) = match dimensions.as_slice() { + [width] => (width.parse::().unwrap(), width.parse::().unwrap()), + [width, height] => ( + width.parse::().unwrap(), + height.parse::().unwrap(), + ), + _ => (19, 19), + }; + Ok((input, Size { width, height })) } #[cfg(test)] mod tests { + use std::{fs::File, io::Read}; + use super::*; - const EXAMPLE_1: &'static str = "(;FF[4]C[root](;C[a];C[b](;C[c]) + 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])))"; - const EXAMPLE_2: &'static str = "(;FF[4]GM[1]SZ[19]AP[SGFC:1.13b] - -PB[troy]BR[12k*] -PW[john]WR[11k*] -KM[0.5]RE[W+12.5] -DT[1998-06-15] -TM[600] - -;B[pd];W[dp];B[pq];W[dd];B[qk];W[jd];B[fq];W[dj];B[jp];W[jj] -;B[cn]LB[dn:A][po:B]C[dada: other ideas are 'A' (d6) or 'B' (q5)] -;W[eo](;B[dl]C[dada: hm - looks troublesome. -Usually B plays the 3,3 invasion - see variation];W[qo];B[qp] -... -;W[sr];B[sk];W[sg];B[pa];W[gc];B[pi];W[ph];B[de];W[ed];B[kn] -;W[dh];B[eh];W[se];B[sd];W[af];B[ie];W[id];B[hf];W[hd];B[if] -;W[fp];B[gq];W[qj];B[sj];W[rh];B[sn];W[so];B[sm];W[ep];B[mn]) -... -(;W[dq]N[wrong direction];B[qo];W[qp]))"; - #[test] fn it_can_parse_properties() { let (_, prop) = parse_property("C[a]").unwrap(); @@ -479,7 +500,7 @@ Usually B plays the 3,3 invasion - see variation];W[qo];B[qp] #[test] fn it_can_parse_example_1() { - let (_, ex_tree) = parse_tree(EXAMPLE_1).unwrap(); + let (_, ex_tree) = parse_tree(EXAMPLE).unwrap(); assert_eq!(ex_tree.sequence.len(), 1); assert_eq!(ex_tree.sequence[0].properties.len(), 2); @@ -515,7 +536,7 @@ Usually B plays the 3,3 invasion - see variation];W[qo];B[qp] #[test] fn it_can_regenerate_the_tree() { - let (_, tree1) = parse_tree(EXAMPLE_1).unwrap(); + let (_, tree1) = parse_tree(EXAMPLE).unwrap(); println!("{}", tree1.to_string()); assert_eq!( tree1.to_string(), @@ -525,41 +546,44 @@ Usually B plays the 3,3 invasion - see variation];W[qo];B[qp] assert_eq!(tree1, tree2); } - /* - fn with_examples(f: impl FnOnce(GameTree, GameTree)) { - let (example_1, _) = parse_sgf(EXAMPLE_1).unwrap(); - let (example_2, _) = parse_sgf(EXAMPLE_2).unwrap(); + fn with_text(text: &str, f: impl FnOnce(GameTree)) { + f(parse_sgf(text).unwrap()); + } - f(example_1, example_2); + fn with_file(path: &std::path::Path, f: impl FnOnce(GameTree)) { + 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_examples(|ex_1, ex_2| { - assert_eq!(ex_1.file_format, 4); - assert_eq!(ex_1.app, None); - assert_eq!(ex_1.game_type, GameType::Go); + with_text(EXAMPLE, |tree| { + assert_eq!(tree.file_format, 4); + assert_eq!(tree.app, None); + assert_eq!(tree.game_type, GameType::Go); assert_eq!( - ex_1.board_size, + tree.board_size, Size { width: 19, height: 19 } ); - assert_eq!(ex_1.text, EXAMPLE_1.to_owned()); + assert_eq!(tree.text, EXAMPLE.to_owned()); + }); - assert_eq!(ex_2.file_format, 4); - assert_eq!(ex_2.app, Some("SGFC:1.13b".to_owned())); - assert_eq!(ex_2.game_type, GameType::Go); + with_file(std::path::Path::new("test_data/print1.sgf"), |tree| { + assert_eq!(tree.file_format, 4); + assert_eq!(tree.app, None); + assert_eq!(tree.game_type, GameType::Go); assert_eq!( - ex_2.board_size, + tree.board_size, Size { width: 19, height: 19 } ); - assert_eq!(ex_2.text, EXAMPLE_2.to_owned()); }); } - */ } diff --git a/go-sgf/test_data/ff4_ex.sgf b/go-sgf/test_data/ff4_ex.sgf new file mode 100644 index 0000000..664527a --- /dev/null +++ b/go-sgf/test_data/ff4_ex.sgf @@ -0,0 +1,165 @@ +(;FF[4]AP[Primiview:3.1]GM[1]SZ[19]GN[Gametree 1: properties]US[Arno Hollosi] + +(;B[pd]N[Moves, comments, annotations] +C[Nodename set to: "Moves, comments, annotations"];W[dp]GW[1] +C[Marked as "Good for White"];B[pp]GB[2] +C[Marked as "Very good for Black"];W[dc]GW[2] +C[Marked as "Very good for White"];B[pj]DM[1] +C[Marked as "Even position"];W[ci]UC[1] +C[Marked as "Unclear position"];B[jd]TE[1] +C[Marked as "Tesuji" or "Good move"];W[jp]BM[2] +C[Marked as "Very bad move"];B[gd]DO[] +C[Marked as "Doubtful move"];W[de]IT[] +C[Marked as "Interesting move"];B[jj]; +C[White "Pass" move]W[]; +C[Black "Pass" move]B[tt]) + +(;AB[dd][de][df][dg][do:gq] + AW[jd][je][jf][jg][kn:lq][pn:pq] +N[Setup]C[Black & white stones at the top are added as single stones. + +Black & white stones at the bottom are added using compressed point lists.] +;AE[ep][fp][kn][lo][lq][pn:pq] +C[AddEmpty + +Black stones & stones of left white group are erased in FF[3\] way. + +White stones at bottom right were erased using compressed point list.] +;AB[pd]AW[pp]PL[B]C[Added two stones. + +Node marked with "Black to play".];PL[W] +C[Node marked with "White to play"]) + +(;AB[dd][de][df][dg][dh][di][dj][nj][ni][nh][nf][ne][nd][ij][ii][ih][hq] +[gq][fq][eq][dr][ds][dq][dp][cp][bp][ap][iq][ir][is][bo][bn][an][ms][mr] +AW[pd][pe][pf][pg][ph][pi][pj][fd][fe][ff][fh][fi][fj][kh][ki][kj][os][or] +[oq][op][pp][qp][rp][sp][ro][rn][sn][nq][mq][lq][kq][kr][ks][fs][gs][gr] +[er]N[Markup]C[Position set up without compressed point lists.] + +;TR[dd][de][df][ed][ee][ef][fd:ff] + MA[dh][di][dj][ej][ei][eh][fh:fj] + CR[nd][ne][nf][od][oe][of][pd:pf] + SQ[nh][ni][nj][oh][oi][oj][ph:pj] + SL[ih][ii][ij][jj][ji][jh][kh:kj] + TW[pq:ss][so][lr:ns] + TB[aq:cs][er:hs][ao] +C[Markup at top partially using compressed point lists (for markup on white stones); listed clockwise, starting at upper left: +- TR (triangle) +- CR (circle) +- SQ (square) +- SL (selected points) +- MA ('X') + +Markup at bottom: black & white territory (using compressed point lists)] +;LB[dc:1][fc:2][nc:3][pc:4][dj:a][fj:b][nj:c] +[pj:d][gs:ABCDEFGH][gr:ABCDEFG][gq:ABCDEF][gp:ABCDE][go:ABCD][gn:ABC][gm:AB] +[mm:12][mn:123][mo:1234][mp:12345][mq:123456][mr:1234567][ms:12345678] +C[Label (LB property) + +Top: 8 single char labels (1-4, a-d) + +Bottom: Labels up to 8 char length.] + +;DD[kq:os][dq:hs] +AR[aa:sc][sa:ac][aa:sa][aa:ac][cd:cj] + [gd:md][fh:ij][kj:nh] +LN[pj:pd][nf:ff][ih:fj][kh:nj] +C[Arrows, lines and dimmed points.]) + +(;B[qd]N[Style & text type] +C[There are hard linebreaks & soft linebreaks. +Soft linebreaks are linebreaks preceeded by '\\' like this one >o\ +k<. Hard line breaks are all other linebreaks. +Soft linebreaks are converted to >nothing<, i.e. removed. + +Note that linebreaks are coded differently on different systems. + +Examples (>ok< shouldn't be split): + +linebreak 1 "\\n": >o\ +k< +linebreak 2 "\\n\\r": >o\ + k< +linebreak 3 "\\r\\n": >o\ +k< +linebreak 4 "\\r": >o\ k<] + +(;W[dd]N[W d16]C[Variation C is better.](;B[pp]N[B q4]) +(;B[dp]N[B d4]) +(;B[pq]N[B q3]) +(;B[oq]N[B p3]) +) +(;W[dp]N[W d4]) +(;W[pp]N[W q4]) +(;W[cc]N[W c17]) +(;W[cq]N[W c3]) +(;W[qq]N[W r3]) +) + +(;B[qr]N[Time limits, captures & move numbers] +BL[120.0]C[Black time left: 120 sec];W[rr] +WL[300]C[White time left: 300 sec];B[rq] +BL[105.6]OB[10]C[Black time left: 105.6 sec +Black stones left (in this byo-yomi period): 10];W[qq] +WL[200]OW[2]C[White time left: 200 sec +White stones left: 2];B[sr] +BL[87.00]OB[9]C[Black time left: 87 sec +Black stones left: 9];W[qs] +WL[13.20]OW[1]C[White time left: 13.2 sec +White stones left: 1];B[rs] +C[One white stone at s2 captured];W[ps];B[pr];W[or] +MN[2]C[Set move number to 2];B[os] +C[Two white stones captured +(at q1 & r1)] +;MN[112]W[pq]C[Set move number to 112];B[sq];W[rp];B[ps] +;W[ns];B[ss];W[nr] +;B[rr];W[sp];B[qs]C[Suicide move +(all B stones get captured)]) +) + +(;FF[4]AP[Primiview:3.1]GM[1]SZ[19]C[Gametree 2: game-info + +Game-info properties are usually stored in the root node. +If games are merged into a single game-tree, they are stored in the node\ + where the game first becomes distinguishable from all other games in\ + the tree.] +;B[pd] +(;PW[W. Hite]WR[6d]RO[2]RE[W+3.5] +PB[B. Lack]BR[5d]PC[London]EV[Go Congress]W[dp] +C[Game-info: +Black: B. Lack, 5d +White: W. Hite, 6d +Place: London +Event: Go Congress +Round: 2 +Result: White wins by 3.5]) +(;PW[T. Suji]WR[7d]RO[1]RE[W+Resign] +PB[B. Lack]BR[5d]PC[London]EV[Go Congress]W[cp] +C[Game-info: +Black: B. Lack, 5d +White: T. Suji, 7d +Place: London +Event: Go Congress +Round: 1 +Result: White wins by resignation]) +(;W[ep];B[pp] +(;PW[S. Abaki]WR[1d]RO[3]RE[B+63.5] +PB[B. Lack]BR[5d]PC[London]EV[Go Congress]W[ed] +C[Game-info: +Black: B. Lack, 5d +White: S. Abaki, 1d +Place: London +Event: Go Congress +Round: 3 +Result: Balck wins by 63.5]) +(;PW[A. Tari]WR[12k]KM[-59.5]RO[4]RE[B+R] +PB[B. Lack]BR[5d]PC[London]EV[Go Congress]W[cd] +C[Game-info: +Black: B. Lack, 5d +White: A. Tari, 12k +Place: London +Event: Go Congress +Round: 4 +Komi: -59.5 points +Result: Black wins by resignation]) +)) diff --git a/go-sgf/test_data/print1.sgf b/go-sgf/test_data/print1.sgf new file mode 100644 index 0000000..024a461 --- /dev/null +++ b/go-sgf/test_data/print1.sgf @@ -0,0 +1,35 @@ +(;FF[4]GM[1]SZ[19]FG[257:Figure 1]PM[1] +PB[Takemiya Masaki]BR[9 dan]PW[Cho Chikun] +WR[9 dan]RE[W+Resign]KM[5.5]TM[28800]DT[1996-10-18,19] +EV[21st Meijin]RO[2 (final)]SO[Go World #78]US[Arno Hollosi] +;B[pd];W[dp];B[pp];W[dd];B[pj];W[nc];B[oe];W[qc];B[pc];W[qd] +(;B[qf];W[rf];B[rg];W[re];B[qg];W[pb];B[ob];W[qb] +(;B[mp];W[fq];B[ci];W[cg];B[dl];W[cn];B[qo];W[ec];B[jp];W[jd] +;B[ei];W[eg];B[kk]LB[qq:a][dj:b][ck:c][qp:d]N[Figure 1] + +;W[me]FG[257:Figure 2];B[kf];W[ke];B[lf];W[jf];B[jg] +(;W[mf];B[if];W[je];B[ig];W[mg];B[mj];W[mq];B[lq];W[nq] +(;B[lr];W[qq];B[pq];W[pr];B[rq];W[rr];B[rp];W[oq];B[mr];W[oo];B[mn] +(;W[nr];B[qp]LB[kd:a][kh:b]N[Figure 2] + +;W[pk]FG[257:Figure 3];B[pm];W[oj];B[ok];W[qr];B[os];W[ol];B[nk];W[qj] +;B[pi];W[pl];B[qm];W[ns];B[sr];W[om];B[op];W[qi];B[oi] +(;W[rl];B[qh];W[rm];B[rn];W[ri];B[ql];W[qk];B[sm];W[sk];B[sh];W[og] +;B[oh];W[np];B[no];W[mm];B[nn];W[lp];B[kp];W[lo];B[ln];W[ko];B[mo] +;W[jo];B[km]N[Figure 3]) + +(;W[ql]VW[ja:ss]FG[257:Dia. 6]MN[1];B[rm];W[ph];B[oh];W[pg];B[og];W[pf] +;B[qh];W[qe];B[sh];W[of];B[sj]TR[oe][pd][pc][ob]LB[pe:a][sg:b][si:c] +N[Diagram 6])) + +(;W[no]VW[jj:ss]FG[257:Dia. 5]MN[1];B[pn]N[Diagram 5])) + +(;B[pr]FG[257:Dia. 4]MN[1];W[kq];B[lp];W[lr];B[jq];W[jr];B[kp];W[kr];B[ir] +;W[hr]LB[is:a][js:b][or:c]N[Diagram 4])) + +(;W[if]FG[257:Dia. 3]MN[1];B[mf];W[ig];B[jh]LB[ki:a]N[Diagram 3])) + +(;W[oc]VW[aa:sk]FG[257:Dia. 2]MN[1];B[md];W[mc];B[ld]N[Diagram 2])) + +(;B[qe]VW[aa:sj]FG[257:Dia. 1]MN[1];W[re];B[qf];W[rf];B[qg];W[pb];B[ob] +;W[qb]LB[rg:a]N[Diagram 1])) diff --git a/go-sgf/test_data/print2.sgf b/go-sgf/test_data/print2.sgf new file mode 100644 index 0000000..3fbfcb0 --- /dev/null +++ b/go-sgf/test_data/print2.sgf @@ -0,0 +1,50 @@ +(;FF[4]GM[1]SZ[19]FG[257:Figure 1]PM[2] +PB[Cho Chikun]BR[9 dan]PW[Ryu Shikun]WR[9 dan]RE[W+2.5]KM[5.5] +DT[1996-08]EV[51st Honinbo]RO[5 (final)]SO[Go World #78]US[Arno Hollosi] +;B[qd];W[dd];B[fc];W[df];B[pp];W[dq];B[kc];W[cn];B[pj];W[jp];B[lq];W[oe] +;B[pf];W[ke];B[id];W[lc];B[lb];W[kb];B[jb];W[kd];B[ka];W[jc];B[ic];W[kb] +;B[mc];W[qc]N[Figure 1] + +;B[pd]FG[257:Figure 2];W[pc];B[od];W[oc];B[kc];W[nd];B[nc];W[kb];B[rd];W[pe] +(;B[rf];W[md];B[kc];W[qe];B[re];W[kb];B[mb];W[qf];B[qg];W[pg];B[qh];W[kc] +;B[hb];W[nf];B[ch];W[cj];B[eh];W[ob] +(;B[cc];W[dc];B[db];W[bf];B[bb] +;W[bh]LB[of:a][mf:b][rc:c][di:d][ja:e]N[Figure 2] + +;B[qp]FG[257:Figure 3];W[lo];B[ej];W[oq] +(;B[np];W[mq];B[mp];W[lp] +(;B[kq];W[nq];B[op];W[jq];B[mr];W[nr];B[lr];W[qr];B[jr];W[ir];B[hr];W[iq] +;B[is];W[ks];B[js];W[gq];B[gr];W[fq];B[pq];W[pr];B[ns];W[or];B[rq];W[hq] +;B[rr];W[cl];B[cg];W[bg];B[og];W[ng] +(;B[ci];W[bi];B[dj];W[dk];B[mm];W[gk];B[gi];W[mn];B[nm];W[kl];B[nh];W[mh] +;B[mi];W[li];B[lh];W[mg];B[ek];W[el];B[ik]LB[kr:a]N[Figure 3] + +;W[ki]FG[257:Figure 4];B[fl];W[fk];B[gl];W[hk];B[hl];W[hj];B[jl];W[kk];B[km] +;W[lm];B[ll];W[jm];B[jj];W[ji];B[kj];W[lj];B[ij];W[hi];B[em];W[dl];B[ii] +;W[hh];B[ih];W[hg];B[ln];W[kn];B[lm];W[im];B[il];W[fg];B[lk];W[ni];B[ef] +;W[eg];B[dg];W[ff];B[oh];W[of];B[oj];W[ph];B[oi];W[mj];B[ee];W[fe];B[de] +;W[ed];B[ce];W[cf];B[rb];W[rc];B[sc];W[qb];B[sb];W[la];B[ma];W[na];B[ja] +;W[nb];B[la];W[pa];B[be];W[fd];B[bj];W[ck];B[ec];W[hs];B[gs];W[fr];B[os] +;W[ps];B[ms];W[nk];B[ok];W[kp];B[fo];W[fs];B[qq];W[hs];B[do];W[co];B[ig] +;W[gc];B[gb];W[jf];B[di];W[fi];B[hf];W[gf];B[af];W[mo];B[he];W[kr];B[qs] +;W[no];B[oo];W[nn];B[on];W[nl];B[ol];W[gn];B[fn];W[in];B[nj];W[mk];B[jg] +;W[kg];B[mi];W[jh];B[ag];W[bk];B[ah];W[aj];B[fh];W[fj];B[gd];W[ra];B[dp] +;W[cp];B[go];W[gm];B[fm];W[sd];B[se];W[ho];B[hm];W[hn];B[ep];W[eq];B[cd] +;W[ei];B[dn];W[gp];B[pi];W[pf];B[dm];W[cm];B[je];W[jd];B[if];W[ie];B[ko] +;W[jo];B[je];W[kf];B[ni];W[dh];B[ge];W[ie];B[rg];W[je]N[Figure 4]) + +(;B[dk]FG[257:Dia. 6]MN[1];W[ck];B[gk]N[Diagram 6])) + +(;B[nq]VW[ai:ss]FG[257:Dia. 5]MN[1];W[mr];B[nr];W[lr]TR[oq]N[Diagram 5])) + +(;B[mp]VW[ai:ss]FG[257:Dia. 4]MN[1];W[op];B[oo];W[no];B[mo];W[on];B[po] +;W[mn];B[np];W[nn];B[or]N[Diagram 4])) + +(;B[rc]VW[aa:sj]FG[257:Dia. 2]MN[1];W[rb];B[sb];W[la];B[ma];W[na];B[ja] +;W[pa]N[Diagram 2]) + +(;B[rb]VW[aa:sj]FG[257:Dia. 3]MN[1];W[rc];B[sc];W[qb];B[pa];W[sb];B[sa] +;W[sd];B[qa]N[Diagram 3])) + +(;B[qf]VW[aa:sj]FG[257:Dia. 1]MN[1];W[mb];B[kc];W[qe];B[ne];W[kb];B[md] +;W[la];B[nb];W[eb]LB[ob:a][na:b][rc:c][sd:d]N[Diagram 1]))