2

Through two previous answers by @StevenB.Segletes (1 and 2), I'm 99% able to parse the 4 most important fields in SGF files/strings: B and W for Black and White moves, respectively; AB and AW for added or edited Black and White stones respectively. (What I'm trying to do is handle single-branch versions of SGFs, not all of it.)

In those answers, @StevenB.Segletes used this to parse things:

\setsepchar{;||(||)/[||]}

; splits the SGF string around nodes, and then the rest splits the string into key[value] data. However, I unfortunately took too long to realize that AB and AW are typically written inside the same node. So I think, it should be better to directly parse the string as key[value]. I've already tried this (and some other variations), but it didn't work:

\setsepchar{[||]}

In the previous way, this SGF string:

(;GM[1]FF[4]CA[UTF-8]AP[Sabaki:0.52.2]KM[6.5]SZ[19]DT[2024-02-05];B[as];W[bs];B[cs];PL[W]AB[dq]AW[eq])

is basically this:

(
  ;GM[1]FF[4]CA[UTF-8]AP[Sabaki:0.52.2]KM[6.5]SZ[19]DT[2024-02-05]
  ;B[as]
  ;W[bs]
  ;B[cs]
  ;PL[W]AB[dq]AW[eq]
)

and would have been parsed like this, I think (before the parsing of the first key[value]):

  • (
  • GM[1]FF[4]CA[UTF-8]AP[Sabaki:0.52.2]KM[6.5]SZ[19]DT[2024-02-05]
  • B[as]
  • W[bs]
  • B[cs]
  • PL[W]AB[dq]AW[eq]
  • )

