Smart Game Formatの文字列を解析します。
SGFは、ボードゲームのファイル、とくに囲碁の棋譜を保存するための標準フォーマットです。
SGFは比較的シンプルなフォーマットです。SGFファイルには通常、ノードの木構造が1つだけ含まれており、各ノードはプロパティリストです。プロパティリストにはキーと値のペアが含まれ、各キーは1回しか登場しませんが、複数の値を持つことができます。
この演習では、SGFの文字列を解析し、プロパティの木構造を返します。
SGFファイルは次のようになっています。
(;FF[4]C[root]SZ[19];B[aa];W[ab])
これは3つのノードを持つ木構造です。
ご想像のとおり、SGFファイルには子を1つだけ持つノードがたくさんあります。だからこそ、そのための省略記法が用意されています。
SGFは、手順のバリエーションを表現できます。囲碁を打つ人は検討の中で何度も手を戻します(これを試してみよう、うまくいかない、ではあれを試そう)し、SGFはそうした手順のバリエーションを扱えます。たとえば、次のようになります。
(;FF[4](;B[aa];W[ab])(;B[dd];W[ee]))
ここでは、ルートノードに2つのバリエーションがあります。1つ目(慣例により、実際に打たれた手を示します)は、黒が1-1に打つものです。黒は先生からこのファイルを受け取りました。先生は、ルートノードの2つ目の子にある、より理にかなった手を指摘しました。それがB[dd]です(4-4の点で、隅を取るごく標準的な布石です)。
1つのキーに複数の値を関連付けることができます。たとえば、次のとおりです。
(;FF[4];AB[aa][ab][ba])
ここでは、AB(黒を追加)を使って、盤面に黒石を3つ追加しています。
プロパティの値はすべてSGF Text型です。他の値の型を実装する必要はありません。Text型の完全なドキュメントも読めますが、重要な点のまとめを以下に示します。
\の直後にある場合は削除され、それ以外の場合は改行のまま残ります。\はエスケープ文字です。\の後にある空白以外の文字は、そのまま挿入されます。\の後にある空白文字は、上記のルールに従います。SGFには、\tや\nのような空白文字のためのエスケープシーケンスはありません。次の2つを混同しないよう注意してください。
文字列リテラル内のエスケープシーケンスは、SGFパーサーに渡される前に、そのプログラミング言語のパーサーによってすでに処理されている場合があります。
SGFには(そして一般に解析には)、もう少し複雑な点がいくつかありますが、ほとんどは無視してかまいません。入力はUTF-8でエンコードされているものと想定してください。テストにはcharsetプロパティが含まれないので、そこは気にしなくて大丈夫です。さらに、改行はすべてunixスタイル(\nで、テストに\rや\r\nは含まれません)であり、プロパティやノードなどの間の省略可能な空白もテストには含まれないものと想定してかまいません。
ときには例外を発生させる必要があります。そのときは、エラーの原因が何であるかを示す意味のあるエラーメッセージを必ず含めるようにしましょう。こうするとコードが読みやすくなり、デバッグにも大いに役立ちます。エラーの原因が特定の型になるとわかっている場合は、組み込みのエラー型の中から1つを選んで発生させてもかまいませんが、その場合でも意味のあるメッセージを含めるようにしましょう。
この演習では、raise文を使って、入力に適切な区切り文字がない場合、すべて大文字でない場合、ノードを持つ木を形成していない場合、または木をまったく形成していない場合にValueErrorを「スロー」する必要があります。テストに合格するには、exceptionをraiseし、メッセージも一緒に含める必要があります。
メッセージ付きでValueErrorを発生させるには、exception型の引数としてメッセージを書きます。
# if the tree properties as given do not have proper delimiters.
raise ValueError("properties without delimiter")
# if the tree properties as given are not all in uppercase.
raise ValueError("property must be in uppercase")
# if the input does not form a tree, or is empty.
raise ValueError("tree missing")
# if the input is a tree without any nodes.
raise ValueError("tree with no nodes")