Skip to content

Instantly share code, notes, and snippets.

@maraigue
Created March 7, 2016 13:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save maraigue/744992eeef8a36aba702 to your computer and use it in GitHub Desktop.
Save maraigue/744992eeef8a36aba702 to your computer and use it in GitHub Desktop.

この資料は、4目並べ https://github.com/sapporocpp/4moku のAIを作る上での、基本事項のまとめや、「~~するにはどんなコードを書けばよい?」といった事例集です。

動かす方法

ビルドはmakeSConsに対応しています。どちらかでもインストールされている環境であれば、まずそれを試してください。またVisual Studio用のプロジェクトファイルもあります。

生成された実行ファイルは、以下のコマンドで動かします。

4moku 幅 高さ AIの番号1 AIの番号2 AIの番号3 ...

同時に対戦させられるプレイヤーの数は10までです。まずは2つで動かすとよいでしょう。

使うAIを差し替えたい場合は、source/4moku.cppの冒頭でincludeされているファイルのファイル名を書き換えた上で、再度ビルドしてください。

AI作成における事例集

プログラムの構造

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>)を明示しなくても引数の型から自動的に判断してくれます。

基本的なAPI

  • 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);の部分がそう)

という手順になります。先読みするそれぞれの一手につき、盤面のコピーを行うことになります。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment