囲連星公式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 が囲連星用の AI であるかどうかをチェックするために、以下の関数が存在するか調べます。
同じ AI 同士を戦わせることができるので、作業用のワークエリアはそれぞれの手番用に確保しなければなりません。
ゲーム本体は、IrenseiInitialize() を呼び出すことで作業メモリの確保を促します。
AI は、この呼び出しに対して、確保したメモリのアドレスやハンドルをキャストして返します。
以後ゲーム本体は、このときの戻り値を第1引数にして AI との通信を行います。
IrenseiInitialize() で確保した作業メモリを開放します。
AI の思考部分です。
相手の前回の手が渡され、今回の自分の手を返します。
ゲーム本体は、現在の盤面の状況を AI に渡しません。AI は自身で現在の盤面を保持しなければなりません。
プレイヤーと AI との対戦の場合、「待った」できないのでは不親切ですが、AI 作者の手間がかかるために、この機能はオプションとなっています。
DLL にこの関数を定義しなければ、ゲーム本体は「待った」を無効にします。
この機能をサポートする AI は、最後に打たれた手を無かったこととして扱います。
文字列ログ表示の他に、盤面の認識情報をゲーム本体の盤面に表示することができます(図)。
盤面の状況は char [ 19/*y*/ ][ 19/*x*/ ] で表される配列に格納します。-128 が格納されている場合には、ゲーム本体は何も表示しません。
1種類だけではなく、何種類かの中から選ぶことができます(図)。例えば、打ちたい場所、守るべき場所、空間の有望さ、連や群の強弱など、あなたが作っている AI で表示したいものを選ぶことができます
この関数は、IrenseiThink() の直後に呼ばれます。
ゲーム本体は、ゲーム本体と同じフォルダ(サブフォルダは探索しません)にある、ai で始まる dll を検索します。
そのため作成したAIはファイル名がaiで始まるdllファイルである必要があります。
ゲーム本体は、全ての関数を名前で取り出して呼び出します。
C++ で作成する場合には、公開する関数を extern "C" として宣言してください。
シンボルの先頭にアンダースコア(_)などを自動的につける処理系では、これを抑制してください。コンパイラにより抑制できない場合は、*.DEF ファイルを書くことでリンク時に解決できます。
ゲーム本体で「新規開始」を選択すると、ゲーム本体は AI 用 DLL の探索を行います。
この時点から、ゲームが終了するまで DLL はロックされています。
AI の追加、交換は、プレイヤー選択ウィンドウが開いておらず、ゲーム中でもないときにのみ行えます。
Windows Platform SDK を使用します。インストールについては Microsoft 社の情報を確認してください。
IDE は使用せずに コマンドラインツールの cl を使用します。
cl はシンボルの先頭にアンダースコア(_)を付けないので、コマンドラインオプションに /LD をつけるだけで DLL を作成でき、非常に簡単に使えます。
ここでは、簡単な AI のサンプルとして、7連有効範囲内を左上から順に石を置くものを作成します。
※1 AI-API で提供されるヘッダです。これは AI を作成するために必ず必要です。ここでは、同じフォルダにコピーしてあることにします。#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; }
/LD は、DLL を作成するオプションです。cl /LD /Feailefttop.dll /IPSDKDIR\include ailefttop.cpp PSDKDIR\lib\uuid.lib
これを抑止するには、コマンドラインオプションに /wd4996 を追加してください。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
とりあえず、DLL インターフェース作成と同様に初手で投了するようにしておきます。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; } };
初期化と終了のときは、IrenseiPrintf() でログ出力すると、後々ログが分かりやすくなります。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) ); }
irensei_rule_description.c と irensei_ai_descripter.c は、AI-API の提供するファイルです。同じフォルダにコピーしておくか、パス付きで指定するかしてください。cl /LD /Feailefttop.dll /I\include ailefttop.cpp irensei_rule_description.c irensei_ai_descripter.c PSDKDIR\lib\uuid.lib
AI::Think() をクラス内で定義するのをやめたので、これを定義します。#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 初手ではなければ、相手の手を game に反映させます。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; }
早速コンパイルして試したいところですが、Irensei::Game は他のユーティリティーも使うため、cl に渡すファイルリストが長くなりすぎます。makefile を書きましょう。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) ) ); }
PSDK のパスは各自書き換えてください。# 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
定義は次の通りです。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) ); }
AI::Debug() の新しい定義を追加します。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; };
表示させるデバッグ情報を dbBoard に設定しましょう。int AI::Debug( IRENSEI_DEBUG_BOARD & dest, int idx, HANDLE hout ) { if ( idx != 0 ) return 0; memcpy( &dest, &dbBoard, sizeof(dest) ); return 1; }
メイクして試してみましょう。ただし、AI 同士で対戦させると速過ぎて分からないかもしれません。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; }
インストールについては、様々な情報が Web 上にありますので、そちらを参照してください。
Visual C++ Express Edition でのチュートリアルに書いた部分は省略しました。併せて参照してください。
bcc はシンボルの先頭にアンダースコア(_)をつけるので、それを回避する必要があります。
ここでは簡単な AI のサンプルとして、ランダムに打つものを作成します。
※1 AI-API で提供されるヘッダです。これは AI を作成するために必ず必要です。ここでは、同じフォルダにコピーしてあることにします。#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; }
-WD は、DLL を作成するオプションです。bcc32 -WD -eairandom.dll -u- aiirandom.c
としますが、この警告は抑制すべきではありません。未実装の標識としても使えますので、抑制するとしても一時的にしましょう。bcc32 -WD -eairandom.dll -u- -w-par aiirandom.c
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(); }
aiirandom.c のほうは、airandom.cpp で定義したアンダースコア付きの同名関数を呼び出しているだけです。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; }
bcc32 -c airandom.cpp bcc32 -WD -eairandom.dll -u- aiirandom.c airandom.obj
となります。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::Think() をクラス内で定義するのをやめたので、これを定義します。#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; }; };
※1 Position(x,y) のようにして簡便に2次元座標インスタンスを作るためのものです。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; }
早速コンパイルして試したいところですが、Irensei::Game は他のユーティリティーも使うため、bcc に渡すファイルリストが長くなりすぎます。makefile を書きましょう。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) ) ); }
それではメイクして、実行結果を確かめてください。# 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::Debug() の新しい定義を追加します。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; }; };
表示させるデバッグ情報を legalbuf と weightTable に設定しましょう。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; }
メイクして試してみましょう。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; }