2017年9月11日月曜日

【Ruby】リバーシの棋譜ファイル読み込み(GGF)

現在リバーシのゲームをつくっています。そこで棋譜学習をしようと思ったのですが、棋譜の読み込みとかいろいろ大変だったのでまとめました。

今回はGGFファイルを対象にRubyで局面と評価値のセットをCSVファイルに抽出するスクリプトを作成するところまで。学習がうまくいったら続きを書きます。



GGSの棋譜

今回は大量のリバーシプログラム同士の対局が記録されているGGSの棋譜
https://www.skatgame.net/mburo/ggs/game-archive/Othello/?C=N;O=A
を使用しました。

これらはGGFというフォーマットのファイルになっているのですが、自分の目的に合ったデータを抽出するために既存のソフトウェアは使用せず読み込みスクリプトを書きます。GGFについては次章へ。

GGFファイル

Generic Game Format の略でリバーシに限らず様々な盤ゲームの棋譜を記述できるフォーマットです。

フォーマットの詳細は
https://skatgame.net/mburo/ggsa/ggf
に書かれています。

簡単にまとめると各対戦プレイヤーの情報と試合結果、盤の大きさ、盤の初期状態、終局までの指し手などを記録できます。

リバーシプログラム同士の対局なので、指し手の情報に局面の評価値が付属しています(評価値付きとそうでないものが混在)。

