我想要完成的是将 SGF 文件(文本文件)解析为 TikZ 图表,如下所示(理想情况下,我会指向 SGF 文件本身,而不是内联字符串):
\parsesgf{(;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])}
在底层,变成这样(我还添加了一些样板,这样我们就有一个最小的、完整的示例):
\documentclass{article}
\usepackage{tikz}
\newlength{\step}
\begin{document}
\begin{tikzpicture}
\setlength{\step}{\dimexpr 10cm / 18 \relax}
\draw[step=\step] (0, 0) grid (10, 10);
\draw[draw = white, fill = black, line width = 0.1mm]
(0 * 10cm / 18, 0 * 10cm / 18)
circle [radius = 0.2575cm]
node[color = white] {1};
\draw[draw = black, fill = white, line width = 0.1mm]
(1 * 10cm / 18, 0 * 10cm / 18)
circle [radius = 0.2575cm]
node[color = black] {2};
\draw[draw = white, fill = black, line width = 0.1mm]
(2 * 10cm / 18, 0 * 10cm / 18)
circle [radius = 0.2575cm]
node[color = white] {3};
\end{tikzpicture}
\end{document}
我真的不知道最好的方法是什么。有没有这种东西的好包?我想正则表达式实际上可以做到这一点,有没有更好的方法?我会在尝试更多东西时更新这个问题。
在这个答案有人帮我提供了一种 PCRE 完整解析 SGF 的方法(在 PHP 中),但我不知道在 TeX 中可以做到什么程度。无论如何,也许该配方的键值部分很有用:
/(?<key>[A-Z]+)\[(?<value>(?:\\\]|[^\]])*)\]/gy
我想要构建一个包,其中包含解析 SGF 的功能这里希望您可以在那里找到一些有关构建 Go 图的有用的宏。
SGF 是文本中树结构的表示,但是为了简单起见,首先,我想我只想解析只有一个分支的文件中的主分支。
(目前有一些 JS 中的 SGF 解析器可用——包括矿和沙巴基— 也许它们是有用的参考,或者也许它们可以通过某种方式被 TeX 调用?)
在围棋游戏中,由于棋盘比国际象棋大得多,我们通常不使用坐标,而是使用可视化编辑器(或纸质 kifus)——其中最好的两个编辑器是哥班和佐巴基— 为了保存游戏。最终,智能游戏格式 (SGF) 是标准(实际上它也支持其他游戏),并且它最终还是以文本格式保存文件。
以下是 SGF 文件的一个示例:
(;GM[1]FF[4]CA[UTF-8]AP[CGoban:3]ST[2]
RU[Japanese]SZ[19]KM[6.50]
DT[2023-12-25]
;B[pd]
;W[dd]
;B[dp]
;W[pp]
;AW[ji]AB[jj]PL[B]
;B[jq]CR[pp]LB[dd:A][jd:C][pd:B]TR[jj]SQ[ji]MA[dp])
()
表示分支,数据在[]
- 之前的标签
[]
表示数据类型 ;
是节点分隔符B
和W
分别是黑棋和白棋AB
并AW
添加或编辑石头LB
是坐标上方的标签,:A
是A
标签CR
是一个圆形标签TR
是一个三角形标签SQ
是一个方形标签MA
是一个十字 (X
) 标签
Collection = { GameTree }
GameTree = "(" RootNode NodeSequence { Tail } ")"
Tail = "(" NodeSequence { Tail } ")"
NodeSequence = { Node }
RootNode = Node
Node = ";" { Property }
Property = PropIdent PropValue { PropValue }
PropIdent = UcLetter { UcLetter }
PropValue = "[" Value "]"
UcLetter = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" |
"J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" |
"S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z"
根据@StevenB.Segletes 的回答进行编辑
关注@StevenB.Segletes'listofitems
包,以及非常有用的答案,尤其是这个,我已经能够创建以下 MWE:
\documentclass{article}
\usepackage{tikz}
\usetikzlibrary{shapes.geometric}
\usepackage{listofitems}
%-----------------------------------------------------------
% Drawing Stones
% From [this answer by @DavidCarlisle](https://tex.stackexchange.com/a/708876/64441).
\newcommand\notwhite{black}
\newcommand\notblack{white}
% From [this answer by @DavidCarlisle](https://tex.stackexchange.com/a/708893/64441).
\ExplSyntaxOn
\cs_generate_variant:Nn \int_from_alph:n {e}
\NewExpandableDocumentCommand{\stringToCoordX}{ m }{
\int_from_alph:e { \use_i:nn #1 }
}
\NewExpandableDocumentCommand{\stringToCoordY}{ m }{
\boardSize + 1 - ~\int_from_alph:e { \use_ii:nn #1 }
}
\ExplSyntaxOff
\newcommand{\setCoords}[1]{
\pgfmathsetmacro{\x}{\stringToCoordX{#1} - 1}
\pgfmathsetmacro{\y}{\stringToCoordY{#1} - 1}
}
\newcommand{\drawStoneFromSgfCoords}[2]{%
\setCoords{#2}
\draw[draw = \UseName{not#1}, fill = #1, line width = 0.1mm]
(\x * \step, \y * \step)
circle [radius = 0.2575cm];
}
\newcommand{\drawMoveFromSgfCoords}[2]{
\drawStoneFromSgfCoords{#1}{#2}
\textLabel{#1}{#2}{\themoveCounter}
\stepMoveCounter
}
\newcounter{moveCounter}
\setcounter{moveCounter}{1}
\newcommand{\stepMoveCounter}{
\stepcounter{moveCounter}
}
%-----------------------------------------------------------
% Labels
\newcommand{\textLabel}[3]{
\setCoords{#2}
\draw (\x * \step, \y * \step)
node[color = \UseName{not#1}] {#3};
}
\newcommand{\crossLabel}[2]{
\setCoords{#2}
\draw (\x * \step, \y * \step)
node[color = \UseName{not#1}] {X};
}
\newcommand{\triangleLabel}[2]{
\setCoords{#2}
\draw (\x * \step, \y * \step)
node[
isosceles triangle,
draw = #1,
line width = 0.5mm,
fill = \UseName{not#1},
minimum height = \step * 10,
minimum width = \step * 10,
rotate = 90,
isosceles triangle apex angle = 60,
inner sep = 0pt,
] {};
}
\newcommand{\squareLabel}[2]{
\setCoords{#2}
\draw (\x * \step, \y * \step)
node[
draw = #1,
line width = 0.5mm,
fill = \UseName{not#1},
minimum size = \step * 10,
inner sep = 0pt,
] {};
}
\newcommand{\circleLabel}[2]{
\setCoords{#2}
\draw[
draw = #1,
line width = 0.5mm,
fill = \UseName{not#1},
inner sep = 0pt,
] (\x * \step, \y * \step)
circle[radius = \step / 4];
}
%-----------------------------------------------------------
% SGF Parser
% From [this answer by @StevenB.Segletes](https://tex.stackexchange.com/a/709014/64441).
\long\def\Firstof#1#2\endFirstof{#1}
\newcommand\thecolorofB{black}
\newcommand\thecolorofAB{black}
\newcommand\thecolorofW{white}
\newcommand\thecolorofAW{white}
\newcommand\thecolorofMA{white}
\newcommand\thecolorofCR{white}
\newcommand\thecolorofTR{white}
\newcommand\thecolorofSQ{white}
\newcommand\thecolorofLB{white}
\long\def\Keytypeof#1{\csname thekeytypeof#1\endcsname}
\newcommand\thekeytypeofB{M} % black move
\newcommand\thekeytypeofAB{A} % added (edited) black stone
\newcommand\thekeytypeofW{M} % white move
\newcommand\thekeytypeofAW{A} % added (edited) white stone
\newcommand\thekeytypeofMA{K} % cross (mark) label
\newcommand\thekeytypeofCR{C} % circle label
\newcommand\thekeytypeofTR{T} % triangle label
\newcommand\thekeytypeofSQ{S} % square label
\newcommand\thekeytypeofLB{L} % text label
\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
\edef\tmp{{\csname thecolorof\KeyName\endcsname}{\KeyValue}}
\if\Keytypeof\KeyName M
\expandafter\drawMoveFromSgfCoords\tmp
\fi
\if\Keytypeof\KeyName A
\expandafter\drawStoneFromSgfCoords\tmp
\fi
\if\Keytypeof\KeyName K
\expandafter\crossLabel\tmp
\fi
\if\Keytypeof\KeyName C
\expandafter\circleLabel\tmp
\fi
\if\Keytypeof\KeyName T
\expandafter\triangleLabel\tmp
\fi
\if\Keytypeof\KeyName S
\expandafter\squareLabel\tmp
\fi
\if\Keytypeof\KeyName L
\expandafter\textLabel\tmp % this macro has 1 extra argument compared to the other ones (the text itself)
\fi
}
}%
}
%-----------------------------------------------------------
% SGFs
\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]
)}
\def\sgfD{( % Basically `sgfE` with no labels (`LB`)
;GM[1]FF[4]CA[UTF-8]AP[CGoban:3]ST[2]RU[Japanese]SZ[19]KM[6.50]DT[2023-12-25]
;B[pd]
;W[dd]
;B[dp]
;W[pp]
;AW[ji]AB[jj]PL[B]
;B[jq]CR[pp]TR[jj]SQ[ji]MA[dp]
)}
\def\sgfE{(
;GM[1]FF[4]CA[UTF-8]AP[CGoban:3]ST[2]RU[Japanese]SZ[19]KM[6.50]DT[2023-12-25]
;B[pd]
;W[dd]
;B[dp]
;W[pp]
;AW[ji]AB[jj]PL[B]
;B[jq]CR[pp]LB[dd:A][jd:C][pd:B]TR[jj]SQ[ji]MA[dp]
% The `LB` data is apparently the only weird one.
% Besides having the `LB[<coords>:<label>]` format,
% If many of them are on the same node, there will only be one `LB` for all of them.
)}
%-----------------------------------------------------------
% Setup
\pgfmathsetmacro{\boardDimension}{10}
\pgfmathsetmacro{\boardSize}{19}
\pgfmathsetmacro{\step}{\boardDimension / (\boardSize - 1)}
%-----------------------------------------------------------
\begin{document}
\begin{tikzpicture}
\draw[step=\step] (0, 0) grid (10, 10);
\parseSgf{\sgfD}
\textLabel{white}{ab}{A}
\triangleLabel{black}{ac}
\squareLabel{black}{ad}
\circleLabel{black}{ae}
\crossLabel{white}{af}
\end{tikzpicture}
\end{document}
本质上,缺少的是:
- 在必需的键中,标签键 (
LB
) 是唯一格式奇怪的键。在上一个答案中,@StevenB.Segletes 确实设法涵盖了格式LB[<coords>:<label>]
,但是,当多个标签位于同一节点上时,情况会变得更加奇怪,因为它们会组合在一起,而没有键,例如LB[dd:A][jd:C][pd:B]
(这实际上显然发生在每个标签键上,请参见下面的示例)。 - 由于标签通常是在放置棋子后才标记的,所以我不知道在解析时如何找到合适的颜色。解析时,由于解析器不知道标签下面是黑色还是白色棋子,所以它不知道要用哪种颜色绘制。也许应该有一个坐标与颜色的列表,然后解析器应该在绘制标签之前进行检查,我不知道。
实际上,显示的分组LB
发生在每个标签和编辑或添加的石头上:
(
;GM[1]FF[4]CA[UTF-8]AP[CGoban:3]ST[2]RU[Japanese]SZ[19]KM[6.50]DT[2023-12-25]
;B[pd]
;W[dd]
;B[dp]
;W[pp]
;AW[ji][jn][kn][ln]AB[jj][jm][km]TR[jp][kp][lp]PL[B]
;B[jq]CR[pp]LB[dd:A][jd:C][pd:B]TR[jj]SQ[ji]MA[dp]
)
答案1
我认为这解决了 OP 所要求的所有问题。
与 OP 之前的问题相比,这个答案的挑战在于,OP 对 SGF 文件的描述中引入了一种新的有效语法。之前,暗示键总是<Keyname>[<Value>]
采取了或的形式<Keyname>[<Value>:<Label>]
。事实上,新引入的语法允许<Keyname>[<Value1>][<Value2>][<Value3>]...
,这导致我之前的解析逻辑无效。这种允许的语法变化可能出现在任何有效的密钥名称。
解决方案是不再忽略列表解析中的空项,而是允许它们,以便<Key1>[<Value1>][<Value2>]
可以有效地解析和理解为的等价物<Key1>[<Value1>]<implied-Key1>[<Value2>]
。当然,允许空白列表项会弄乱必须解决的解析的其他方面。
最后,我要求进行 5 级解析:
任何被
(
或)
我称为分支的东西在每个分支中,任何被
;
我称为“组”的东西在每个组中,任何由
]
我称为键分隔的内容(尽管它将包括键名称及其关联值)在每个键中,
[
分隔符将键名与键值分隔开在每个键值中,
:
可以使用来分隔实际值与关联标签。
通过这种方法,我可以有效地解析OP的测试SGF:
(
;GM[1]FF[4]CA[UTF-8]AP[CGoban:3]ST[2]RU[Japanese]SZ[19]KM[6.50]DT[2023-12-25]
;B[pd]
;W[dd]
;B[dp]
;W[pp]
;AW[ji][jn][kn][ln]AB[jj][jm][km]TR[jp][kp][lp]PL[B]
;B[jq]CR[pp]LB[dd:A][jd:C][pd:B]TR[jj]SQ[ji]MA[dp]
)
以下是 MWE:
\documentclass{article}
\usepackage{tikz}
\usetikzlibrary{shapes.geometric}
\usepackage{listofitems}
%-----------------------------------------------------------
% Drawing Stones
% From [this answer by @DavidCarlisle](https://tex.stackexchange.com/a/708876/64441).
\newcommand\notwhite{black}
\newcommand\notblack{white}
% From [this answer by @DavidCarlisle](https://tex.stackexchange.com/a/708893/64441).
\ExplSyntaxOn
\cs_generate_variant:Nn \int_from_alph:n {e}
\NewExpandableDocumentCommand{\stringToCoordX}{ m }{
\int_from_alph:e { \use_i:nn #1 }
}
\NewExpandableDocumentCommand{\stringToCoordY}{ m }{
\boardSize + 1 - ~\int_from_alph:e { \use_ii:nn #1 }
}
\ExplSyntaxOff
\newcommand{\setCoords}[1]{
\pgfmathsetmacro{\x}{\stringToCoordX{#1} - 1}
\pgfmathsetmacro{\y}{\stringToCoordY{#1} - 1}
}
\newcommand{\drawStoneFromSgfCoords}[2]{%
\setCoords{#2}
\draw[draw = \UseName{not#1}, fill = #1, line width = 0.1mm]
(\x * \step, \y * \step)
circle [radius = 0.2575cm];
}
\newcommand{\drawMoveFromSgfCoords}[2]{
\expandafter\xdef\csname thecolorat#2\endcsname{#1}% SAVE COLOR AT #2
\drawStoneFromSgfCoords{#1}{#2}
\textLabel{#1}{#2}{\themoveCounter}
\stepMoveCounter
}
\newcounter{moveCounter}
\setcounter{moveCounter}{1}
\newcommand{\stepMoveCounter}{
\stepcounter{moveCounter}
}
%-----------------------------------------------------------
% Labels
\newcommand\StoneColor[1]{% COMPLEMENTARY COLOR OF REFERENCED STONE
\ifcsname thecolorat#1\endcsname
\csname thecolorat#1\endcsname
\else
white% COMPLEMENT OF DEFAULT LABEL COLOR WHEN NOT ATOP EXISTING STONE
\fi
}
\newcommand{\textLabel}[3]{%
\setCoords{#2}%
\draw (\x * \step, \y * \step)
node[
color = -\StoneColor{#2},
fill = \StoneColor{#2},
inner sep=1pt
] {#3};
}
\newcommand{\crossLabel}[2]{
\setCoords{#2}
\draw (\x * \step, \y * \step)
node[
color = -\StoneColor{#2},
fill = \StoneColor{#2},
inner sep=1pt
] {$\times$};
}
\newcommand{\triangleLabel}[2]{
\setCoords{#2}
\draw (\x * \step, \y * \step)
node[
isosceles triangle,
draw = #1,
line width = 0.5mm,
color = -\StoneColor{#2},
fill = -\StoneColor{#2},
minimum height = \step * 10,
minimum width = \step * 10,
rotate = 90,
isosceles triangle apex angle = 60,
inner sep = 0pt,
] {};
}
\newcommand{\squareLabel}[2]{
\setCoords{#2}
\draw (\x * \step, \y * \step)
node[
draw = #1,
line width = 0.5mm,
color = -\StoneColor{#2},
fill = -\StoneColor{#2},
minimum size = \step * 10,
inner sep = 0pt,
] {};
}
\newcommand{\circleLabel}[2]{
\setCoords{#2}
\draw[
draw = #1,
line width = 0.5mm,
color = -\StoneColor{#2},
fill = -\StoneColor{#2},
inner sep = 0pt,
] (\x * \step, \y * \step)
circle[radius = \step / 4];
}
%-----------------------------------------------------------
% SGF Parser
% From [this answer by @StevenB.Segletes](https://tex.stackexchange.com/a/709014/64441).
\long\def\Firstof#1#2\endFirstof{#1}
\newcommand\thecolorofB{black}
\newcommand\thecolorofAB{black}
\newcommand\thecolorofW{white}
\newcommand\thecolorofAW{white}
\newcommand\thecolorofMA{white}
\newcommand\thecolorofCR{white}
\newcommand\thecolorofTR{white}
\newcommand\thecolorofSQ{white}
\newcommand\thecolorofLB{white}
\long\def\Keytypeof#1{\csname thekeytypeof#1\endcsname}
\newcommand\thekeytypeofB{M} % black move
\newcommand\thekeytypeofAB{A} % added (edited) black stone
\newcommand\thekeytypeofW{M} % white move
\newcommand\thekeytypeofAW{A} % added (edited) white stone
\newcommand\thekeytypeofMA{K} % cross (mark) label
\newcommand\thekeytypeofCR{C} % circle label
\newcommand\thekeytypeofTR{T} % triangle label
\newcommand\thekeytypeofSQ{S} % square label
\newcommand\thekeytypeofLB{L} % text label
\newcommand{\parseSgf}[1]{%
\setsepchar{(||)/;/]/[/:}%
\readlist*\Z{#1}%
\foreachitem \Branch \in \Z[]{%
\foreachitem \Group \in \Z[\Branchcnt]{%
\foreachitem \Key \in \Z[\Branchcnt, \Groupcnt]{%
\if\relax\Key\relax% IF BLANK KEY & VALUE, SKIP
\else
\itemtomacro\Z[\Branchcnt, \Groupcnt, \Keycnt, 1]\KeyName
\if\relax\KeyName\relax% IF VALUE, BUT NO KEYNAME, USE RECENT KEYNAME
\let\KeyName\MostRecentKeyname
\else
\xdef\MostRecentKeyname{\KeyName}%
\fi
\itemtomacro\Z[\Branchcnt, \Groupcnt, \Keycnt, 2, 1]\KeyValue
\edef\tmp{{\csname thecolorof\KeyName\endcsname}{\KeyValue}}%
\if\Keytypeof\KeyName M
\expandafter\drawMoveFromSgfCoords\tmp
\fi
\if\Keytypeof\KeyName A
\expandafter\drawStoneFromSgfCoords\tmp
\fi
\if\Keytypeof\KeyName K
\expandafter\crossLabel\tmp
\fi
\if\Keytypeof\KeyName C
\expandafter\circleLabel\tmp
\fi
\if\Keytypeof\KeyName T
\expandafter\triangleLabel\tmp
\fi
\if\Keytypeof\KeyName S
\expandafter\squareLabel\tmp
\fi
\if\Keytypeof\KeyName L
\expandafter\textLabel\tmp{\Z[\Branchcnt, \Groupcnt, \Keycnt, 2, 2]}
\fi
\fi
}
}%
}%
}
%-----------------------------------------------------------
% SGFs
\def\sgfE{(
;GM[1]FF[4]CA[UTF-8]AP[CGoban:3]ST[2]RU[Japanese]SZ[19]KM[6.50]DT[2023-12-25]
;B[pd]
;W[dd]
;B[dp]
;W[pp]
;AW[ji][jn][kn][ln]AB[jj][jm][km]TR[jp][kp][lp]PL[B]
;B[jq]CR[pp]LB[dd:A][jd:C][pd:B]TR[jj]SQ[ji]MA[dp]
)}
%-----------------------------------------------------------
% Setup
\pgfmathsetmacro{\boardDimension}{10}
\pgfmathsetmacro{\boardSize}{19}
\pgfmathsetmacro{\step}{\boardDimension / (\boardSize - 1)}
%-----------------------------------------------------------
\begin{document}
\begin{tikzpicture}
\draw[step=\step] (0, 0) grid (10, 10);
\parseSgf{\sgfE}
\textLabel{white}{ab}{A}
\triangleLabel{black}{ac}
\squareLabel{black}{ad}
\circleLabel{black}{ae}
\crossLabel{white}{af}
\end{tikzpicture}
\end{document}
需要克服几个问题:
要将 放置
\textLabel
在现有棋子上方,我必须记住底层棋子的颜色,这样我才能将标签设为互补色。我将棋子的颜色(在创建时)保存在 中\thecolorat<coordinate>
。然后,当需要文本标签时,我使用\StoneColor{<coordinate>}
来计算互补色……如果之前没有在该位置创建过棋子,我会使用默认颜色,假设底层颜色为white
(这样互补标签就是black
)。当覆盖现有(默认)标签时,我必须添加
fill
石头颜色,并inner sep
用 来消除先前的标签。对于几何标签(正方形、圆形、三角形),需要同时
color
指定fill
补色,以实现全线统一。如前所述,需要 5 级解析,并通过获得
\setsepchar{(||)/;/]/[/:}
。如果代表“键”的项目完全为空(没有键名或值),例如
(;...
解析时的情况,则解析将跳到已解析的“键”列表中的下一个项目。在迭代所有关键操作时,如果存在键名,则通过 保存它的全局副本
\xdef\MostRecentKeyname{\KeyName}
。如果键名为空,但关联的键值存在,则使用最新的非空键名,通过
\let\KeyName\MostRecentKeyname
。这涵盖以下情况默示键名,例如AB[jj][jm][km]
,其中在三个指定坐标处各添加一颗黑棋。对于文本标签类型的键,例如
LB[dd:A]
,需要第五级解析才能将坐标dd
与标签分离A
。对于交叉标签,我使用
$\times$
而不是X
。