コンピュータゲームの実装: リバーシ

作田 誠

目次

1  コンピュータゲームの実装 (1) データ表現と探索法

1.1 リバーシというゲーム

リバーシ (登録商標オセロとしても知られる) は二人ゼロ和完全情報ゲームでコンピュータゲーム研究の対象として盛んにプログラムが開発されてきた。現在はコンピュータが人間世界チャンピオンよりもはるかに強くなっている。また、先手後手双方が最善を尽くした時引き分けになることをコンピュータプログラムで証明したとする論文が2023年10月に arXiv (アーカイブ) で公開されている。

一方、上のような高度な話題とは別に、リバーシはルールが単純なためプログラムを作りやすく、入門用あるいは教育用ゲームプログラミングの対象として利用されることも多い。

  • 8マス×8マスの盤と黒と白が表裏になっているディスクを使う。
  • 黒側と白側とが対戦する。
  • 開始時に黒と白それぞれ2個のディスクを盤中央の4マスに同じ色が対角線上になるように配置しておく。
  • 黒から始めて、黒のディスクと白のディスクを交互に置いていく。置くときには必ず相手側のディスクを自分のディスクで挟まないといけない。上下左右ナナメ8方向について挟まれた相手側のディスクはすべて裏返され味方の色になる。挟んだ相手側ディスクはすべて裏返さなければならない。
  • 自分の番で挟むディスクがないときはパスしなければならない。次は相手の番になる。
  • 挟むディスクが一つでもあるときはパスできない。
  • 両方ともディスクを置けなくなったとき、もしくは盤面全部にディスクが埋まったときに終了となる。
  • 終了時に自分側のディスクの多い方が勝ちとなる。同数の場合は引き分けとなる。

今回は 8マス×8マス のゲームだけでなく、4マス×4マス、6マス×6マス、10マス×10マス にも対応させる。

1.2 データ表現

コンピュータゲームを作るにあたってGUIでなくコンソール上で動作するタイプのプログラムも作られるが、人間が見たときアピールが弱く、プログラミングするやる気も起きにくい。そこで今回はGUIプログラムとするが、C言語やC++言語は標準のGUIライブラリを持たないので、Visual C++ を使って Windows 上で動作するプログラムを作るなど、一般性のない特定環境での開発となりあまり教育的でない。

そこで、様々な環境で動作させられるよう、言語はJavaを使うこととし、雛形を用意しそれに追加・修正を加える演習をやってもらいプログラムを動作させてもらう。演習では極力Java特有の知識が必要なものは避け、C言語の構文の知識+α程度で行えるものを用意する。

とは言え、多くの部分はJavaを用いたオブジェクト指向のプログラムになっている。簡単に、プログラムの構造を示す。

1.2.1 全体構造

主なクラスの構造を示す。

App
アプリケーション (プログラム自身) を表す

Game
一ゲームを表す

Player
プレーヤを表す
ComputerPlayer
コンピュータプレーヤを表す

AlphaBeta
αβ探索をする

Kyokumen
ゲーム中の一状態 (局面) を表す

Sashite
指し手 (リバーシの着手) を表す

Evaluator
局面の評価関数を表す抽象クラス。実際の評価関数はサブクラス

下に主なGUI関係のクラスを示す。


ReversiFrame
メインのフレームウィンドウを表す。

StartGameDialog
「ゲーム」ボタンを押したときのダイアログを表す。

1.2.2 盤の表現

局面を表す Kyokumen クラスの中で、盤は ban という一次元配列で表されている。実際の盤の大きさよりひと回り大きい配列を用意し、外側は盤の外を表すようにする。
配列のインデックスと実際の盤の位置との対応を、4×4盤、6×6盤、8×8盤のそれぞれについて示す。10×10盤のものは省略したが同様になっている。

6行6列左上が0右下が35
図1  4x4盤でのban配列


8行8列左上が0右下が63
図2  6x6盤でのban配列


6行6列左上が0右下が99
図3  8x8盤でのban配列


ban 配列の要素は、

   BLACK = -1 なら黒が置かれている
   EMPTY = 0 なら何も置かれていない
   WHITE = 1 なら白が置かれている

1.2.3 局面の表現

