囲連星 AI 作成チュートリアル

囲連星公式PCゲーム向けの AI 作成について、簡単に説明します。

概要

囲連星公式PCゲームは、Windows98 以降で動作するゲームソフトです。
囲連星公式PCゲームは、外部の AI を使用する仕組みを持っており、AI をユーザが作成することが可能です。
2つの AI を戦わせることも、同じ AI 同士を戦わせることもできます。
強い AI や斬新な AI を作ったら、他のユーザーに配布して使ってもらいましょう。

この文書は、C/C++ の言語の知識および Windows プログラミングの知識がある方を対象として書かれています。
ここでは、Microsoft Visual C++ ExpressEdition 及び Borland C++ Compiler 5.5.1 のそれぞれで AI を作成する方法を説明しています。
それぞれ別の内容となっているので、処理系に依存しない部分は両方見ていただければと思います。


目次

AI について
AI 用 DLL について
共通の知識
Visual C++ Express Edition でのチュートリアル
Borland C++ 5.5.1 でのチュートリアル
チュートリアルの後
参考資料

AI について

AI は、ゲーム本体と以下のやりとりします。

以上のやり取りは、DLL の関数呼び出しを介して行われます。

ゲーム本体とのやりとりは、全てゲーム側から AI に対しての呼び出しとして行われます。
呼び出す関数は以下の通りです。

先頭に * が付いているものはオプションです。

AI の素性や能力の通知

ゲーム本体は、その DLL が囲連星用の AI であるかどうかをチェックするために、以下の関数が存在するか調べます。

これらを全て持っていれば、ゲーム本体が扱えるバージョンかどうかを調べるため、IrenseiGetFirstVersion() と IrenseiGetLastVersion() を呼び出してルールのバージョンを調べます。
また、ユーザーに対して表示する名前を、IrenseiGetName() を呼び出すことで取得します。 IrenseiGetName( name ) の第1引数は、ゲーム本体が保持する文字列バッファへのアドレスです。この内容を書き換えます。

AI の初期化 IrenseiInitialize()

同じ AI 同士を戦わせることができるので、作業用のワークエリアはそれぞれの手番用に確保しなければなりません。
ゲーム本体は、IrenseiInitialize() を呼び出すことで作業メモリの確保を促します。
AI は、この呼び出しに対して、確保したメモリのアドレスやハンドルをキャストして返します。
以後ゲーム本体は、このときの戻り値を第1引数にして AI との通信を行います。

int IrenseiInitialize(const IRENSEI_RULE_DESCRIPTION * rule, int side, int hout)
戻り値
0 ならば失敗、それ以外は作業メモリの ID です。ゲーム本体は、この値を第1引数にして AI との通信を行います。
rule
囲連星のルール調整の余地のためにある構造体です。AI-API で提供される CheckIrenseiRuleDescriptionValidity() というユーティリティーでチェックした後、rule->version をチェックして、バージョン不整合があれば失敗を返してください。
side
黒番か白番かの情報です。1 が黒で、2 が白です。
hout
デバッグ表示のために、 Win32API の WriteFile() や AI-API で提供される IrenseiPrintf() に渡すハンドルです。

AI の破棄 IrenseiFinalize()

IrenseiInitialize() で確保した作業メモリを開放します。

int IrenseiFinalize(int param, int hout)
戻り値
失敗なら 0、成功なら 0 以外を返します。
param
IrenseiInitialize() が返した値です。ハンドルやポインタとして、キャストして使います。
hout
デバッグ表示のために、 Win32API の WriteFile() や AI-API で提供される IrenseiPrintf() に渡すハンドルです。

次の手を打つ IrenseiThink()

AI の思考部分です。
相手の前回の手が渡され、今回の自分の手を返します。
ゲーム本体は、現在の盤面の状況を AI に渡しません。AI は自身で現在の盤面を保持しなければなりません。

int IrenseiThink(int param, IRENSEI_BOARD_POSITION * result, const IRENSEI_BOARD_POSITION * lastMove, int hout)
戻り値
投了なら 0、それ以外では 0 以外を返します。
param
IrenseiInitialize() が返した値です。ハンドルやポインタとして、キャストして使います。
result
打つ場所を返します。result には、ゲーム本体の変数のアドレスが入っているのでこれを書き換えます。
合法手ではなかった場合、ゲーム本体は投了として扱います。
lastMove
相手の前回の打った場所です。初手の場合は 0 、それ以外の場合ではゲーム本体の変数のアドレスです。
hout
デバッグ表示のために、 Win32API の WriteFile() や AI-API で提供される IrenseiPrintf() に渡すハンドルです。

相手からの「待った」への対応 IrenseiUndo() (オプション)

プレイヤーと AI との対戦の場合、「待った」できないのでは不親切ですが、AI 作者の手間がかかるために、この機能はオプションとなっています。
DLL にこの関数を定義しなければ、ゲーム本体は「待った」を無効にします。
この機能をサポートする AI は、最後に打たれた手を無かったこととして扱います。

int IrenseiUndo( int param, IRENSEI_BOARD_POSITION * lastMove, int hout )
戻り値
失敗なら 0、成功なら 0 以外を返します。
param
IrenseiInitialize() が返した値です。ハンドルやポインタとして、キャストして使います。
lastMove
破棄した石の場所を返します。lastMove には、ゲーム本体の変数のアドレスが入っているのでこれを書き換えます。
ゲーム本体の認識している場所と違った場合は、投了として扱います。
hout
デバッグ表示のために、 Win32API の WriteFile() や AI-API で提供される IrenseiPrintf() に渡すハンドルです。

デバッグ表示(オプション)

