この資料は、4目並べ https://github.com/sapporocpp/4moku のAIを作る上での、基本事項のまとめや、「~~するにはどんなコードを書けばよい?」といった事例集です。
ビルドはmake
とSCons
に対応しています。どちらかでもインストールされている環境であれば、まずそれを試してください。またVisual Studio用のプロジェクトファイルもあります。
生成された実行ファイルは、以下のコマンドで動かします。
4moku 幅 高さ AIの番号1 AIの番号2 AIの番号3 ...
同時に対戦させられるプレイヤーの数は10までです。まずは2つで動かすとよいでしょう。
使うAIを差し替えたい場合は、source/4moku.cpp
の冒頭でincludeされているファイルのファイル名を書き換えた上で、再度ビルドしてください。
AIを定義しているファイル(source
ディレクトリ内の*.hpp
ファイル、ただし4moku.hpp
を除く)をご覧いただけるとわかるのですが、AIは以下の形の関数として定義されている必要があります。
std::tuple<int, int> AI_FUNCTION(const Board & board, int player);
返り値の型になっているtuple
はC++11で規格化されたクラスで、複数の値をひとまとめにしたデータを表現するのに用います。
参考:tuple - cpprefjp C++日本語リファレンス
このtuple
型の値を生成するための関数がstd::make_tuple(a, b)
です(AIのコード中でもたびたび使われています)。これがあると、tuple
のインスタンスを作るのに、クラス名(std::tuple<int, int>
)を明示しなくても引数の型から自動的に判断してくれます。
board(x, y)
- (x, y)の地点にある石を得る。石がなければ0を返す。
player_id(player)
- 現在のプレイヤーの石を取得する。
board(x, y)
の結果と比較したい場合はこれを使う。
- 現在のプレイヤーの石を取得する。
placeable(board, x, y)
- (x, y)の地点に石が置けるかを返す。
単純な例として、「ランダムな場所に置く」というものを考えます。
まず、乱数を生成しないとなりませんが、その前に盤面のサイズを知らなければ、適切な乱数を生成することもできません。それを取得するのが次のコードです。
int nx,ny;
std::tie(nx,ny) = board.size();
では、乱数を生成するのに必要な準備をします。ここではC++で規格化された乱数ライブラリを使います。
std::random_device rd;
std::mt19937 mt(rd());
std::uniform_int_distribution<> rndx(0, nx);
std::uniform_int_distribution<> rndy(0, ny);
rndx
は0以上nx未満の整数を、rndy
は0以上ny未満の整数を生成します。
続いて、乱数を生成し、その場所に石を置くことにします。
int x = rndx(mt), y = rndy(mt);
return std::make_tuple(x, y);
ところがこれでは、置けない場所(すでに石がある場所、あるいは置いても石が落ちていってしまう場所)に置いてしまう可能性があります。そこで、石を置ける場所なのかチェックを入れて、それが大丈夫だったときに初めて置くようにします。
int x = rndx(mt), y = rndy(mt);
if(placeable(board, x, y)){
return std::make_tuple(x, y);
}
またそのため、石を置けなかった場合には再度置く場所を乱数で選びたいので、ループの中に入れます。
for(int i=0;i<300;++i) {
int x = rndx(mt), y = rndy(mt);
if(placeable(board, x, y)){
return std::make_tuple(x, y);
}
}
これをまとめたものが test_ai.hpp
になります。
サンプルコード:test_ai.hpp
乱数の生成は、C言語にもあるrand
を使う方法のほか、C++11で規格化された乱数ライブラリを使う方法があります。
参考:random - cpprefjp C++日本語リファレンス
C++11の乱数は、「乱数の種(初期値を選ぶための値)」「乱数の生成方法」「それにより生成される分布(指定区間の整数とか、指定区間のdouble値とか)」のそれぞれについてクラスが定義されており、これらを選んで乱数を生成します。この例では、乱数の生成方法としてstd::mt19937
、それにより生成される分布としてstd::uniform_int_distribution
(指定された範囲の整数から無作為選択)を使っています。
サンプルコード:chain.hpp
, winning.hpp
自分の石を4つ並べればよいため、「なるべく自分の石が多く並ぶ」ようにするのは有効な戦術といえます。
この実装は力技に近くて、
- 各マス目を起点に、
- 4方向に見る向きを伸ばし(実際は8方向あるが、例えば「上への繋がりを見ていく」ことと「下への繋がりを見ていく」ことは片方だけ行えばよいため)、
- 何個繋がっているか調べる
というものになります。
サンプルコード:kakutei.hpp
手を先読みするには、
- 現在の盤面をコピーして(
Board board_tmp(board);
の部分がそう)、 - 実際に石を置く(
board_tmp(i, j) = player_id(player);
の部分がそう)
という手順になります。先読みするそれぞれの一手につき、盤面のコピーを行うことになります。