局面を表す Kyokumen クラスには盤の状態を表す ban 配列のほかに以下の変数などが定義されている。

  color
      現在の手番のディスクの色を表す。

  tesuu
      現在の局面の進行手数を表す。

  ishisuu[0]
      現在の局面の黒ディスクの数を表す。
  ishisuu[1]
      現在の局面の白ディスクの数を表す。

  emptyCount
      現在の局面で空いているマスの数を表す。

1.2.4 着手の表現

着手を表す Sashite クラスには以下の変数などが定義されている。

  xy
      着手の ban 配列上でのインデックスを表す。パスなら 0 がセットされる。

1.2.5 ゲームの表現

ゲームを表す Game クラスには以下の変数などが定義されている。

  player[0]
      黒側のプレーヤを表す。
  player[1]
      白側のプレーヤを表す。

  kyokumen
      ゲームにおける現在局面を表す。

  配列 sasiteretu
      ゲームにおいて現在局面までに指された着手を記録してある配列。
      sasiteretu[0] が一手目、sasiteretu[1] が二手目、と続く。

1.2.6 探索の表現

αβ探索を表す AlphaBeta クラスには以下の変数などが定義されている。

  kyokumen
      ゲームにおける現在局面を表す。

1.3 統合開発環境 Eclipse の使用

これから取り組むリバーシのプログラムでは、統合開発環境 (IDE) として Eclipse を使用する。
Eclipseは大学の演習室PCにインストールされており、そのまま利用できる。

各自のPCで演習を行いたい場合は、各自の責任でmyFITドキュメントにある説明などを参考にして各自のPCに Eclipse をインストールして使用する。

1.4 negamax探索の実装

対象としているゲームは、一方が勝ちで他方が負けになるゼロ和ゲームと言われるものである。
このとき、評価関数を手番が反対だと値のプラスマイナスが反転するようにして、個々の局面における手番側の有利をプラスとするように作ることができる。

      評価関数(自分側から見たとき) + 評価関数(相手側から見たとき) = 0

  つまり、

      評価関数(自分側から見たとき) = - 評価関数(相手側から見たとき)
      評価関数(相手側から見たとき) = - 評価関数(自分側から見たとき)

このような評価関数を使うと negamax探索 が実現できる。

αβ探索 で示したように、固定深さまで一律に探索する単純なnegamax探索は以下の再帰的アルゴリズムで記述できる。

リスト1  negamax探索

整数型  negamax( 残り深さ, 現在局面, a, b ) {

    「現在局面」で可能な着手をすべて生成

    可能着手数が 0 (末端節点) なら {
        「現在局面」を末端節点用の評価関数によって評価した値を返す
    }

    「残り深さ」が 0 なら {
        「現在局面」を評価関数によって評価した値を返す    // 探索打ち切り
    }

    m = a

    すべての可能着手について {

        「現在局面」を着手によって一手進める

        value = -negamax(「残り深さ」- 1, 現在局面, -b, -m)

        もし value > m または 第一候補着手なら {
            最善着手列として記録
        }
        もし value > m なら {
            m = value
            もし m が b 以上 なら {
                着手によって進んだ「現在局面」を一手戻す
                m を返り値として戻る /* β-cut (MAXノード) または α-cut (MINノード) */
            }
        }

        着手によって進んだ「現在局面」を一手戻す
    }

    m を返り値として戻る
}

上のリストのnegamax探索を使ってある局面の着手を選択させる場合、以下のように下限値 a を -∞ 上限値 b を +∞ でnegamax探索を呼べばよい。なお、プログラム上では ∞ ではなく、局面の評価関数の値にならないような大きい値とすればよい。

        r = negamax( 探索深さ, 局面, -∞, ∞ );

AlphaBeta の中にあるnegamax探索の実装においては、以下で示すコード部品を使用する。

evalPosition( kyokumen )
        パラメータ kyokumen で表されるゲームの一局面を評価関数によって評価した値、すなわち、
        kyokumen の手番側から見た評価値を求める
evalTerminalPosition( kyokumen )
        パラメータ kyokumen で表されるゲームの末端局面 (ゲームの終わった局面) を評価関数によって評価した値、すなわち、
        kyokumen の手番側から見た評価値を求める。
        kyokumen が末端局面であることが前提
