読み書きプログラミング

日常のプログラミングで気づいたことを綴っています

囲碁AIアプリの作り方3

今日はちょっと変わったところで、SGFファイルを扱う方法です。

SGFというのは、様々なプラットフォーム上で販売されているSmartGoというアプリケーションのファイルフォーマットです。Smart Game Formatの略で、囲碁以外のボードゲームにも対応可能なフォーマットです。
囲碁のファイルフォーマットはアプリケーション毎に他にも色々ありますが、仕様がドキュメント化されているこのSGFが事実上標準フォーマットという感じです。

テキストフォーマットなので中身を見たことのある人も多いかと思います。こんな感じ。

(;FF[4]GM[1]CA[UTF-8]AP[囲碁の師匠:1.1.2]SZ[19]HA[2]AB[pd][dp];W[dd])

見ただけでなんとなく意味もわかる、わかりやすいフォーマットですね。

こういうフォーマットを扱うには、扱うプログラムを直接書くのではなく、パーサジェネレータというものを使うと楽ができます。
パーサジェネレータというのは、フォーマットの規則を与えると、その規則に則ってフォーマットに従った文字列からデータ構造を抽出するコード(パーサ)を生成してくれるものです。

パーサジェネレータを使って作った自作のパーサをいくつかご紹介します。

  • jssgf: JavaScript用SGFパーサ。Jisonというパーサジェネレータを使いました。
  • rust-sgf: Rust用SGFパーサ。rust-pegというパーサジェネレータを使いました。
  • swift-SGF: Swift用SGFパーサ。citronというパーサジェネレータを使いました。


なんでいくつも作っているのかというと、それだけ作るのが簡単だからです^^

jssgfを作った時のSGFフォーマットを記述を見てみましょう。

/lex

%% /* language grammar */

output
	: collection EOF
        { return $1; }
	;

collection
	: gametree
		{ $$ = [$1]; }
	| collection gametree
		{ $1.push($2); $$ = $1; }
    ;

gametree
    : '(' sequence ')'
		{ $$ = $2; }
    | '(' sequence gametrees ')'
        { $$ = addGameTrees($2, $3); }
    ;

gametrees
	: gametree
		{ $$ = [$1]; }
	| gametrees gametree
		{ $1.push($2); $$ = $1; }
	;

sequence
	: node
		{ $$ = $1; }
	| node sequence
		{ $1._children.push($2); $$ = $1; }
	;

node
	: ';'
		{ $$ = {_children: []}; }
	| node property
		{
            if (typeof $1[$2[0]] !== 'undefined') {
                if (strict) {
                    throw new Error('double properties');
                }
            } else {
                $1[$2[0]] = $2[1];
                $$ = $1;
            }
        }
	;

property
    : PROPIDENT propvalues
        { $$ = [$1, $2]; }
    ;

propvalues
    : propvalue
		{ $$ = $1; }
	| propvalues propvalue
		{ var a; if ($1 instanceof Array) a = $1; else a = [$1]; $$ = a.concat($2); }
	;

propvalue
    :  /* empty */
        { $$ = ''; }
	| CVALUETYPE
		{ $$ = decodeValue($1); }
	;

%%

こんな風に、コレクションはどういうもの?ゲームツリーはどういうもの?シークエンスはどういうもの?ノードはどういうもの?という規則を書くと、JavaScriptで書かれたパーサを生成してくれます。

実はこの規則、SGFの仕様書に書かれたものをほぼ書き写したものです。以下はSGFの仕様書より。

Collection = GameTree { GameTree }
GameTree   = "(" Sequence { GameTree } ")"
Sequence   = Node { Node }
Node       = ";" { Property }
Property   = PropIdent PropValue { PropValue }
PropIdent  = UcLetter { UcLetter }
PropValue  = "[" CValueType "]"
CValueType = (ValueType | Compose)
ValueType  = (None | Number | Real | Double | Color | SimpleText |
		Text | Point  | Move | Stone)

似てるでしょ?
なので、新しいプログラミング言語に取り組んだときも、その言語のパーサジェネレータがあれば、その言語用にSGFパーサを用意することは簡単なのです。

今日はこの辺で。