GGSファイルのひとつ Othello.01e4.ggf から選んだ1試合分の例を以下に示します。
わかりやすいように改行していますが、実際はこれが1行に繋がっていて1行1試合となっています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(;
GM[Othello]PC[GGS/os]DT[2000-6-25 06:30 EST]
PB[scorpion]PW[stepmose]
RB[1883.42]RW[1127.31]
TI[15:00//02:00]TY[8]RE[+40.00]
BO[8
--------
--------
--------
---O*---
---*O---
--------
--------
--------
*]
B[E6/-1.00]W[F4//0.67]B[D3/-6.38/0.50]W[D6//0.78]B[E3/-20.64]W[F6//0.73]B[E7/-2.64]W[C4//2.61]B[F5/-2.70]W[F8//5.50]B[C3/11.59/0.01]W[F7//3.84]B[F3/10.12/0.02]W[C2//2.97]B[C5/12.37/0.02]W[C6//3.04]
B[B4/11.11/0.04]W[A3//6.66]B[B3/13.95/0.04]W[A4//3.85]B[B5/18.65/0.03]W[G6//13.00]B[D2/20.50/0.04]W[E1//8.15]B[G5/25.84/0.04]W[E2//9.48]B[F2/26.27/0.04]W[F1//7.53]B[G4/30.60/0.05]W[H3//4.33]B[H4/30.56/0.05]W[H6//6.39]
B[H2/28.59/0.04]W[G3//3.62]B[G2/30.05/0.03]W[A6//4.34]B[H5/32.95/0.03]W[B6//1.67]B[H7/34.04/0.03]W[H1//0.74]B[G7/33.89/0.02]W[H8//0.27]B[D8/34.76/0.03]W[D7//0.36]B[B2/37.34/0.03]W[A1//0.22]B[G8/39.52/0.03]W[pass//0.17]
B[C8/34.89/0.02]W[E8//0.95]B[B1/38.86/0.02]W[D1//0.38]B[C1/40.92/0.01]W[B8//0.18]B[A8/41.68/0.01]W[pass//0.17]B[A7/40.00/0.01]W[B7//0.17]B[C7/40.00/0.01]W[pass//0.17]B[A5/40.00/0.01]W[pass//0.17]B[A2/40.00/0.01]W[pass//0.17]B[G1]
;)

読み込みスクリプト

ここから局面と評価値のセットをCSVファイルに抽出するスクリプトを書きます。
GGFファイルからは局面の状態まではわからないので、指し手の履歴を元に実際に局面を進めて評価値の情報があればそれを記録する、というようにします。学習自体はRubyで行わないので他の言語でも比較的読み込みやすいCSVファイルにしました。

局面をつくるためにRubyでリバーシ関連のもろもろができるgem、reversiを使用します。
手前味噌ですが、このgemは大昔に私がつくったものです。
今改めてみると肝心の局面を取り出すのがとても面倒であることに気が付きました。
(他にも直したい箇所がたくさん、、、)
局面は黒石、白石、空白のビットボードにしました。

評価値についてですが、GGFでは黒番も白番も勝勢であるほど正の評価値となっています。プレイヤーの色もセットで記録しなければいけないのは面倒なので、白番が勝っているほど負、黒番が勝っているほど正というように統一するために白番の評価値の符号を反転させています。

動作環境

  • Windows 10 64ビット
  • Ruby 2.1.5
  • reversi 2.0.4

1
$ gem install reversi

Gistにも同じコードをアップしてあります。
https://gist.github.com/seinosuke/a481196230676e038e779fdd968feed5

load_ggf.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
require "reversi"
 
# 盤面の状態を出力する際の見た目設定
Reversi.configure do |config|
  config.disk_color_b = 'cyan'
  config.disk_b = "O"
  config.disk_w = "O"
end
 
# 初期状態から始まっている8x8盤面のみ読み込む
valid_board =  /BO\[8 -------- -------- -------- ---O\*--- ---\*O--- -------- -------- -------- \*\]/
 
# 読み込むGGFファイル
ggf_path = "./hoge.ggf"
File.open(ggf_path, "r") do |ggf_file|
 
  # 同じ階層に読み込み結果のcsvファイルを生成
  csv_path = ggf_path << ".csv"
  File.open(csv_path, "w") do |csv_file|
    ggf_file.each_line do |line|
 
      # 初期状態が不正なものは除く
      next unless line.match(valid_board)
 
      # 大文字に統一し着手履歴のみ抽出
      moves_info = line
        .gsub("(;", "").gsub(";)", "").chomp
        .gsub("]/", "/").upcase
        .split("*]").last.split("]")
 
      # 着手履歴から試合を再生
      game = Reversi::Game.new
      moves_info.each do |move_info|
        col, info = move_info.split("[")
        info = info.split("/")
        move = info.first
 
        # 一手進める
        unless ["PASS", "PA"].include?(move)
          move = move.tr("A-H", "1-8").split("")[0..1].map(&:to_i)
          case col
          when "B" then game.player_b.put_disk(*move)
          when "W" then game.player_w.put_disk(*move)
          end
          game.board.push_stack
        end
 
        # 評価値と局面のセットを記録
        if (info.size > 1) && (!info[1].empty?)
          val = info[1].to_f
          val = -val if col == "W"
          b_bb = game.board.stack.last[:black]
          w_bb = game.board.stack.last[:white]
          e_bb = 0xFFFF_FFFF_FFFF_FFFF ^ (b_bb | w_bb)
          csv_file.puts "#{b_bb}, #{w_bb}, #{e_bb}, #{val}"
        end
      end
      # puts game.board
    end
  end
end


これはあくまで自分用の一例なので、目的が異なる方などはご自分でスクリプトを書いてみるのもよいかもしれません。

参考までに着手履歴の部分で私が発見したパターンを以下に挙げておきます。
小文字と大文字が混在しており、パスの表現だけでも pass, pa, PAがあります。

1
2
3
4
5
6
7
8
9
10
手番[着手/評価値(任意)/経過時間(任意)]
 
W[G5/-52.60/0.01]
B[H3//4.17]
B[pass//1.17]
W[h4/-10.42]
W[B2]
B[pa/-2.00]
W[PA//2.20]
B[d6]//1.22]



おわりに

とりあえず150ほどあるファイル全てをエラーなく読み込めました。
次回は学習編でお会いしましょう。


参考

2 件のコメント:

  1. 丁度同じサイトのGGFファイルの取り扱いに難儀していたので大変参考になりました。学習編も楽しみにしています。

    返信削除
    返信
    1. コメントいただきありがとうございます。
      私もネット上に情報が少ないと思ったので、お役にたててなによりです。

      削除