Sashite[] moves = new Sashite[ReversiFrame.MAX_SASHITE];
int numMoves = kyokumen.generateMoves(teban, moves);
        kyokumen で可能な teban 側の着手をすべて生成し、
        着手配列 moves のインデックス 0 の要素から順に入れる。
        可能な着手の数が返され、変数 numMoves にセットされる。
kyokumen.makeMove( moves[i] );
        kyokumen を着手 moves[i] によって一手進める
kyokumen.undoMove( moves[i] );
        着手 moves[i] によって進んだ kyokumen を一手戻す


演習15 リバーシ雛型プログラムのダウンロードとインポート

Teams授業チームの「ファイル」にある リバーシ雛型プログラム ReversiProto.zip をダウンロードし、Eclipseでプロジェクトをインポートしなさい。

●インポート手順

Eclipseを起動する。演習室PCでは、デスクトップの All-in-one Eclipse のアイコンなどから起動できる。

起動すると、最初にワークスペースを聞いてくる。Eclipseはプロジェクト単位でプログラム開発をするが、ワークスペースはプロジェクトの置かれるフォルダのことである。

演習室PCの場合、ワークスペースはデフォルトでは H:\workspace フォルダになっている。原則、このままで利用することを勧める。
授業別にワークスペースを設定する場合などは、別フォルダを設定する。Hドライブ等、演習室PCが再起動しても消えてしまわない場所に、授業用のフォルダを作り、設定すること。一応、USBディスクのフォルダに設定することも可能だと思われるが、別の日に同じUSBディスクを持参していないと作業が続けられないので注意が必要となる。

※ 起動時にワークスペースを授業用のフォルダに設定できなかった場合は、

メニューの 「ファイル(F)」-「ワークスペースの切り替え(W)」-「その他(O)...」

を選択し、設定すること。

Eclipseを起動し、プロジェクトが何も開いていない状態とし、

メニューの 「ファイル(F)」-「インポート(I)」 を選択
「一般」

の中にある

「既存プロジェクトをワークスペースへ」

を選択する。

次の画面で、

ラジオボタンの「アーカイブ・ファイルの選択(A)」 をチェックし、
「参照(R)」ボタンをクリックして、ダウンロードしたzipファイルを指定

すると、

プロジェクト Reversi にチェックが入っているはず
「完了(F)」ボタンをクリック

以上により、Eclipseのプロジェクトが開き、ソースファイルの閲覧・編集などが可能になる。

※ ソースはプロジェクトフォルダの中の

src\reversi

フォルダに入っている。



演習16 negamax探索の実装
リスト1と上のコード部品を参考に、AlphaBeta クラスの中にある negamax探索を完成させなさい。

雛型プログラムの AlphaBeta.java ファイル内の negamax メソッドの中で TODO と書かれてある部分を追加または修正すればよい。



※ プログラムの実行の際、大学PC演習室で Eclipse を使っている場合は正しいソースプログラムであっても実行時エラーで止まってしまう。この現象を解決するための方法をStreamのビデオで公開している。


演習17 4x4リバーシでの完全読み

完成させたプログラムをビルド・Javaアプリケーションとして実行し、4x4リバーシで先手・後手両方の探索深さを 20 に設定して完全読みの状態でプレイさせ、ゲームの結果を調べなさい。何手で、何対何で、どちらがいくつ勝つだろうか?

※ ソースファイルを追加・修正したら、保存し、実行する。

初回の実行の際のみ、

メニュー「実行(R)」-「実行構成(N)...」

を開き設定をする必要がある。開いたダイアログの左側のペインの

> Javaアプリケーション

となっている箇所の > をクリックして開き、中にある

App

を選択する。App という名前の Reversi の実行設定が出るので、下部にある「実行(R)」ボタンを押せばよい。

二回目以降の実行では、単に

メニュー「実行(R)」-「実行(R)」

を選べばよい。もしくは Ctrl-F11 (Ctrl キーと F11 キーの同時押し) でもよい。



演習18 4x4リバーシMisereゲームでの完全読み

ルールを逆にしたディスクの少ない方が勝つゲーム (Misereゲーム と言う) が考えられる。4x4リバーシのMisereゲームを実装し、完全読みさせてゲームの結果を調べなさい。 何手で、何対何で、どちらがいくつ勝つ(負ける??)だろうか?