文字列ログ表示の他に、盤面の認識情報をゲーム本体の盤面に表示することができます()。
盤面の状況は char [ 19/*y*/ ][ 19/*x*/ ] で表される配列に格納します。-128 が格納されている場合には、ゲーム本体は何も表示しません。
1種類だけではなく、何種類かの中から選ぶことができます()。例えば、打ちたい場所、守るべき場所、空間の有望さ、の強弱など、あなたが作っている AI で表示したいものを選ぶことができます
この関数は、IrenseiThink() の直後に呼ばれます。

int IrenseiDebug( int param, IRENSEI_DEBUG_BOARD * dest, int idx, int hout )
戻り値
失敗なら 0、成功なら 0 以外を返します。
失敗ならば、デバッグ情報を盤面に表示しません。
また、-128 のマスはゲーム本体の盤面に表示されません。これによって、不要な情報を減らして見た目をスッキリとさせることができます。
param
IrenseiInitialize() が返した値です。ハンドルやポインタとして、キャストして使います。
dest
盤面を表す2次元配列です。
ゲーム本体の変数のアドレスが入っているのでこれを書き換えます。
signed char なので -128 から 127 を格納することができますが、-128 はゲーム本体の盤面に表示されません。idx
種類の番号です。0 から IrenseiDebugCount() -1 までの値が入っています。
hout
デバッグ表示のために、 Win32API の WriteFile() や AI-API で提供される IrenseiPrintf() に渡すハンドルです。

int IrenseiDebugCount( void )
戻り値
盤面情報の種類の数を返します。
param
IrenseiInitialize() が返した値です。ハンドルやポインタとして、キャストして使います。
hout
デバッグ表示のために、 Win32API の WriteFile() や AI-API で提供される IrenseiPrintf() に渡すハンドルです。


AI 用 DLL について

DLL の命名と配置場所

ゲーム本体は、ゲーム本体と同じフォルダ(サブフォルダは探索しません)にある、ai で始まる dll を検索します。
そのため作成したAIはファイル名がaiで始まるdllファイルである必要があります。

エクスポート関数の命名について

ゲーム本体は、全ての関数を名前で取り出して呼び出します。
C++ で作成する場合には、公開する関数を extern "C" として宣言してください。
シンボルの先頭にアンダースコア(_)などを自動的につける処理系では、これを抑制してください。コンパイラにより抑制できない場合は、*.DEF ファイルを書くことでリンク時に解決できます。

寿命

ゲーム本体で「新規開始」を選択すると、ゲーム本体は AI 用 DLL の探索を行います。
この時点から、ゲームが終了するまで DLL はロックされています。
AI の追加、交換は、プレイヤー選択ウィンドウが開いておらず、ゲーム中でもないときにのみ行えます。


共通の知識

AI-API として、以下の構造体、クラス、関数がユーティリティーとして提供されます。
irensei_rule_decription.h: struct IRENSEI_RULE_DESCRIPTION
irensei_rule_decription.h: CheckIrenseiRuleDescriptionValidity()
irensei_ai_descripter.h: struct IRENSEI_BOARD_POSITION
irensei_ai_descripter.h: struct IRENSEI_DEBUG_BOARD
irensei_ai_descripter.h: IrenseiPrintf()
board.h: class Irensei::Board
StringData.h: struct Irensei::StringData
game.h: class Irensei::Game
これらは全て、ゲーム本体で使われているものと同じものです。
スコープ指定(Irensei::)されていないものは全て C 言語から使用可能ですが、他は C++ からのみ利用できます。

struct IRENSEI_RULE_DESCRIPTION

ゲーム本体から IrenseiInitialize() が呼ばれたとき、バージョンをチェックするために使用します。
事実上 version メンバしか使用しません。

CheckIrenseiRuleDescriptionValidity( const IRENSEI_RULE_DESCRIPTION * rule )

ゲーム本体から IrenseiInitialize() が呼ばれたとき、IRENSEI_RULE_DESCRIPTION の整合性を確認するために使用します。
この関数が 1 を返したら、あとは AI が version メンバをチェックするだけです。

struct IRENSEI_BOARD_POSITION

2次元座標です。

struct IRENSEI_DEBUG_BOARD

signed char の配列です。
AI のデバッグに使用します。

IrenseiPrintf( HANDLE hout, const char * fmt, ... )

AI が、デバッグログを表示するためのユーティリティーです。
hout は、AI の各関数が呼ばれたときに渡されます。
fmt 以降は、標準 C ライブラリの printf() と同じです。
のように、黒番、白番、それぞれのログがそれぞれのログウィンドウに表示されます。

class Irensei::Board

盤面を保持するクラスです。
劫や手番など、ゲームのコンテキストを一切保持しません。
Get(x,y) で、その座標に石があるか、あれば何色かを取得できます。
ChangeStone() は、盤面境界チェック付きのただの代入です。

struct Irensei::StringData

盤上のを表現する構造体です。
Remake( board ) で、盤上の全ての連を列挙して保持します。
特にアクセサは無く、メンバに自由にアクセスします。
pos2stringTable は、ある座標にある石が何番の連に属しているかを保持します。 strings は、全ての連を保持する std::vector です。
実行速度は速いとは言えません。

class Irensei::Game

盤面を持ち、合法手を判断します。
また、現在の手番や劫かどうかを覚えており、手の履歴を保持しています。
C++ で AI を作成する場合、このクラスのインスタンスを持つと簡単に作れます。但し、実行速度が速いとはいえないため、必要に応じて作り直すべきです。
CheckLegalMove(x,y) で、合法手の判定ができます。
IrenseiThink() が呼ばれたら、まず NewMove( lastMove->x, lastMove->y ) を呼びましょう。そして、打つ手を決定したら再び NewMove( x, y )を呼びましょう。
IrenseiUndo() が呼ばれたら、Undo() を呼びましょう。そして、lastMove に GetLastMove() の戻り値の x, y を代入しましょう。