But, that wouldn't be able to capture AB or AW in the code below, I don't think. I think that if we directly parse through key[value], then it's possible to do so. The string would be like this:

  • (;GM[1]
  • FF[4]
  • ...
  • ;B[as]
  • ;W[bs]
  • ...
  • ;PL[W]
  • AB[dq]
  • AW[eq]

In truth, we would then have to check for ;B, ;W instead, but it's pretty much it, I think. Or maybe we should strip the of ; outright?


This is the main part of the code:

\long\def\Firstof#1#2\endFirstof{#1}

\newcommand\thecolorofB{black} \newcommand\thecolorofAB{black} \newcommand\thecolorofW{white} \newcommand\thecolorofAW{white}

\ignoreemptyitems \newcommand{\parseSgf}[1]{ % Maybe first strip the SGF string from (, ), and ;?

\setsepchar{;||(||)/[||]} % This is where things should change the most, I think \readlist*\Z{#1}

\foreachitem \i \in \Z[]{ \itemtomacro\Z[\icnt, 1]\Color \itemtomacro\Z[\icnt, 2]\sgfCoords \edef\tmp{{\csname thecolorof\Color\endcsname}{\sgfCoords}}

\ifthenelse{
  \equal{\Color}{B}  \OR % maybe use some sort of `\contains` in TeX to simplify this if?
  \equal{\Color}{W}  \OR
  \equal{\Color}{AB} \OR
  \equal{\Color}{AW}
}{
  \expandafter\drawStoneFromSgfCoords\tmp
}{}

} }


Here's the code I'm using, which is a slight modification to this previous answer (the important part is the \parseSgf macro):

\documentclass{article}

\usepackage{tikz} \usepackage{listofitems} \usepackage{ifthen}

% From this answer by @DavidCarlisle. \newcommand\notwhite{black} \newcommand\notblack{white}

% From this answer by @DavidCarlisle. \ExplSyntaxOn \cs_generate_variant:Nn \int_from_alph:n {e}

\NewExpandableDocumentCommand{\stringToCoordX}{ m }{ \int_from_alph:e { \use_i:nn #1 } } \NewExpandableDocumentCommand{\stringToCoordY}{ m }{ \int_from_alph:e { \use_ii:nn #1 } } \ExplSyntaxOff

\newcommand{\drawStoneFromSgfCoords}[2]{ \pgfmathsetmacro{\x}{\stringToCoordX{#2} - 1} \pgfmathsetmacro{\y}{\stringToCoordY{#2} - 1}

\draw[draw = \UseName{not#1}, fill = #1, line width = 0.1mm] (\x * 10cm / 18, \y * 10cm / 18) circle [radius = 0.2575cm]; }

\long\def\Firstof#1#2\endFirstof{#1}

\newcommand\thecolorofB{black} \newcommand\thecolorofAB{black} \newcommand\thecolorofW{white} \newcommand\thecolorofAW{white}

\ignoreemptyitems \newcommand{\parseSgf}[1]{ \setsepchar{;||(||)/[||]} % This is where things should change, I think \readlist*\Z{#1}

\foreachitem \i \in \Z[]{ \itemtomacro\Z[\icnt, 1]\Color \itemtomacro\Z[\icnt, 2]\sgfCoords \edef\tmp{{\csname thecolorof\Color\endcsname}{\sgfCoords}}

\ifthenelse{
  \equal{\Color}{B}  \OR
  \equal{\Color}{W}  \OR
  \equal{\Color}{AB} \OR
  \equal{\Color}{AW}
}{
  \expandafter\drawStoneFromSgfCoords\tmp
}{}

} }

\def\sgfA{;B[ab];W[cd]} \def\sgfB{(;GM[1]FF[4]CA[UTF-8]AP[Sabaki:0.52.2]KM[6.5]SZ[19]DT[2024-02-05];B[as];W[bs];B[cs])} \def\sgfC{(;GM[1]FF[4]CA[UTF-8]AP[Sabaki:0.52.2]KM[6.5]SZ[19]DT[2024-02-05];B[as];W[bs];B[cs];PL[W]AB[dq]AW[eq])}

\begin{document} \begin{tikzpicture} \pgfmathsetmacro{\step}{10 / 18}

\draw[step=\step] (0, 0) grid (10, 10);

% \drawStoneFromSgfCoords{black}{ab}
% \drawStoneFromSgfCoords{white}{cd}

\parseSgf{\sgfC}

\end{tikzpicture} \end{document}

psygo
  • 438

1 Answers1

2

First, I must improve my ability to parse the SGF inputs, relative to my prior answer(s). Here is a short MWE just showing that parsing. I use listofitems to perform a 3-depth (nested) parsing: the highest level parses on ;, (, and ), the 2nd level on ], and the 3rd level on [.

Blank items in the list are set to be ignored, so that (; effectively contributes nothing to the parsed list (rather than contributing a blank entry between ( and ;).

First in the output, each parsed \KeyName[\KeyValue] of the nested \foreachitem loop is shown with space separators. If a color key is detected, a \rightarrow is inserted before the entry. We see that all keys of the type B, W, AB, and AW are successfully detected as color keys and all other keys are excluded, which is the goal of this exercise.

To see details on the parsing, the final three lines output by the MWE are demonstrably provided. The SGF definition, given in the MWE input as

\def\sgfC{(
  ;GM[1]FF[4]CA[UTF-8]AP[Sabaki:0.52.2]KM[6.5]SZ[19]DT[2024-02-05]
  ;B[as]
  ;W[bs]
  ;B[cs]
  ;PL[W]AB[dq]AW[eq]
)}

is parsed into a list named \Z. We see the 5th element of \Z, denoted Z[5], is PL[W]AB[dq]AW[eq], corresponding to the 5th block of non-empty text that lies between the characters ;, (, or ).

The 2nd level of parsing, with ] as a separator, is applied to each element <i> of \Z[<i>]. The element \Z[5,2], a sublist of \Z[5], is shown as AB[dq. We have now been able to isolate each key linked with its associated value. Finally, the 3rd level of parsing is applied to each item of the secondary list, such that \Z[5,2,1] is given as AB and \Z[5,2,2] is given by dq. We have thus isolated each key and can independently access the value associated with it.

As with prior answers in this sequence of questions, we have introduced macros such as \newcommand\thecolorofB{black} so that SGF keys like B can be converted into an actual color black. But we now also introduce macros like \newcommand\thekeytypeofAB{C} to indicate that the key AB is a key associated with the property of color.

\documentclass{article}
\usepackage{listofitems}
\long\def\Firstof#1#2\endFirstof{#1}
\newcommand\thecolorofB{black}
\newcommand\thecolorofAB{black}
\newcommand\thecolorofW{white}
\newcommand\thecolorofAW{white}
\long\def\Keytypeof#1{\csname thekeytypeof#1\endcsname}
\newcommand\thekeytypeofB{C}% C DENOTING A "COLOR" KEY
\newcommand\thekeytypeofAB{C}
\newcommand\thekeytypeofW{C}
\newcommand\thekeytypeofAW{C}
\ignoreemptyitems
\newcommand{\parseSgf}[1]{%
  \setsepchar{;||(||)/]/[}%
  \readlist*\Z{#1}%
  \foreachitem \i \in \Z[]{%
    \foreachitem \z \in \Z[\icnt]{%
    \itemtomacro\Z[\icnt, \zcnt, 1]\KeyName
    \itemtomacro\Z[\icnt, \zcnt, 2]\KeyValue
    \if\Keytypeof\KeyName C
      \edef\tmp{{\csname thecolorof\KeyName\endcsname}{\KeyValue}}%
      \expandafter\drawStoneFromSgfCoords\tmp
    \fi
    \KeyName[\KeyValue]
  }}%
}
\newcommand{\drawStoneFromSgfCoords}[2]{\unskip$\rightarrow$}
\def\sgfC{(
  ;GM[1]FF[4]CA[UTF-8]AP[Sabaki:0.52.2]KM[6.5]SZ[19]DT[2024-02-05]
  ;B[as]
  ;W[bs]
  ;B[cs]
  ;PL[W]AB[dq]AW[eq]
)}
\begin{document}
\parseSgf{\sgfC}

\bigskip\textbackslash Z[5] is \Z[5]

\bigskip\textbackslash Z[5,2] is \Z[5,2]

\bigskip\textbackslash Z[5,2,1] and \textbackslash Z[5,2,2] are \Z[5,2,1] and \Z[5,2,2] \end{document}

enter image description here

Now, we are ready to apply the parsing approach shown above to the actual \drawStoneFromSgfCoords macro provided by the OP. When that is done, the stones from not only the B and W keys are plotted, but also those from AB and AW commands---in essence, any color key provokes a plotting action.

\documentclass{article}

\usepackage{tikz}

% From this answer by @DavidCarlisle. \newcommand\notwhite{black} \newcommand\notblack{white}

% From this answer by @DavidCarlisle. \ExplSyntaxOn \cs_generate_variant:Nn \int_from_alph:n {e}

\NewExpandableDocumentCommand{\stringToCoordX}{ m }{ \int_from_alph:e { \use_i:nn #1 } } \NewExpandableDocumentCommand{\stringToCoordY}{ m }{ \int_from_alph:e { \use_ii:nn #1 } } \ExplSyntaxOff

\newcommand{\drawStoneFromSgfCoords}[2]{% \pgfmathsetmacro{\x}{\stringToCoordX{#2} - 1} \pgfmathsetmacro{\y}{\stringToCoordY{#2} - 1} % \draw[draw = \UseName{not#1}, fill = #1, line width = 0.1mm] (\x * 10cm / 18, \y * 10cm / 18) circle [radius = 0.2575cm]; }

\usepackage{listofitems} % From this answer by @StevenB.Segletes. \long\def\Firstof#1#2\endFirstof{#1} \newcommand\thecolorofB{black} \newcommand\thecolorofAB{black} \newcommand\thecolorofW{white} \newcommand\thecolorofAW{white} \long\def\Keytypeof#1{\csname thekeytypeof#1\endcsname} \newcommand\thekeytypeofB{C}% C DENOTING A "COLOR" KEY \newcommand\thekeytypeofAB{C} \newcommand\thekeytypeofW{C} \newcommand\thekeytypeofAW{C} \ignoreemptyitems \newcommand{\parseSgf}[1]{% \setsepchar{;||(||)/]/[}% \readlist*\Z{#1}% \foreachitem \i \in \Z[]{% \foreachitem \z \in \Z[\icnt]{% \itemtomacro\Z[\icnt, \zcnt, 1]\KeyName \itemtomacro\Z[\icnt, \zcnt, 2]\KeyValue \if\Keytypeof\KeyName C% \edef\tmp{{\csname thecolorof\KeyName\endcsname}{\KeyValue}}% \expandafter\drawStoneFromSgfCoords\tmp \fi }}% }

\def\sgfA{;B[ab];W[cd]} \def\sgfB{( ;GM[1]FF[4]CA[UTF-8]AP[Sabaki:0.52.2]KM[6.5]SZ[19]DT[2024-02-05] ;B[as] ;W[bs] ;B[cs] )} \def\sgfC{( ;GM[1]FF[4]CA[UTF-8]AP[Sabaki:0.52.2]KM[6.5]SZ[19]DT[2024-02-05] ;B[as] ;W[bs] ;B[cs] ;PL[W]AB[dq]AW[eq] )}

\begin{document} \begin{tikzpicture} \pgfmathsetmacro{\step}{10 / 18} \draw[step=\step] (0, 0) grid (10, 10); \parseSgf{\sgfC} \end{tikzpicture} \end{document}

enter image description here

BONUS

In response to a comment on how to handle split keys (split by :), here is how...add another layer of parsing, based on :. I have defined the logic to detect defined split keys (that must include : separator) as well as optional split keys (that may or may not include : separator).

\documentclass{article}
\usepackage{listofitems}
\long\def\Firstof#1#2\endFirstof{#1}
\newcommand\thecolorofB{black}
\newcommand\thecolorofAB{black}
\newcommand\thecolorofW{white}
\newcommand\thecolorofAW{white}
\long\def\Keytypeof#1{\csname thekeytypeof#1\endcsname}
\newcommand\thekeytypeofB{C}% C DENOTING A "COLOR" KEY
\newcommand\thekeytypeofAB{C}
\newcommand\thekeytypeofW{C}
\newcommand\thekeytypeofAW{C}
\newcommand\thekeytypeofAP{S}% DENOTING SPLIT KEY VALUE
\ignoreemptyitems
\newcommand{\parseSgf}[1]{%
  \setsepchar{;||(||)/]/[/:}%
  \readlist*\Z{#1}%
  \foreachitem \i \in \Z[]{%
    \foreachitem \z \in \Z[\icnt]{%
    \itemtomacro\Z[\icnt, \zcnt, 1]\KeyName
    \itemtomacro\Z[\icnt, \zcnt, 2]\KeyValue
    \if\Keytypeof\KeyName C
      \edef\tmp{{\csname thecolorof\KeyName\endcsname}{\KeyValue}}%
      \expandafter\drawStoneFromSgfCoords\tmp
    \fi
    \if \Keytypeof\KeyName S
      DEFINED SPLIT KEY ``\Z[\icnt, \zcnt, 2, 1]'' and 
                ``\Z[\icnt, \zcnt, 2, 2]''
    \else
    \ifnum\listlen\Z[\icnt, \zcnt, 2]=2
      OPTIONAL SPLIT KEY ``\Z[\icnt, \zcnt, 2, 1]'' and 
                ``\Z[\icnt, \zcnt, 2, 2]''
    \fi\fi
    \KeyName[\KeyValue]
  }}%
}
\newcommand{\drawStoneFromSgfCoords}[2]{\unskip$\rightarrow$}
\def\sgfC{(
  ;GM[1]FF[4]CA[UTF-8]AP[Sabaki:0.52.2]KM[6.5]SZ[19]DT[2024-02-05]
  ;B[as]
  ;W[bs]
  ;B[cs]
  ;PL[W]AB[dq]AW[eq]LB[bs:labelname]
)}
\begin{document}
\parseSgf{\sgfC}
\end{document}

enter image description here

  • 1
    Amazing stuff, @StevenB.Segletes. And, thanks to listofitems, this is way less complicated than I imagined. – psygo Feb 13 '24 at 12:10
  • 1
    @PhilippeFanaro With this approach, you can define \thekeytypeof<keyname> for other keys of different <keytype>s. Then, in \parseSgf, you can provide additional \if\Keytypeof\KeyName <keytype> blocks to perform different actions for different key-types. – Steven B. Segletes Feb 13 '24 at 13:39
  • 1
    Yes, that's true, I've even just now adapted your code to be able to draw moves and stones! Here's the code in my repo so far. – psygo Feb 13 '24 at 13:55
  • I know this is stretching your good will a bit too far, but if you ever feel like it, this whole saga of parsing SGFs in TeX started with this question. It's basically everything you've already done so far plus some extra labels (simple if's with your code), which I think I could handle on my own actually, based on this answer. Howeve, the only parsing I don't know is probably the labels, which have this format: LB[<coords>:<label_name>]. – psygo Feb 13 '24 at 13:59
  • If you feel like you would need a bounty to answer that question, just let me know. And I don't know if that question needs an update to its description, it probably does. Just let me know... – psygo Feb 13 '24 at 14:00
  • 1
    If I get a chance, I will have a look (though today is not likely). The approach would be to add another level of parsing based on :. None of the existing code would have to be changed except for \setsepchar{;||(||)/]/[/:}. Then, if the key LB is detected, you should be able to access \Z[5,2,2,1] and \Z[5,2,2,2]. – Steven B. Segletes Feb 13 '24 at 14:04
  • I'll give it a try later on! Thanks! – psygo Feb 13 '24 at 14:06
  • 1
    @PhilippeFanaro I have improved my BONUS edit to make split keys either defined or optional. – Steven B. Segletes Feb 13 '24 at 14:22