(ヒント) AlphaBeta の中にある末端局面の評価関数 evalTerminalPosition の中をごく少し修正するだけでよい。


演習18が終ったら、評価関数を元に戻しておくこと。

αβ探索が正常に動作しないと、以下の内容が無効になる。
まずは、演習17演習18で20手探索で実行した結果が正しいものであることを確認すること。

通常ゲーム結果
Misereゲーム結果

2  コンピュータゲームの実装 (2) 評価関数

2.1 評価関数の重要性

人間にとってそこそこに難しい完全情報ゲームでは、現在の高速・大容量なコンピュータでも一般に最後まで読みきることはできない。そのため、コンピュータの探索では探索を途中で止めて評価する評価関数が使われる。評価関数の出来はプログラムの強さに直結するが、確固たる作成指針はなく、各ゲーム種の性質に応じて工夫が必要な部分となっている。

2.2 完全読み用の評価関数

前回の評価関数は単に黒と白の石数の差を求めるものだった。 実際のリバーシでは途中で石が多いのは必ずしも有利ではないので、この評価関数を使っても弱いプログラムにしかならない。

しかし、終り近く、具体的には空きマスの数が十数個程度になれば、探索を途中局面で止めない完全読みが短時間で実行可能になる。 そこで空きマスが 10 ~ 15 個程度で完全読みに切り替える機能をつける。
完全読みでは評価するのがすべて末端局面 (勝負がついて結果が分かった局面) だけなので、石数の差を求める評価関数でよい。

4マス×4マスのゲームでは最初から完全読みが短時間で可能で、石数の差を求める評価関数だけを使えばよい。6マス×6マス以上のゲームでは最初の辺りでは短時間での完全読みはできないので、工夫した評価関数を用意する必要がある。


演習19 完全読み切り替え機能

雛型プログラムの ComputerPlayer.java ファイルの最後にある decideMove() の中の TODO と書かれてある部分に以下を追加し完全読み切り替え機能が働くようにしなさい。

擬似コード

    もし完全読みでないときは {
      emptyCount = 局面の空きマス数
      もし emptyCount が emptyCountOnPerfectLookup 以下なら {
        完全読みに切り替え
        探索深さ最大を 2 * emptyCount にする  /* 絶対に途中で探索が終わらないようにする */
      }
    }


実際のコード (これを貼り付ければよい)

    if (!isPerfectLookup()) {
      int emptyCount = sakikyokumen.getEmptyCount();
      if (emptyCount <= emptyCountOnPerfectLookup) {
        setPerfectLookup(true);
        maxDepth = 2 * emptyCount;
      }
    }


2.3 各マスに点数付けする評価関数

リバーシでは隅 (☆) や辺 (A, B) を取ることが重要で、隅のナナメのマス (X)・隅の隣のマス (C) は「自分が打つと相手に隅を取られる危険性が高くなる」ので最初のうちは打ってはいけないとされている。

隅☆, 辺 C, A, B, 隅のナナメ X
図4  8x8盤でのマスの種類分け

その知識を生かすべく、各マスに点数をつけ、自分の石の点数の和と相手の石の点数の和との差を評価関数にする方法が知られている。 隅のナナメのマス (X) には大きいマイナス点をつけ自分からは極力打たないようにする。隅の隣のマス (C) にも X ほど大きくはないマイナス点をつけて打ちにくくしておく。 また、その他の辺は取っていると有利なことが多いので少しだけプラス点を与えておく。
以上の考えで、例えば図のような隅辺Xのみの点数付けができる。

☆35, C-3, A3, B3, X-10
図5  8x8盤での点数付け例


演習20 点数付け評価関数1 (隅辺Xのみ点数付け)

上図の点数付けによる評価関数を作り、8x8リバーシのコンピュータの強さを、(1) 自分が対戦して、(2) 石数だけの評価関数と対戦させて、調べてみなさい。

雛型プログラムの Evaluator.java ファイルのクラス Eval8x8B 内の TODO 部分を追加または修正すればよい。



演習21 点数付け評価関数2 (各自の工夫)

