大阪市中央区 システムソフトウェア開発会社

営業時間:平日09:15〜18:15
MENU

石取りゲームで遺伝的アルゴリズムの実験 その3

著者:北本 敦
公開日:2021/03/24
最終更新日:2021/04/07
カテゴリー:技術情報

北本です。

前回は、石取りゲーム部分のコードを紹介しました。今回は、そのコードの中で呼ばれていた自作の関数の中身を紹介します。

まず、SetActPattern関数です。
前回にも記載した通り、取得済みの石の数が(添数)個の時にターンが回ってきたら何個石を取るかの情報をランダムに引数arrayとして渡した配列に設定します。相手の行動パターンの設定や第1世代の個体の遺伝子パラメータのランダム設定に使っています。コード中のMAX_TAKE_PER_TURNは、1ターンあたりに取れる最大の石数を示す定数で3と定義しています。

static void SetActPattern(BYTE* array)
{
  for (int i = 0; i < NUMBER_OF_STONES; i++)
  {
    int max = MAX_TAKE_PER_TURN;
    if (NUMBER_OF_STONES - i < MAX_TAKE_PER_TURN)
      max = NUMBER_OF_STONES - i;
    array[i] = rand() % max + 1;
  }
}

とりわけ複雑なことはやっていません。
引数で渡された配列の各要素に1~3の数値をランダムに設定しています。ただし、存在する個数を超えて石を取らないよう、if文の箇所で設定される得る値の範囲を調整しています。
引数に要素数がNUMBER_OF_STONES以外の配列が渡された場合に関してはケアをしておりません。

また、rand関数が使われていますが、アプリの起動時にsrand((unsigned int)time(NULL))を実行するようにしているのでご安心を。

 

少し脇道にそれますが、ここで本連載のその1で掲載した画像を再掲します。
石取りゲームUI
これは、SetActPattern関数を使ってランダムに生成した第1世代の個体の遺伝子情報です。何か気付かないでしょうか?

ランダムという割に同じ数の連続が多くないでしょうか。そもそもrand関数で生成されるのがあくまでも疑似乱数で、完全に偏りのない乱数ではないためこうなったわけですが、その偏りが思ったよりも酷いということにUIでビジュアル化したことによって気付かされました。

rand関数をもっと工夫して使うことで偏りを軽減することも可能だと思いますが、乱数の偏りが今回の実験の趣旨に大きくは影響しないだろうということで、そのままにします。

 

続いて、世代の情報をファイルに書き出すWriteGeneData関数です。

BOOL WriteGeneData(const GENERATION_DATA* data, int generation)
{
  CFile cFile;
  try
  {
    CString path = GetDataPath(); // ファイルのパスを取得
    CFileFind find;
    if (!find.FindFile(path))
    {
      // ファイルが存在しなければ新規作成
      cFile.Open(path, CFile::modeCreate | CFile::modeWrite);
    }
    else
    {
     // ファイルが存在する場合は既存のものに上書き
      cFile.Open(path, CFile::modeWrite);
    }
    cFile.Seek(sizeof(GENERATION_DATA) * generation, CFile::begin); // 書き込む世代の位置までシーク
    cFile.Write(data, sizeof(GENERATION_DATA));
    cFile.Close();
    return TRUE;
  }
  catch (...)
  {
    cFile.Close();
    return FALSE;
  }
}

引数dataには書き込むデータ(GENERATION_DATA構造体については前回を参照)、generationには(書き込むデータの世代-1)を指定します。

関数内で呼ばれているGetDataPathはデータファイルのパスを取得するものです(詳細は割愛)。

単一のファイルに先頭から第1世代、第2世代、第3世代……とデータを記録するようにしていますので、書き込みの際には、1世代分のデータサイズ(GENERATION_DATAのサイズ)×(書き込むデータの世代-1)だけシークを行っています。

なお、データの読み込み用のReadGeneData関数は以下のように実装しています。

BOOL ReadGeneData(GENERATION_DATA* data, int generation)
{
  CFile cFile;
  try
  {
    CString path = GetDataPath(); // ファイルのパスを取得
    CFileFind find;
    if (!find.FindFile(path))
    {
      return FALSE;
    }
    cFile.Open(path, CFile::modeRead);
    cFile.Seek(sizeof(GENERATION_DATA) * generation, CFile::begin); // 読み込む世代の位置までシーク
    cFile.Read(data, sizeof(GENERATION_DATA));
    cFile.Close();
    return TRUE;
  }
  catch (...)
  {
    cFile.Close();
    return FALSE;
  }
}

こちらも書き込みと同様、1世代分のデータサイズ(GENERATION_DATAのサイズ)×(読み込むデータの世代-1)のシークを行って読み込みをしています。

今回紹介した関数は実験の根幹に関わるものではないため当初は詳細を取り上げないつもりでしたが、特に疑似乱数のところについて触れておきたくなったので、予定を変えて紹介することにしました。

次回は、今回の実験の根幹である遺伝的操作を行うGetNextGeneration関数の実装を取り上げます。

    上に戻る