Visual C++ Express Edition でのチュートリアル

Windows Platform SDK を使用します。インストールについては Microsoft 社の情報を確認してください。

IDE は使用せずに コマンドラインツールの cl を使用します。

cl はシンボルの先頭にアンダースコア(_)を付けないので、コマンドラインオプションに /LD をつけるだけで DLL を作成でき、非常に簡単に使えます。

ここでは、簡単な AI のサンプルとして、7連有効範囲内を左上から順に石を置くものを作成します。

サンプルの内容

7連有効範囲内を左上から順に石を置くものを作成します。
左上から順に調べて、最初に見つけた置ける場所に置きます。
調べた場所について合法だったかどうかをデバッグ表示に出力します。
ファイル名は ailefttop.dll です。
ゲーム本体に通知する名前は lefttop(cl sample) です。
ソースファイルは ailefttop.cpp 1つで作成します。

DLL インターフェース作成

まず、インターフェースとして、ゲーム本体から AI 用 DLL として認められるものを作成します。
ここでは、初手で投了することにします。
#include "irensei_rule_description.h"		/*※1*/
#include "irensei_ai_descripter.h"		/*※1*/

extern "C" __declspec(dllexport) int IrenseiGetName( char dest[IRENSEI_AI_NAME_LEN] )
{
	strcpy( dest, "lefttop(cl sample)" );	/*※2*/
	return 0;
}
extern "C" __declspec(dllexport) int IrenseiGetFirstVersion( void )
{
	return 100;
}
extern "C" __declspec(dllexport) int IrenseiGetLastVersion( void )
{
	return 100;
}

extern "C" __declspec(dllexport) int IrenseiInitialize(const IRENSEI_RULE_DESCRIPTION * rule, int side, int hout)
{
	return 1;				/*※3*/
}

extern "C" __declspec(dllexport) int IrenseiFinalize(int param, int hout)
{
	return 1;				/*※4*/
}

extern "C" __declspec(dllexport) int IrenseiThink"(int param, IRENSEI_BOARD_POSITION * result, const IRENSEI_BOARD_POSITION * lastMove, int hout)
{
	return 0;				/*※5*/
}

/*まだ定義しません
extern "C" __declspec(dllexport) int IrenseiUndo( int param, IRENSEI_BOARD_POSITION * lastMove, int hout )
*/

extern "C" __declspec(dllexport) int IrenseiDebug( int param, IRENSEI_DEBUG_BOARD * dest, int idx, int hout )
{
	return 0;
}

extern "C" __declspec(dllexport) int IrenseiDebugCount( void )
{
	return 0;
}
※1 AI-API で提供されるヘッダです。これは AI を作成するために必ず必要です。ここでは、同じフォルダにコピーしてあることにします。
※2 名前です。name はゲーム本体のバッファを表しており、ここに文字列をコピーします。
※3 すぐに投了するために全くバッファを必要としないので、成功を返します。
※4 何のメモリも確保していないので、何もせずに成功を返します。
※5 0 を返すと投了になります。

コンパイル

先ほどのソースをコンパイルします。
ここでは IDE ではなく、コマンドラインからコンパイルします。
cl /LD /Feailefttop.dll /IPSDKDIR\include ailefttop.cpp PSDKDIR\lib\uuid.lib
/LD は、DLL を作成するオプションです。
/Fe は、出力ファイル名を指定するオプションです。
/I は、#include プリプロセッサ指令が探しに行くパスを指定するオプションです。irensei_ai_descripter.h が windows.h を使用するためにこれが必要です。
PSDKDIR は、Windows Platform SDK をインストールしたパスです。ここはあなた自身で置き換えてください。

コンパイルすると以下の警告が出ます。
irensei_rule_description.c(25) : warning C4996: 'strncpy' が古い形式として宣言されました。
C:\Program Files\Microsoft Visual Studio 8\VC\INCLUDE\string.h(156) : 'strncpy' の宣言を確認してください。
メッセージ: 'This function or variable may be unsafe. Consider using strncpy_s instead. To disable deprecation, use _CRT_SECURE_NO_DEPRECATE. See online
    
これを抑止するには、コマンドラインオプションに /wd4996 を追加してください。
この警告は、バッファオーバーランなどの予期せぬバグについてより安全な、Safe C ライブラリを使用するべきだという指摘です。興味のあるかたは調べて使ってみると良いでしょう。

ゲーム本体で使ってみる

出来上がった ailefttop.dll を、ゲーム本体(irensei.exe)と同じフォルダにコピーしてください。
「新規開始」を選ぶと、プレイヤーリストに lefttop(cl sample) があるのが分かると思います。
それでは黒番を人間、白番を lefttop(cl sample) にして試して見ましょう。
正しく動きましたか?

AI class を作る

簡単に説明するために、C++ を用います。
class AI {
public:
	AI()
	{
	}

	int Think( IRENSEI_BOARD_POSITION & result, const IRENSEI_BOARD_POSITION * lastMove, HANDLE hout )
	{
		return 0;		/* 投了 */
	}

	int Debug( IRENSEI_DEBUG_BOARD & dest, int idx, HANDLE hout )
	{
		return 0;
	}
	static int CountDebug( void )
	{
		return 0;
	}

};
とりあえず、DLL インターフェース作成と同様に初手で投了するようにしておきます。
それではDLL インターフェース作成で作った C の関数から呼び出すようにしてみましょう。以下の関数を変更してください。
extern "C" __declspec(dllexport) int IrenseiInitialize(const IRENSEI_RULE_DESCRIPTION * rule, int side, int hout)
{
	if ( ! CheckIrenseiRuleDescriptionValidity( rule ) )
		return 0;
	if ( rule->version > 100 )
		return 0;

	IrenseiPrintf( reinterpret_cast<HANDLE>(hout), "lefttop(cl) AI initialized.\n" );
	return reinterpret_cast<int>( new AI() );
}

