リバーシ (登録商標オセロとしても知られる) は二人ゼロ和完全情報ゲームでコンピュータゲーム研究の対象として盛んにプログラムが開発されてきた。現在はコンピュータが人間世界チャンピオンよりもはるかに強くなっている。また、先手後手双方が最善を尽くした時引き分けになることをコンピュータプログラムで証明したとする論文が2023年10月に arXiv (アーカイブ) で公開されている。
一方、上のような高度な話題とは別に、リバーシはルールが単純なためプログラムを作りやすく、入門用あるいは教育用ゲームプログラミングの対象として利用されることも多い。
今回は 8マス×8マス のゲームだけでなく、4マス×4マス、6マス×6マス、10マス×10マス にも対応させる。
コンピュータゲームを作るにあたってGUIでなくコンソール上で動作するタイプのプログラムも作られるが、人間が見たときアピールが弱く、プログラミングするやる気も起きにくい。そこで今回はGUIプログラムとするが、C言語やC++言語は標準のGUIライブラリを持たないので、Visual C++ を使って Windows 上で動作するプログラムを作るなど、一般性のない特定環境での開発となりあまり教育的でない。
そこで、様々な環境で動作させられるよう、言語はJavaを使うこととし、雛形を用意しそれに追加・修正を加える演習をやってもらいプログラムを動作させてもらう。演習では極力Java特有の知識が必要なものは避け、C言語の構文の知識+α程度で行えるものを用意する。
とは言え、多くの部分はJavaを用いたオブジェクト指向のプログラムになっている。簡単に、プログラムの構造を示す。
主なクラスの構造を示す。
App アプリケーション (プログラム自身) を表す Game 一ゲームを表す Player プレーヤを表す ComputerPlayer コンピュータプレーヤを表す AlphaBeta αβ探索をする Kyokumen ゲーム中の一状態 (局面) を表す Sashite 指し手 (リバーシの着手) を表す Evaluator 局面の評価関数を表す抽象クラス。実際の評価関数はサブクラス
下に主なGUI関係のクラスを示す。
ReversiFrame メインのフレームウィンドウを表す。 StartGameDialog 「ゲーム」ボタンを押したときのダイアログを表す。
局面を表す Kyokumen クラスの中で、盤は ban という一次元配列で表されている。実際の盤の大きさよりひと回り大きい配列を用意し、外側は盤の外を表すようにする。
配列のインデックスと実際の盤の位置との対応を、4×4盤、6×6盤、8×8盤のそれぞれについて示す。10×10盤のものは省略したが同様になっている。
![]() |
| 図1 4x4盤でのban配列 |
![]() |
| 図2 6x6盤でのban配列 |
![]() |
| 図3 8x8盤でのban配列 |
ban 配列の要素は、
BLACK = -1 なら黒が置かれている EMPTY = 0 なら何も置かれていない WHITE = 1 なら白が置かれている
局面を表す Kyokumen クラスには盤の状態を表す ban 配列のほかに以下の変数などが定義されている。
color
現在の手番のディスクの色を表す。
tesuu
現在の局面の進行手数を表す。
ishisuu[0]
現在の局面の黒ディスクの数を表す。
ishisuu[1]
現在の局面の白ディスクの数を表す。
emptyCount
現在の局面で空いているマスの数を表す。
ゲームを表す Game クラスには以下の変数などが定義されている。
player[0]
黒側のプレーヤを表す。
player[1]
白側のプレーヤを表す。
kyokumen
ゲームにおける現在局面を表す。
配列 sasiteretu
ゲームにおいて現在局面までに指された着手を記録してある配列。
sasiteretu[0] が一手目、sasiteretu[1] が二手目、と続く。
これから取り組むリバーシのプログラムでは、統合開発環境 (IDE) として Eclipse を使用する。
Eclipseは大学の演習室PCにインストールされており、そのまま利用できる。
各自のPCで演習を行いたい場合は、各自の責任でmyFITドキュメントにある説明などを参考にして各自のPCに Eclipse をインストールして使用する。
対象としているゲームは、一方が勝ちで他方が負けになるゼロ和ゲームと言われるものである。
このとき、評価関数を手番が反対だと値のプラスマイナスが反転するようにして、個々の局面における手番側の有利をプラスとするように作ることができる。
評価関数(自分側から見たとき) + 評価関数(相手側から見たとき) = 0
つまり、
評価関数(自分側から見たとき) = - 評価関数(相手側から見たとき)
評価関数(相手側から見たとき) = - 評価関数(自分側から見たとき)
このような評価関数を使うと negamax探索 が実現できる。
αβ探索 で示したように、固定深さまで一律に探索する単純な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 を一手戻す
Teams授業チームの「ファイル」にある リバーシ雛型プログラム ReversiProto.zip をダウンロードし、Eclipseでプロジェクトをインポートしなさい。
●インポート手順
Eclipseを起動する。演習室PCでは、デスクトップの All-in-one Eclipse のアイコンなどから起動できる。
起動すると、最初にワークスペースを聞いてくる。Eclipseはプロジェクト単位でプログラム開発をするが、ワークスペースはプロジェクトの置かれるフォルダのことである。
演習室PCの場合、ワークスペースはデフォルトでは H:\workspace フォルダになっている。原則、このままで利用することを勧める。
授業別にワークスペースを設定する場合などは、別フォルダを設定する。Hドライブ等、演習室PCが再起動しても消えてしまわない場所に、授業用のフォルダを作り、設定すること。一応、USBディスクのフォルダに設定することも可能だと思われるが、別の日に同じUSBディスクを持参していないと作業が続けられないので注意が必要となる。
※ 起動時にワークスペースを授業用のフォルダに設定できなかった場合は、
を選択し、設定すること。
Eclipseを起動し、プロジェクトが何も開いていない状態とし、
の中にある
を選択する。
次の画面で、
すると、
以上により、Eclipseのプロジェクトが開き、ソースファイルの閲覧・編集などが可能になる。
※ ソースはプロジェクトフォルダの中の
src\reversiフォルダに入っている。
雛型プログラムの AlphaBeta.java ファイル内の negamax メソッドの中で TODO と書かれてある部分を追加または修正すればよい。
※ プログラムの実行の際、大学PC演習室で Eclipse を使っている場合は正しいソースプログラムであっても実行時エラーで止まってしまう。この現象を解決するための方法をStreamのビデオで公開している。
完成させたプログラムをビルド・Javaアプリケーションとして実行し、4x4リバーシで先手・後手両方の探索深さを 20 に設定して完全読みの状態でプレイさせ、ゲームの結果を調べなさい。何手で、何対何で、どちらがいくつ勝つだろうか?
※ ソースファイルを追加・修正したら、保存し、実行する。
初回の実行の際のみ、
を開き設定をする必要がある。開いたダイアログの左側のペインの
となっている箇所の > をクリックして開き、中にある
を選択する。App という名前の Reversi の実行設定が出るので、下部にある「実行(R)」ボタンを押せばよい。
二回目以降の実行では、単に
を選べばよい。もしくは Ctrl-F11 (Ctrl キーと F11 キーの同時押し) でもよい。
ルールを逆にしたディスクの少ない方が勝つゲーム (Misereゲーム と言う) が考えられる。4x4リバーシのMisereゲームを実装し、完全読みさせてゲームの結果を調べなさい。 何手で、何対何で、どちらがいくつ勝つ(負ける??)だろうか?
(ヒント) AlphaBeta の中にある末端局面の評価関数 evalTerminalPosition の中をごく少し修正するだけでよい。
αβ探索が正常に動作しないと、以下の内容が無効になる。
まずは、演習17と演習18で20手探索で実行した結果が正しいものであることを確認すること。
人間にとってそこそこに難しい完全情報ゲームでは、現在の高速・大容量なコンピュータでも一般に最後まで読みきることはできない。そのため、コンピュータの探索では探索を途中で止めて評価する評価関数が使われる。評価関数の出来はプログラムの強さに直結するが、確固たる作成指針はなく、各ゲーム種の性質に応じて工夫が必要な部分となっている。
前回の評価関数は単に黒と白の石数の差を求めるものだった。 実際のリバーシでは途中で石が多いのは必ずしも有利ではないので、この評価関数を使っても弱いプログラムにしかならない。
しかし、終り近く、具体的には空きマスの数が十数個程度になれば、探索を途中局面で止めない完全読みが短時間で実行可能になる。
そこで空きマスが 10 ~ 15 個程度で完全読みに切り替える機能をつける。
完全読みでは評価するのがすべて末端局面 (勝負がついて結果が分かった局面) だけなので、石数の差を求める評価関数でよい。
4マス×4マスのゲームでは最初から完全読みが短時間で可能で、石数の差を求める評価関数だけを使えばよい。6マス×6マス以上のゲームでは最初の辺りでは短時間での完全読みはできないので、工夫した評価関数を用意する必要がある。
雛型プログラムの ComputerPlayer.java ファイルの最後にある decideMove() の中の TODO と書かれてある部分に以下を追加し完全読み切り替え機能が働くようにしなさい。
擬似コード
もし完全読みでないときは {
emptyCount = 局面の空きマス数
もし emptyCount が emptyCountOnPerfectLookup 以下なら {
完全読みに切り替え
探索深さ最大を 2 * emptyCount にする /* 絶対に途中で探索が終わらないようにする */
}
}
実際のコード (これを貼り付ければよい)
if (!isPerfectLookup()) {
int emptyCount = sakikyokumen.getEmptyCount();
if (emptyCount <= emptyCountOnPerfectLookup) {
setPerfectLookup(true);
maxDepth = 2 * emptyCount;
}
}
リバーシでは隅 (☆) や辺 (A, B) を取ることが重要で、隅のナナメのマス (X)・隅の隣のマス (C) は「自分が打つと相手に隅を取られる危険性が高くなる」ので最初のうちは打ってはいけないとされている。
![]() |
| 図4 8x8盤でのマスの種類分け |
その知識を生かすべく、各マスに点数をつけ、自分の石の点数の和と相手の石の点数の和との差を評価関数にする方法が知られている。
隅のナナメのマス (X) には大きいマイナス点をつけ自分からは極力打たないようにする。隅の隣のマス (C) にも X ほど大きくはないマイナス点をつけて打ちにくくしておく。
また、その他の辺は取っていると有利なことが多いので少しだけプラス点を与えておく。
以上の考えで、例えば図のような隅辺Xのみの点数付けができる。
![]() |
| 図5 8x8盤での点数付け例 |
上図の点数付けによる評価関数を作り、8x8リバーシのコンピュータの強さを、(1) 自分が対戦して、(2) 石数だけの評価関数と対戦させて、調べてみなさい。
雛型プログラムの Evaluator.java ファイルのクラス Eval8x8B 内の TODO 部分を追加または修正すればよい。
自分で工夫した点数付けによる評価関数を作り、8x8リバーシのコンピュータの強さを、(1) 自分が対戦して、(2) 演習20の評価関数と対戦させて、調べてみなさい。
雛型プログラムの Evaluator.java ファイル内で二番目に TODO と書かれてある部分を修正すればよい。
※ 複数プログラムを同時起動してテストする方法
下にあるように、実行可能ファイル Reversi.jar を作って複数プログラムを同時起動してテストできる。例えば、2プレーヤ同士の対戦なら、10ゲームなら先手・後手を 5-5 になるようにして結果をみる。
実行可能jarファイルでテストする場合は、ソースを修正・保存する度に実行可能jarファイルを作り直す必要があるので注意する。
各マスに点数付けする評価関数でも人間相手なら中々負けないプログラムになる。しかし、強いコンピュータプログラムには到底かなわない。評価関数も上のような単純なものでなく、いくつもの要素を評価し総合して数値化するものにする必要がある。
考慮すべき要素としては例えば以下のようなものがある。
自分の着手が多くなると有利なのでプラス評価し、相手の着手が多くなると不利なのでマイナス評価する。
開放度とはある石の回りの空きマスの個数のこと。開放度が大きい石ほど相手に返される可能性が高いのでマイナス評価する。
辺に石が固まった状態 (山・ウイングなどと言われる) を色々なパターン別に点数付けして評価する。
確定石とは絶対返されなくなった石のことで自分の確定石数をプラス評価・相手の確定石数をマイナス評価する。正確に求めるのは難しいので近似値を求めてもよい。
リバーシのトッププログラムの評価関数は、上で示したような多数のパラメータを持っており、自動学習によってパラメータが調整されている。
講義では詳しく説明しきれないので省略するが、興味のある人は書籍やWebサイトなどで調べてみるとよい。またWebにはソースコード付きで公開されている強いプログラムも多くある。参考にしてみるとよい。
学籍番号でプログラムを識別できるようにする。
ComputerPlayer.java の以下の部分に自分の学籍番号を入れ書き直して保存する。
// 名前の自動決定
void setName() {
setName("$自分の学籍番号-" + searchDepth + "-" + emptyCountOnPerfectLookup
+ "-" + evalIndex);
}
同様に ReversiFrame.java の以下の部分に自分の学籍番号を入れ書き直して保存する。
static final String DEFAULT_HUMAN_NAMEBASE = "自分の学籍番号";
同様に Evaluator.java の以下の二箇所に自分の学籍番号等を入れ書き直して保存する。
クラス Eval8x8C の、javadoc コメント内
* @author 自分の学籍番号と氏名その下のコンストラクタ内
Eval8x8C() {
name = "自分の学籍番号";
}
自分のプログラムだけでは強さを高めるにも目標が設定しにくい。そこで、他のプログラムと対戦させ弱点を調べ改良していく手法がよく採られる。
他のプログラムとの対戦には、現在ではネットワークを使った通信を使う場合が多いが、相互のプログラムがネットワーク対応で同一通信プロトコルであることが条件で、ネットワーク対戦ができない場合もある。そこで今回は以下のように人手を介した対戦を行う。
できれば他の人のプログラムと対戦し合って互いに強化し合うとよい。
時間に余裕があれば、黒と白を入れ換えて何ゲームかやって勝敗を見た方がよい。
一台のパソコンで二つプログラムを起動し全部自分一人で対戦させることもできる。
対戦相手を変えてゲームの相手と勝敗を記録していき、適当なところで対戦の状況を分析し、必要ならばプログラムに変更を加えていく。変更したときはその内容を記録しておく。 二回目以降の変更では今までの変更点も参考にできる。
EclipseでReversiプロジェクトを開いた状態で、
で、
の中にある
を選択し、
をクリックする。
次の画面で、
と入力・設定した上で、
すれば、指定した場所に実行可能jarファイルが作成される。
(PowerShell の場合)
cd jarファイルのあるフォルダ
(コマンドプロンプト の場合)
cd /D jarファイルのあるフォルダ
※ 名前にスペースを含むフォルダがある場合は上記のフォルダ指定で
"jarファイルの ある フォルダ"
のように、フォルダ指定全体をダブルクォーテーションで囲むこと。
次に、以下のコマンドでjarファイルを実行する。
java -jar Reversi.jar
また、以下のように、先頭に start を付けたコマンドを何回も実行すれば、すきなだけ複数プログラムを起動できる。
start java -jar Reversi.jar
※ 課題11と課題12は履修者全員のための課題で、よくテスト・調整した上で、指定を守り提出のこと。
提出されていても、正常動作しない、あるいは、著しく弱いプレイをするプログラムの場合は、課題達成と認めない。
テキストエディタで、先頭に「課題番号 課題名」、「自分の学籍番号・氏名」を入れた後、Evaluator.java の評価関数 Eval8x8C に各自で調整を加え、できるだけ強いプログラムとした部分のソースファイル (Evaluator.java の各自が追加・修正した部分のみ) を入れ、テキストファイル (拡張子 txt) として保存し、授業Webページから提出。
Eclipse を使って生成した「実行可能jarファイル Reversi.jar」を提出する。
プログラムが正常に起動し、対戦開始から終了まで一通り正常動作することを確かめた上で提出すること。
コンピュータが著しく弱いプレイをするものは認めない。
EclipseでReversiプロジェクトを開いた状態で、
で、
の中にある
を選択し、
をクリックする。
次の画面では、
と入力・設定した上で、
すれば、指定した場所にZIPファイルが作成される。
Reversiのプロジェクトを別名のプロジェクトにコピーした上、課題11と課題12のプログラムにはない独自の工夫をしてコンピュータプレーヤを強化しなさい。Eclipse を使って独自強化版のプロジェクトをアーカイブしたZIPファイル ReversiSource.zip を作成し提出しなさい。