AI対戦サーバープログラム作成のヒント

Author:久住 涼

はじめに

まだ作成中ですよ

何年もAI対戦を見てきて思ったことと、CEDEC2013AIChallengeプロジェクトに参戦して便利だと思ったこと、調べたことをまとめたTipsです。
AI対戦のプログラム作成の役立ててやってください。

今までのサーバー―クライアント間通信の手法

過去のAI対戦では、サーバーとクライアントの情報のやりとりには、次のような手段が使われてきました。

「同じ実行ファイルにまとめる」とは、AIプログラムもサーバプログラム内に入れてコンパイルする手法のことです。
VC++やVisualStadioで言えば、同じプロジェクトファイル内にサーバプログラムとクライアントプログラムの両方を含めてコンパイルすることで、1つの実行ファイルとして出力、実行します。

「同じ実行ファイルにまとめる」では、サーバプログラムとクライアントプログラムを別々の.exeファイルで作成します。
データ通信は、サーバからクライアントへは実行時引数として渡し、クライアントからサーバへはプログラムの返り値を行動としてサーバプログラムに返す手法です。

これら手法はそれぞれ、プログラムの作成が楽であったり、既知の技術だけで実装できるなどの利点がありますが、以下のような欠点もあります。

既存手法の欠点

この項では、上にあげた2つの手法の欠点を上げたいと思います。

同じ実行ファイルにまとめる

この手法では、今までこの問題に苦しめられてきました。

サーバとクライアントを同じ実行ファイル内に入れてしまうため、無限ループの検出や実行エラーへの対処はクライアント側に依存していました。
そのために、苦汁を舐めた人もいました。
また、AIプログラムを書き換えるたびに全体をコンパイルしなおさなければならず、これは大変な手間となります。

実行時引数に情報を与える

この手法の欠点には、次が考えられると思います。

つまり、各ターンの行動を毎回決めなければならず、数ターン分の行動をまとめて決められないということです。
モンテカルロ法などのアルゴリズム(詳しくは調べてね)で得られた情報を捨てなければならないのは、クライアントにとって大きな損失です。

提案したい通信方法

そこで、これらの欠点を解消する情報の伝達方法として、次を提案します。

これは、サーバ(クライアント)の標準出力がクライアント(サーバ)への標準入力となる方法です。

これには、プロセス間通信について触れる必要がありますが、一度使いこなせれば、非常に心強い武器となります。
CEDEC2013AIChallengeプロジェクトや、ICPC2013でも使われていたものなので、汎用性は高いと思います。
ここから、プロセス間通信による情報転送のやり方について説明します。

プロセス間通信

ここからは、ある程度C言語を知っている前提で話を進めます。(実装言語はC(あるいはC++)と仮定します)
また、AI対戦のサーバはwindows上で開発するでしょうから、windowsでの実装の話になります。(Win APIを使用します)
LINUX系の場合はfork()やexec()などで簡単に代用できるしょうから、適宜調べて、置き換えながら読んでください。(余裕あればLINUX対応も執筆するかも)

プロセス間通信の初歩:子プロセスの作成

まずは、以下のプログラムを実行してみてください。なぜか電卓が起動します。

 #include <stdio.h>
 #include <windows.h>

 int execute(LPTSTR commandLine)
 {
   STARTUPINFO si = { sizeof(STARTUPINFO) };
   PROCESS_INFORMATION pi = {};
   if (!CreateProcess(
     NULL,
     commandLine,
     NULL,  //プロセスのセキュリティー記述子
     NULL,  //スレッドのセキュリティー記述子
     FALSE,  //ハンドルを継承しない
     0,  //作成フラグ
     NULL,  //環境変数は引き継ぐ
     NULL,  //カレントディレクトリーは同じ
     &si,
     &pi)
   ) {
     printError("CreateProcess");
     return -1;
   }
   // 子プロセス起動成功
   childProcess = pi.hProcess;

   // 不要なスレッドハンドルをクローズする
   if (!CloseHandle(pi.hThread)) {
     return -1;
   }

   printf("child processId=%d\n", pi.dwProcessId);

   // 子プロセスの終了待ち
   DWORD r = WaitForSingleObject(childProcess, INFINITE);
   switch(r) {
   case WAIT_FAILED:
     printError("wait result=WAIT_FAILED");
     return -1;
   case WAIT_ABANDONED:
     printf("wait result=WAIT_ABANDONED\n");
     return -1;
   case WAIT_OBJECT_0: //正常終了
     printf("wait result=WAIT_OBJECT_0\n");
     break;
   case WAIT_TIMEOUT:
     printf("wait result=WAIT_TIMEOUT\n");
     return -1;
   default:
     printf("wait result=%d\n", r);
     return -1;
   }

   // 子プロセスの終了コードを取得
   DWORD exitCode;
   if (!GetExitCodeProcess(childProcess, &exitCode)) {
     printError("GetExitCodeProcess");
     return -1;
   }
   printf("exitCode=%d/%x\n", exitCode, exitCode);

   return exitCode;
 }

 int main()
 {
   printf("main[%d] start\n", GetCurrentProcessId());

   LPTSTR commandLine = TEXT("calc.exe"); //電卓
   int r = execute(commandLine);

   // 子プロセスのハンドルのクローズ
   if (childProcess != NULL) {
     if(!CloseHandle(childProcess)) {
       printError("CloseHandle(childProcess)");
     }
   }

   printf("main[%d] end\n", GetCurrentProcessId());
   return r;
 }

このうち、

   LPTSTR commandLine = TEXT("calc.exe"); //電卓

の行の"calc.exe"を起動したいファイルの名前に変更すると、そのファイルが起動できるようになります。

now researching...

参考元

http://www.ne.jp/asahi/hishidama/home/tech/c/windows/CreateProcess.html


トップ   新規 一覧 検索 最終更新   ヘルプ   最終更新のRSS