extern "C" __declspec(dllexport) int IrenseiFinalize(int param, int hout)
{
	delete reinterpret_cast<AI*>( param );
	IrenseiPrintf( reinterpret_cast<HANDLE>(hout), "lefttop(cl) AI finalized.\n" );
	return 1;
}

extern "C" __declspec(dllexport) int IrenseiThink(int param, IRENSEI_BOARD_POSITION * result, const IRENSEI_BOARD_POSITION * lastMove, int hout)
{
	return reinterpret_cast<AI*>( param )->Think( *result, lastMove, reinterpret_cast<HANDLE>(hout) );
}
初期化と終了のときは、IrenseiPrintf() でログ出力すると、後々ログが分かりやすくなります。
さきほどは使っていなかった AI-API の関数(CheckIrenseiRuleDescriptionValidityIrenseiPrintf)を使うので、コンパイルするときは必要なファイルを追加して指定します。
cl /LD /Feailefttop.dll /I\include ailefttop.cpp irensei_rule_description.c irensei_ai_descripter.c PSDKDIR\lib\uuid.lib
irensei_rule_description.c と irensei_ai_descripter.c は、AI-API の提供するファイルです。同じフォルダにコピーしておくか、パス付きで指定するかしてください。
コンパイルして、いままでと動作が同じことを確認してください。

思考ルーチンを作る

簡単に作るために、AI-API のユーティリティークラス Irensei::Game を使います。
Irensei::Game::CheckLegalMove(x,y,side) を使うことで、合法手の判定を簡単に行うことができます。
クラス宣言を以下のように変更しましょう。また、game.h をインクルードしましょう。変更は赤字にしてあります。
#include "game.h"

using namespace Irensei;

class AI {
public:
	AI( Game::EBlackOrWhite side )
		: myside(side)
	{
	}

	int Think( IRENSEI_BOARD_POSITION & result, const IRENSEI_BOARD_POSITION * lastMove, HANDLE hout );

	int Debug( IRENSEI_DEBUG_BOARD & dest, int idx, HANDLE hout )
	{
		return 0;
	}
	static int CountDebug( void )
	{
		return 0;
	}

private:
	
	Game game;
	Game::EBlackOrWhite myside;
	
};
AI::Think() をクラス内で定義するのをやめたので、これを定義します。
int AI::Think( IRENSEI_BOARD_POSITION & result, const IRENSEI_BOARD_POSITION * const lastMove, HANDLE hout )
{
	/* ※1 */
	if ( lastMove )
		game.NewMove( lastMove->x, lastMove->y );

	for ( int y = 2; y < 19-2; y++ ) {
		for ( int x = 2; x < 19-2; x++ ) {
			/* ※2 */
			if ( Game::EMResultWin >= game.CheckLegalMove( game.GetBoard(), x, y, myside ) ) {
				result.x = x;
				result.y = y;
				game.NewMove( x, y );
				return 1;
			}
		}
	}

	// 投了
	return 0;
}
※1 初手ではなければ、相手の手を game に反映させます。
※2 その座標が合法手ならば、result に結果をコピーして成功を返します。また、game に反映させます。

AI のコンストラクタ引数が変更されたので、IrenseiInitialize() を変更します。
extern "C" __declspec(dllexport) int IrenseiInitialize(const IRENSEI_RULE_DESCRIPTION * rule, int side, int hout)
{
	if ( ! CheckIrenseiRuleDescriptionValidity( rule ) )
		return 0;

	return reinterpret_cast<int>( new AI( static_cast<Game::EBlackOrWhite>(side) ) );
}
早速コンパイルして試したいところですが、Irensei::Game は他のユーティリティーも使うため、cl に渡すファイルリストが長くなりすぎます。makefile を書きましょう。

makefile を書く

make や makefile についてご存知で無い方は、各自調べてください。
makefile というファイルに以下の記述をし、コマンドラインから nmake と打てばコンパイルできるようになります。
# for nmake & cl(Visual C++)

PSDK = "C:\PROGRA~1\Microsoft Platform SDK"

CC = cl
CFLAGS = /c /wd4996 /EHsc /I$(PSDK)\Include

.cpp.obj:
	$(CC) $(CFLAGS) $<
.c.obj:
	$(CC) $(CFLAGS) $<

OBJS = ailefttop.obj game.obj StringData.obj \
	irensei_rule_description.obj irensei_ai_descripter.obj


ailefttop.dll: $(OBJS)
	cl /LD /Fe$< $** $(PSDK)\lib\uuid.lib
    
PSDK のパスは各自書き換えてください。
それではメイクして、実行結果を確かめてください。
AI と対戦して、AI が劫のところへ打ってこないことを確認しましょう。AI 同士の対戦もさせてみましょう。

「待った」の実装

Irensei::GameUndo() を持っているので、これを利用して「待った」実装します。
まず、class AI の public 部にに以下の宣言を加えます。
	int Undo( IRENSEI_BOARD_POSITION & lastMove, HANDLE hout );
    
定義は次の通りです。
int AI::Undo( IRENSEI_BOARD_POSITION & lastMove, HANDLE hout )
{
	game.Undo();

	const Game::Move lm = game.GetLastMove();
	lastMove.x = lm.x;
	lastMove.y = lm.y;

	return 1;
}
    