自分で工夫した点数付けによる評価関数を作り、8x8リバーシのコンピュータの強さを、(1) 自分が対戦して、(2) 演習20の評価関数と対戦させて、調べてみなさい。

雛型プログラムの Evaluator.java ファイル内で二番目に TODO と書かれてある部分を修正すればよい。


※ 複数プログラムを同時起動してテストする方法
にあるように、実行可能ファイル Reversi.jar を作って複数プログラムを同時起動してテストできる。例えば、2プレーヤ同士の対戦なら、10ゲームなら先手・後手を 5-5 になるようにして結果をみる。
実行可能jarファイルでテストする場合は、ソースを修正・保存する度に実行可能jarファイルを作り直す必要があるので注意する。

2.4 本格的な評価関数

各マスに点数付けする評価関数でも人間相手なら中々負けないプログラムになる。しかし、強いコンピュータプログラムには到底かなわない。評価関数も上のような単純なものでなく、いくつもの要素を評価し総合して数値化するものにする必要がある。

考慮すべき要素としては例えば以下のようなものがある。

可能着手数

自分の着手が多くなると有利なのでプラス評価し、相手の着手が多くなると不利なのでマイナス評価する。

開放度

開放度とはある石の回りの空きマスの個数のこと。開放度が大きい石ほど相手に返される可能性が高いのでマイナス評価する。

山・ウイングなど

辺に石が固まった状態 (山・ウイングなどと言われる) を色々なパターン別に点数付けして評価する。

確定石の個数

確定石とは絶対返されなくなった石のことで自分の確定石数をプラス評価・相手の確定石数をマイナス評価する。正確に求めるのは難しいので近似値を求めてもよい。

リバーシのトッププログラムの評価関数は、上で示したような多数のパラメータを持っており、自動学習によってパラメータが調整されている。

講義では詳しく説明しきれないので省略するが、興味のある人は書籍やWebサイトなどで調べてみるとよい。またWebにはソースコード付きで公開されている強いプログラムも多くある。参考にしてみるとよい。

3  コンピュータゲームの実装 (3) 調整・強化

3.1 プログラムの識別

学籍番号でプログラムを識別できるようにする。


演習22 プログラムの識別

ComputerPlayer.java の以下の部分に自分の学籍番号を入れ書き直して保存する。

  // 名前の自動決定
  void setName() {
    setName("$自分の学籍番号-" + searchDepth + "-" + emptyCountOnPerfectLookup
            + "-" + evalIndex);
  }

同様に ReversiFrame.java の以下の部分に自分の学籍番号を入れ書き直して保存する。

  static final String DEFAULT_HUMAN_NAMEBASE = "自分の学籍番号";

同様に Evaluator.java の以下の二箇所に自分の学籍番号等を入れ書き直して保存する。

クラス Eval8x8C の、javadoc コメント内

 * @author 自分の学籍番号と氏名

その下のコンストラクタ内

  Eval8x8C() {
    name = "自分の学籍番号";
  }


3.2 他プログラムとの対戦による調整・強化

自分のプログラムだけでは強さを高めるにも目標が設定しにくい。そこで、他のプログラムと対戦させ弱点を調べ改良していく手法がよく採られる。

他のプログラムとの対戦には、現在ではネットワークを使った通信を使う場合が多いが、相互のプログラムがネットワーク対応で同一通信プロトコルであることが条件で、ネットワーク対戦ができない場合もある。そこで今回は以下のように人手を介した対戦を行う。

  1. A, B 両方のパソコンで各自のプログラムを起動する。
  2. A は「コンピュータが黒」「人間が白」でゲームを開始する。
    B は「コンピュータが白」「人間が黒」でゲームを開始する。
  3. A の番では、A のコンピュータが着手したら A は B に指し手を記号 (c4 など) で伝える。
    B は指し手を聞いたら自分でマウスクリックでその指し手を着手する。
  4. 次の B の番では、B のコンピュータが着手したら B は A に指し手を記号で伝える。
    A は指し手を聞いたら自分でマウスクリックでその指し手を着手する。
  5. 以下ゲームが終わるまで繰り返す。

できれば他の人のプログラムと対戦し合って互いに強化し合うとよい。

時間に余裕があれば、黒と白を入れ換えて何ゲームかやって勝敗を見た方がよい。

