ochalog

RubyとMediaWikiとIRCが好き。

NHK 杯囲碁トーナメントの棋譜ファイルを SGF 形式で入手する:Part 2. SGF 形式への変換

前回公式サイトから棋譜データを入手する方法を紹介した。今回は入手した棋譜データの構造と SGF 形式に変換する方法を述べる。

データ構造

例として 2013 年 1 月 27 日の棋譜データ http://cgi2.nhk.or.jp/goshogi/kifu/score.cgi?d=20130127&t=i を見てみよう。

NIP100;
OnAir=2013/01/27;
Title=第60回NHK杯;
Stage=3回戦第7局;
Start=2012/12/10;
End=2012/12/10;
Player1=山田 規三生 九段;
Player2=山下 敬吾 名人;
Result=1;
p=0000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000pd;
h1=0;
h2=0;
p=0000000000000000000000000000000000000000000000000000000000002000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dd;
h1=0;
h2=0;
(中略)
END

棋譜データは キー=; と表されるプロパティの集合から成る。プロパティのうち、SGF と対応するものを以下に示す。

Title

イベント名。SGF では EV[第60回NHK杯]

Stage

ゲーム名。SGF では GN[3回戦第7局]

Start

対局開始日。ほとんどの場合 End と同じだろうから、対局日と見なしても良いだろう。この場合、SGF では DT[2012-12-10]

Player1

黒番の名前・ランク。値の書式は ランク。SGF では PB[山田 規三生]BR[九段]

Player2

白番の名前・ランク。SGF では PW[山下 敬吾]WR[名人]

Result

結果。値の書式は x(:y)?

x はどちらの棋士が勝ったか。1 ならば黒番の勝ち、2 ならば白番の勝ち。いずれでもなければ持碁。

y は勝ち方。t ならば時間切れ勝ち、0 以上の整数ならば y + 0.5 目勝ち。y が省略されていれば中押し勝ち。

この場合、SGF では RE[B+R]

p

着手。値のうち最初の 361 桁の数字は盤面の状態を表す。後ろの小文字アルファベットは着手の座標で、SGF と互換。SGF では、最初は B[pd] で、次は W[dd]。以下同様に続く。

変換プログラム

以上の規則を基に SGF 形式への変換プログラムを書いた(score2sgf.rb)。使用法は

ruby score2sgf.rb ./score/score-60/20130127.txt

など。拡張子.sgf に変わった同名の SGF ファイルが、棋譜データファイルのあるディレクトリに出力される。

一括変換は、例えば bash の場合

for f in ./score/score-60/*.txt; do
  ruby score2sgf.rb $f
done

のように行えばよい。

# coding: utf-8
 
unless ARGV.length == 1
  puts "usage: ruby score2sgf.rb FILE"
  abort
end
 
file_name_score = ARGV[0]
file_name_sgf = file_name_score.sub(/\.[^.]+$/, ".sgf")
 
# SGF のルートノード
root = {
  FF: 4,
  CA: "utf-8",
  GM: 1,
  SZ: 19,
  KM: 6.5
}
 
# 棋譜ファイルの読み込み
score = nil
File.open(file_name_score) {|f| score = f.read.gsub(/[\r\n]/, "")}
props_score = score.split(";").map {|p| p.split("=")}
 
# 棋譜データの解析
moves = []
move_num = 0
 
props_score.each do |p|
  case p[0]
  when "Title"
    root[:EV] = p[1]
  when "Stage"
    root[:GN] = p[1]
  when "Start"
    root[:DT] = p[1].gsub("/", "-")
  when "Player1" # 黒の棋士
    b_rank = ""
    b_name = p[1].sub(/ ([^ ]+)$/) do
      b_rank = $1
      ""
    end
    root[:PB] = b_name
    root[:BR] = b_rank
  when "Player2" # 白の棋士
    w_rank = ""
    w_name = p[1].sub(/ ([^ ]+)$/) do
      w_rank = $1
      ""
    end
    root[:PW] = w_name
    root[:WR] = w_rank
  when "Result"
    result = p[1].split(":")
    case result[0]
    when "1" # 黒の勝ち
      if result[1]
        if result[1] == "t" # 時間切れ
          root[:RE] = "B+T"
        else # 作り碁
          root[:RE] = "B+#{result[1].to_f + 0.5}"
        end
      else # 中押し勝ち
        root[:RE] = "B+R"
      end
    when "2" # 白の勝ち
      if result[1]
        if result[1] == "t" # 時間切れ
          root[:RE] = "W+T"
        else # 作り碁
          root[:RE] = "W+#{result[1].to_f + 0.5}"
        end
      else # 中押し勝ち
        root[:RE] = "W+R"
      end
    else # 持碁
      root[:RE] = 0
    end
  when "p"
    move_num += 1
    c = p[1].sub(/\d+/, "")
    if move_num.odd?
      moves << {B: c}
    else
      moves << {W: c}
    end
  end
end
 
# SGF の生成・書き出し
def sgf_node(prop)
  s = ";"
  prop.each {|k, v| s << "#{k}[#{v}]"}
  s
end
 
sgf = "(" + sgf_node(root)
moves.each {|p| sgf << sgf_node(p)}
sgf << ")"
 
File.open(file_name_sgf, "w:utf-8") {|f| f.print(sgf)}
puts "converted #{file_name_score} to #{file_name_sgf}"