そしてインターフェース部分に以下を追加します。
extern "C" __declspec(dllexport) int IrenseiUndo( int param, IRENSEI_BOARD_POSITION * lastMove, int hout )
{
	return reinterpret_cast<AI*>( param )->Undo( *lastMove, reinterpret_cast<HANDLE>(hout) );
}
    
メイクして試してみましょう。

デバッグ出力

IrenseiPrintf() によるログ出力だけではなく、盤面を AI がどのように認識しているかを盤面に表示させることができます。
class AI の宣言を以下のように修正します。
class AI {
public:
	AI( Game::EBlackOrWhite side )
		: myside(side)
	{
	}

	int Think( IRENSEI_BOARD_POSITION & result, const IRENSEI_BOARD_POSITION * lastMove, HANDLE hout );

	int Undo( IRENSEI_BOARD_POSITION & lastMove, HANDLE hout );

	int Debug( IRENSEI_DEBUG_BOARD & dest, int idx, HANDLE hout );
	static int CountDebug( void )
	{
		return 1;
	}

private:
	Game game;
	Game::EBlackOrWhite myside;
	
	IRENSEI_DEBUG_BOARD dbBoard;
};
AI::Debug() の新しい定義を追加します。
int AI::Debug( IRENSEI_DEBUG_BOARD & dest, int idx, HANDLE hout )
{
	if ( idx != 0 )
		return 0;

	memcpy( &dest, &dbBoard, sizeof(dest) );
	return 1;
}
    