一台のパソコンで二つプログラムを起動し全部自分一人で対戦させることもできる。

対戦相手を変えてゲームの相手と勝敗を記録していき、適当なところで対戦の状況を分析し、必要ならばプログラムに変更を加えていく。変更したときはその内容を記録しておく。 二回目以降の変更では今までの変更点も参考にできる。

3.3 Eclipseでの、実行可能jarファイルおよびプロジェクトのアーカイブ作成の作成

実行可能jarファイルの作成方法

EclipseでReversiプロジェクトを開いた状態で、

「ファイル(F)」
「エクスポート(O)」

で、

「Java」

の中にある

「実行可能JARファイル」

を選択し、

「次へ(N)」

をクリックする。

次の画面で、

「起動構成(L)」に「App - Reversi」が設定されていなければ、ドロップダウンリストで設定する。
「エクスポート先(D)」の「参照(R)」ボタンをクリックし、保存場所と名前を指定する。
ファイル名は Reversi.jar とする。
「ライブラリー処理」は「生成されるJARに必須ライブラリを抽出(E)」とする。

と入力・設定した上で、

下部にある「完了(F)」ボタンをクリック

すれば、指定した場所に実行可能jarファイルが作成される。

実行可能jarファイルの実行方法
PowerShell または コマンドプロンプト を起動する。まず、下記のようにjarファイルのあるフォルダに移動する。

(PowerShell の場合)

cd    jarファイルのあるフォルダ

(コマンドプロンプト の場合)

cd    /D    jarファイルのあるフォルダ

※ 名前にスペースを含むフォルダがある場合は上記のフォルダ指定で

"jarファイルの ある フォルダ"

のように、フォルダ指定全体をダブルクォーテーションで囲むこと。

次に、以下のコマンドでjarファイルを実行する。

java    -jar    Reversi.jar

また、以下のように、先頭に start を付けたコマンドを何回も実行すれば、すきなだけ複数プログラムを起動できる。

start    java    -jar    Reversi.jar

課題11課題12は履修者全員のための課題で、よくテスト・調整した上で、指定を守り提出のこと。
提出されていても、正常動作しない、あるいは、著しく弱いプレイをするプログラムの場合は、課題達成と認めない。



課題11 リバーシのまとめ (1) Evaluator.javaの変更部分 (重要課題)

テキストエディタで、先頭に「課題番号 課題名」、「自分の学籍番号・氏名」を入れた後、Evaluator.java の評価関数 Eval8x8C に各自で調整を加え、できるだけ強いプログラムとした部分のソースファイル (Evaluator.java の各自が追加・修正した部分のみ) を入れ、テキストファイル (拡張子 txt) として保存し、授業Webページから提出。



課題12 リバーシのまとめ (2) 実行可能jarファイル (重要課題)

Eclipse を使って生成した「実行可能jarファイル Reversi.jar」を提出する。
プログラムが正常に起動し、対戦開始から終了まで一通り正常動作することを確かめた上で提出すること。
コンピュータが著しく弱いプレイをするものは認めない。



プロジェクトのアーカイブ作成方法   (下の 課題13 をやって提出する人のみ)

EclipseでReversiプロジェクトを開いた状態で、

「ファイル(F)」
「エクスポート(O)」

で、

「一般」

の中にある

「アーカイブ・ファイル」

を選択し、

「次へ(N)」

をクリックする。

次の画面では、

プロジェクト Reversi だけにチェックが入っているはず。
「宛先アーカイブ・ファイル(A)」の「参照(R)」ボタンをクリックし、保存場所と名前を指定する。
ファイル名は ReversiSource.zip とする。
オプションは「ZIPフォーマットで保管(Z)」がセットされているはず。

と入力・設定した上で、

下部にある「完了(F)」ボタンをクリック

すれば、指定した場所にZIPファイルが作成される。


課題13 独自工夫して強化したリバーシプログラムzipファイル (任意提出・発展課題)

Reversiのプロジェクトを別名のプロジェクトにコピーした上、課題11課題12のプログラムにはない独自の工夫をしてコンピュータプレーヤを強化しなさい。Eclipse を使って独自強化版のプロジェクトをアーカイブしたZIPファイル ReversiSource.zip を作成し提出しなさい。