表示させるデバッグ情報を dbBoard に設定しましょう。
ここでは、AI::Think() での途中経過を保持させますが、AI::Debug() が呼ばれたときに再計算して設定しても構いません。
int AI::Think( IRENSEI_BOARD_POSITION & result, const IRENSEI_BOARD_POSITION * const lastMove, HANDLE hout )
{
	if ( lastMove )
		game.NewMove( lastMove->x, lastMove->y );

	memset( dbBoard.stones, 128, sizeof(dbBoard.stones) );
	for ( int y = 2; y < 19-2; y++ ) {
		for ( int x = 2; x < 19-2; x++ ) {
			Game::EMoveResult legality = game.CheckLegalMove( game.GetBoard(), x, y, myside );
			dbBoard.stones[x][y] = legality;
			if ( Game::EMResultWin >= legality  {
				result.x = x;
				result.y = y;
				game.NewMove( x, y );
				return 1;
			}
		}
	}

	// 投了
	return 0;
}
    
メイクして試してみましょう。ただし、AI 同士で対戦させると速過ぎて分からないかもしれません。

Borland C++ Compiler 5.5.1 でのチュートリアル

インストールについては、様々な情報が Web 上にありますので、そちらを参照してください。

Visual C++ Express Edition でのチュートリアルに書いた部分は省略しました。併せて参照してください。

bcc はシンボルの先頭にアンダースコア(_)をつけるので、それを回避する必要があります。

ここでは簡単な AI のサンプルとして、ランダムに打つものを作成します。

サンプルの内容

盤上にランダムに石を置くものを作成します。
盤上全ての地点を調べて合法手を集め、その集合からランダムな場所に置きます。
「待った」には対応しません。「待った」の実装を参考にしてください。
合法手一覧をデバッグ表示に出力します。
ファイル名は airandom.dll です。
ゲーム本体に通知する名前は random(bcc sample) です。
ソースファイルは airandom.cpp と aiirandom.c の2つで作成します。

DLL インターフェース作成

まず、インターフェースとして、ゲーム本体から AI 用 DLL として認められるものを作成します。
ここでは、初手で投了することにします。
#include "irensei_rule_description.h"		/*※1*/
#include "irensei_ai_descripter.h"		/*※1*/

extern "C" __declspec(dllexport) int IrenseiGetName( char dest[IRENSEI_AI_NAME_LEN] )
{
	_strcpy( dest, "random(bcc sample)" );	/*※2*/
	return 0;
}

extern "C" __declspec(dllexport) int IrenseiGetFirstVersion( void )
{
	return 100;
}

extern "C" __declspec(dllexport) int IrenseiGetLastVersion( void )
{
	return 100;
}

extern "C" __declspec(dllexport) int IrenseiInitialize(const IRENSEI_RULE_DESCRIPTION * rule, int side, int hout)
{
	return 1;				/*※3*/
}

extern "C" __declspec(dllexport) int IrenseiFinalize(int param, int hout)
{
	return 1;				/*※4*/
}

extern "C" __declspec(dllexport) int IrenseiThink(int param, IRENSEI_BOARD_POSITION * result, const IRENSEI_BOARD_POSITION * lastMove, int hout)
{
	return 0;				/*※5*/
}

extern "C" __declspec(dllexport) int IrenseiDebug( int param, IRENSEI_DEBUG_BOARD * dest, int idx, int hout )
{
	return 0;
}

extern "C" __declspec(dllexport) int IrenseiDebugCount( void )
{
	return 0;
}
※1 AI-API で提供されるヘッダです。これは AI を作成するために必ず必要です。ここでは、同じフォルダにコピーしてあることにします。
※2 名前です。name はゲーム本体のバッファを表しており、ここに文字列をコピーします。strcpy() が _strcpy() になっているのは、bcc がシンボル名にアンダースコア(_)を付加するのを抑制するようにしたときの不具合を回避するためです。
※3 すぐに投了するために全くバッファを必要としないので、成功を返します。
※4 何のメモリも確保していないので、何もせずに成功を返します。
※5 0 を返すと投了になります。

コンパイル

先ほどのソースをコンパイルします。
bcc32 -WD -eairandom.dll -u- aiirandom.c
-WD は、DLL を作成するオプションです。
-e は、出力ファイル名を指定するオプションです。
-u- は、リンカシンボルにアンダースコア(_)を付けるのを抑制させるオプションです。

コンパイルするとたくさん警告が出ますが、1つを除いて未使用引数に関するものです。抑制したい場合は、
bcc32 -WD -eairandom.dll -u- -w-par aiirandom.c
としますが、この警告は抑制すべきではありません。未実装の標識としても使えますので、抑制するとしても一時的にしましょう。
もう1つの警告は、プロトタイプ宣言の無い関数呼び出しに関するものです。後ほど対処しますので、ここでは我慢して無視してください。

ゲーム本体で使ってみる

出来上がった airandom.dll を、ゲーム本体(irensei.exe)と同じフォルダにコピーしてください。
「新規開始」を選ぶと、プレイヤーリストに random(bcc sample) があるのが分かると思います。
それでは黒番を人間、白番を random(bcc sample) にして試して見ましょう。
正しく動きましたか?

bcc がリンカシンボルにアンダースコア(_)を付けることによる問題とその回避

bcc はリンカシンボルの先頭にアンダースコア(_)をつける動作をデフォルトとしています。
例えば IrenseiGetName は _IrenseiGetName になってしまいます。
これではゲーム本体が、望む関数を見つけられないため、DLL インターフェース作成で説明したように、-u- オプションでこの問題を回避します。
しかし、これによって別の問題が発生します。
aiirandom.c: IrenseiGetName() で使用している、_strcpy() のように、-u- なしでコンパイルしたものに含まれる全て(例えば bcc についてくるライブラリ)のシンボルにアンダースコア(_)をつけて使用しなければなりません。
これを回避するため、-u- を使うのは aiirandom.c だけとします。
aiirandom.c には、DLL が必要とする定義のみを置き、実体は airandom.cpp に置くことにします。aiirandom.c からは、airandom.cpp にある実体を呼び出します。それぞれ以下のようになります。
aiirandom.c
#include "irensei_rule_description.h"
#include "irensei_ai_descripter.h"

/* aiirandom.c にあるもの以外は全て先頭にアンダースコア(_)をつけます。
   これをヘッダで宣言するのは、airandom.c にある実体とシンボルが一致しないので意味ありません */
extern int _IrenseiGetName( char dest[IRENSEI_AI_NAME_LEN] );
extern int _IrenseiGetFirstVersion( void );
extern int _IrenseiGetLastVersion( void );
extern int _IrenseiInitialize(const IRENSEI_RULE_DESCRIPTION * rule, int side, int hout);
extern int _IrenseiFinalize(int param, int hout);
extern int _IrenseiThink(int param, IRENSEI_BOARD_POSITION * result, const IRENSEI_BOARD_POSITION * lastMove, int hout);
extern int _IrenseiUndo( int param, IRENSEI_BOARD_POSITION * lastMove, int hout );
extern int _IrenseiDebug( int param, IRENSEI_DEBUG_BOARD * dest, int idx, int hout );
extern int _IrenseiDebugCount( void );

/* airandom.cpp にある実体を呼び出します。 */
__declspec(dllexport) int IrenseiGetName( char dest[IRENSEI_AI_NAME_LEN] )	{ return _IrenseiGetName( dest ); }
__declspec(dllexport) int IrenseiGetFirstVersion( void )			{ return _IrenseiGetFirstVersion(); }
__declspec(dllexport) int IrenseiGetLastVersion( void )				{ return _IrenseiGetLastVersion(); }
__declspec(dllexport) int IrenseiInitialize(const IRENSEI_RULE_DESCRIPTION * rule, int side, int hout)
	{ return _IrenseiInitialize(rule, side, hout); }
__declspec(dllexport) int IrenseiFinalize(int param, int hout)			{ return _IrenseiFinalize(param, hout); }
__declspec(dllexport) int IrenseiThink(int param, IRENSEI_BOARD_POSITION * result, const IRENSEI_BOARD_POSITION * lastMove, int hout)
	{ return _IrenseiThink(param, result, lastMove, hout); }
__declspec(dllexport) int IrenseiDebug( int param, IRENSEI_DEBUG_BOARD * dest, int idx, int hout )
	{ return _IrenseiDebug( param, dest, idx, hout ); }
__declspec(dllexport) int IrenseiDebugCount( void )				{ return _IrenseiDebugCount(); }
    
airandom.cpp
#include "irensei_rule_description.h"
#include "irensei_ai_descripter.h"

extern "C" __declspec(dllexport) int IrenseiGetName( char dest[IRENSEI_AI_NAME_LEN] )
{
	strcpy( dest, "random(bcc sample)" );
	return 0;
}

extern "C" __declspec(dllexport) int IrenseiGetFirstVersion( void )	
{
	return 100;
}

extern "C" __declspec(dllexport) int IrenseiGetLastVersion( void )
{
	return 100;
}

extern "C" __declspec(dllexport) int IrenseiInitialize(const IRENSEI_RULE_DESCRIPTION * rule, int side, int hout)
{
	return 1;
}

extern "C" __declspec(dllexport) int IrenseiFinalize(int param, int hout)
{
	return 1;
}

extern "C" __declspec(dllexport) int IrenseiThink(int param, IRENSEI_BOARD_POSITION * result, const IRENSEI_BOARD_POSITION * lastMove, int hout)
{
	return 0;
}

extern "C" __declspec(dllexport) int IrenseiDebug( int param, IRENSEI_DEBUG_BOARD * dest, int idx, int hout )
{
	return 0;
}

extern "C" __declspec(dllexport) int IrenseiDebugCount( void )
{
	return 0;
}
    
aiirandom.c のほうは、airandom.cpp で定義したアンダースコア付きの同名関数を呼び出しているだけです。
airandom.cpp のほうは、アンダースコアを削除し、C++ になったので extern "C" をつけただけです。
以後 aiirandom.c を変更するのは「待った」に対応するときだけです(このチュートリアルでは行いません)。

airandom.cpp は、「Visual C++ Express Edition でのチュートリアル」の 「DLL インターフェース作成」のソースとほとんど同じものになっています。実際、ここで解決した問題に対処すれば、「Visual C++ Express Edition でのチュートリアル」で作ったソースも使えますし、ここで作るソースも Visual C++ Express Edition でビルドすることができます。

コンパイルは2回に分けて行います。
bcc32 -c airandom.cpp
bcc32 -WD -eairandom.dll -u- aiirandom.c airandom.obj

AI class を作る

この項目は、プログラミングの部分に関してはAI class を作ると全く一緒なのでこちらを参照してください。
コンパイルのためのコマンドラインは、
bcc32 -c airandom.cpp irensei_rule_description.c irensei_ai_descripter.c
bcc32 -WD -eairandom.dll -u- aiirandom.c airandom.obj irensei_rule_description.obj irensei_ai_descripter.obj
となります。

思考ルーチンを作る

簡単に作るために、AI-API のユーティリティークラス Irensei::Game を使います。
Irensei::Game::CheckLegalMove(x,y,side) を使うことで、合法手の判定を簡単に行うことができます。
クラス宣言を以下のように変更しましょう。また、game.h をインクルードしましょう。変更は赤字にしてあります。
#include <vector>
#include <algorithm>
#include "game.h"

using namespace Irensei;

class AI {
public:
	AI( Game::EBlackOrWhite side )
		: myside(side)
	{
	}

	int Think( IRENSEI_BOARD_POSITION & result, const IRENSEI_BOARD_POSITION * lastMove, HANDLE hout );

	int Debug( IRENSEI_DEBUG_BOARD & dest, int idx, HANDLE hout )
	{
		return 0;
	}
	static int CountDebug( void )
	{
		return 0;
	}

private:
	Game game;
	Game::EBlackOrWhite myside;

	/* ※1 */
	struct Position {
		Position()	{}
		Position( int a, int b ) : x(a), y(b)	{}
		Position(const Position&o) : x(o.x), y(o.y)	{}
		int x;
		int y;
	};
};
AI::Think() をクラス内で定義するのをやめたので、これを定義します。
int AI::Think( IRENSEI_BOARD_POSITION & result, const IRENSEI_BOARD_POSITION * const lastMove, HANDLE hout )
{
	/* ※2 */
	if ( lastMove )
		game.NewMove( lastMove->x, lastMove->y );

	// 合法手の列挙
	std::vector< Position > legalMoves;	/* ※3 */
	legalMoves.reserve( 19*19 );
	for ( int y = 0; 19 > y; y++ ) {
		for ( int x = 0; 19 > x; x++ ) {
			/* ※3 */
			if ( Game::EMResultWin <= game.CheckLegalMove( game.GetBoard(), x, y, myside ) )
				legalMoves.push_back( Position(x,y) );
		}
	}

	// 合法手が無ければ投了
	if ( legalMoves.size() == 0 )
		return 0;

	// ランダムで選ぶ
	/* ※4 */
	{
		// なるべく中央に近い手を打つようにする
		std::vector< int > cofs;	// 重みの累積
		int total = 0;
		for ( std::vector<Position>::const_iterator itor = legalMoves.begin(); itor != legalMoves.end(); itor++ ) {
			int cof = abs(itor->x - 9) + abs(itor->y - 9);
			cof = 20 - cof;		// 2..20 ここで、中央が大きくなる。また、端も 0 にはならない
			cof = cof * cof;	// 2乗して、係数を大きく
			total += cof;
			cofs.push_back(total);
		}
		int r = total * rand() / (RAND_MAX + 1);
		std::vector< int >::const_iterator i = std::lower_bound( cofs.begin(), cofs.end(), r );
		*reinterpret_cast<Position*>(&result) = legalMoves[ i - cofs.begin() ];
	}

	// その手を打つ
	game.NewMove( result.x, result.y );

	return 1;
}
※1 Position(x,y) のようにして簡便に2次元座標インスタンスを作るためのものです。
※2 初手ではなければ、相手の手を game に反映させます。
※3 その座標が合法手ならば、合法手集合に追加します。
※4 それぞれの合法手に対する { 0, 2, 5, 9 ... } のような集合を作り、最後の値まで(最後の値を含まず)の乱数を取ると、間隔の広い項目を取りやすくなります。

AI のコンストラクタ引数が変更されたので、IrenseiInitialize() を変更します。
extern "C" __declspec(dllexport) int IrenseiInitialize(const IRENSEI_RULE_DESCRIPTION * rule, int side, int hout)
{
	if ( ! CheckIrenseiRuleDescriptionValidity( rule ) )
		return 0;
	if ( rule->version > 100 )
		return 0;

	IrenseiPrintf( reinterpret_cast<HANDLE>(hout), "random(bcc) AI initialized.\n" );
	return reinterpret_cast<int>( new AI( static_cast<Game::EBlackOrWhite>(side) ) );
}
早速コンパイルして試したいところですが、Irensei::Game は他のユーティリティーも使うため、bcc に渡すファイルリストが長くなりすぎます。makefile を書きましょう。

makefile を書く

make や makefile についてご存知で無い方は、各自調べてください。
makefile というファイルに以下の記述をし、コマンドラインから make と打てばコンパイルできるようになります。
# for bcc

CC = bcc32
CFLAGS = -c

OBJS = aiirandom.obj airandom.obj game.obj StringData.obj \
	irensei_rule_description.obj irensei_ai_descripter.obj

.cpp.obj:
	$(CC) $(CFLAGS) $<
.c.obj:
	$(CC) $(CFLAGS) $<

airandom.dll: $(OBJS)
	bcc32 -WD -e$< $**

aiirandom.obj: aiirandom.c
	$(CC) $(CFLAGS) -u- $**

game.obj: game.cpp
StringData.obj: StringData.cpp
irensei_ai_descripter.obj: irensei_ai_descripter.c
irensei_rule_description.obj: irensei_rule_description.c
    
それではメイクして、実行結果を確かめてください。
AI 同士の対戦もさせてみましょう。

デバッグ出力

IrenseiPrintf() によるログ出力だけではなく、盤面を AI がどのように認識しているかを盤面に表示させることができます。
ここでは、ある位置が合法手かどうかと、乱数を取るときの重みを表示してみましょう
class AI の宣言を以下のように修正します。
class AI {
public:
	AI( Game::EBlackOrWhite side )
		: myside(side)
	{
	}

	int Think( IRENSEI_BOARD_POSITION & result, const IRENSEI_BOARD_POSITION * lastMove, HANDLE hout );

	int Debug( IRENSEI_DEBUG_BOARD & dest, int idx, HANDLE hout );
	static int CountDebug( void )
	{
		return 2;
	}

private:
	Game game;
	Game::EBlackOrWhite myside;

	IRENSEI_DEBUG_BOARD legalbuf;
	IRENSEI_DEBUG_BOARD weightTable;

	struct Position {
		Position()	{}
		Position( int a, int b ) : x(a), y(b)	{}
		Position(const Position&o) : x(o.x), y(o.y)	{}
		int x;
		int y;
	};
};
AI::Debug() の新しい定義を追加します。
iint AI::Debug( IRENSEI_DEBUG_BOARD & dest, int idx, HANDLE /*hout*/ )
{
	switch ( idx ) {
		case 0:
			memcpy( &dest, &legalbuf, sizeof(dest) );
			break;
		case 1:
			memcpy( &dest, &weightTable, sizeof(dest) );
			break;
		default:
			return 0;
	}
	return 1;
}
表示させるデバッグ情報を legalbuf と weightTable に設定しましょう。
ここでは、AI::Think() での途中経過を保持させますが、AI::Debug() が呼ばれたときに再計算して設定しても構いません。
int AI::Think( IRENSEI_BOARD_POSITION & result, const IRENSEI_BOARD_POSITION * const lastMove, HANDLE hout )
{
	if ( lastMove )
		game.NewMove( lastMove->x, lastMove->y );

	// 合法手の列挙
	std::vector< Position > legalMoves;	/* ※3 */
	legalMoves.reserve( 19*19 );
	for ( int y = 0; 19 > y; y++ ) {
		for ( int x = 0; 19 > x; x++ ) {
			legalbuf.stones[y][x] = game.CheckLegalMove( game.GetBoard(), x, y, myside );
			if ( legalbuf.stones[y][x] <= Game::EMResultWin )		// 合法手?
				legalMoves.push_back( Position(x,y) );
		}
	}

	// 合法手が無ければ投了
	if ( legalMoves.size() == 0 )
		return 0;

	// ランダムで選ぶ
	memset( weightTable.stones, -128, sizeof(weightTable.stones) );
	{
		// なるべく中央に近い手を打つようにする
		std::vector< int > cofs;	// 重みの累積
		int total = 0;
		for ( std::vector<Position>::const_iterator itor = legalMoves.begin(); itor != legalMoves.end(); itor++ ) {
			int cof = abs(itor->x - 9) + abs(itor->y - 9);
			cof = 20 - cof;		// 2..20 ここで、中央が大きくなる。また、端も 0 にはならない
			weightTable.stones[ itor->y ][ itor->x ] = cof;
			cof = cof * cof;	// 2乗して、係数を大きく
			total += cof;
			cofs.push_back(total);
		}
		int r = total * rand() / (RAND_MAX + 1);
		std::vector< int >::const_iterator i = std::lower_bound( cofs.begin(), cofs.end(), r );
		*reinterpret_cast<Position*>(&result) = legalMoves[ i - cofs.begin() ];
	}

	// その手を打つ
	game.NewMove( result.x, result.y );

	return 1;
}
メイクして試してみましょう。


チュートリアルの後

さて、これで一通り、AI として動くための説明が終わりました。
ご承知のように、ここで作ったものは AI と呼ぶには恥ずかしいものです。誰とやってもあっという間に負かされてしまいます。
どうしたら強い AI を作ることができるのでしょうか?
まずは、自分と相手の連長を数えましょう。相手の連長が長くなったら、止めに行きましょう。自分の連長を伸ばせる場所に打ちましょう。
石の死活を強くしましょう。これは、既存の囲碁プログラムの知識が役に立つでしょう。
そして、囲連星は囲碁と比べてもまだまだ研究の余地のあるゲームです。囲連星の研究を深め、より強い AI を作りましょう。

参考資料

コンピュータ囲碁の入門 コンピュータ囲碁フォーラム編 共立出版 ISBN4-320-12150-3
ゲームプログラミング 松原仁・竹内郁雄編 共立出版 ISBN4-320-02898-8
コンピュータ囲碁フォーラム http://www.computer-go.jp/indexj.html
囲連星 ★囲碁でも連珠でもない無料ゲーム★ http://irensei.com/

コンピュータ囲碁用語で、string の訳です。
つながった石を1つの単位として扱うもので、死活の基本単位です。
コンピュータ囲碁用語で、group の訳です。
連が集まったものです。参考資料を参照してください。