囲碁プログラムの作り方(中級編)

はじめに

囲碁プログラムの作り方(初級編)では、 囲碁プログラムの考え方について解説し、簡単な思考ルーチンを作りました。 この中級編では、思考ルーチンのうちの盤面の認識部分の改良を行ないます。

盤面の認識の改良

強いプログラムを作るために最も重要なものは、 盤面の状況を詳しく正確に認識することです。 盤面の状況と言っても、黒石や白石がどこにあるかということではありません。 どの石が生きていてどの石が死んでいるのか、どの石が重要な石なのか、 などといった視覚情報だけではわからない(考えて初めてわかる)情報です。 このような情報を正確に認識できないと、どの手が良いのか悪いのかわかりません。
今、「どの石が」と言いましたが、これは一つ一つの石を指しているのではなく、 幾つかの石の集合を指しています。 人間は、幾つかの石の集合を一つの単位として捉えていて、 その集合ごとに情報を認識しています。 プログラムにも同じことをさせます。

最も基本的な単位(連)

石の生死を考えるとき、最も基本的な単位は何でしょう。 それは、縦横につながった同じ色の石の集合です。 別の言い方をすると、 相手の石によって囲まれたら盤上から取り上げられる石の集合です。 囲碁用語には、この「縦横につながった同じ色の石の集合」を表現する言葉は ありませんが、多くの囲碁プログラムの作者は「連」と呼んでいるので、 ここでも連と呼ぶことにします (海外では英語でstring(ストリング)と呼ばれることが多い)。

以下にその例を描きます。左は盤面で、右は連ごとに同じ番号を付けたものです。 この盤面には8つの連が存在していることがわかります。

盤面                連番号
+++++++      +++++++
+●●●+++      +111+++
++○●○++      ++213++
+●●○●○+      +44567+
+++○+++      +++5+++
++++●++      ++++8++
+++++++      +++++++

連のデータ構造の例

それぞれの連について、どんな情報を認識するとよいのでしょうか? 残念ながら、その答はわかりません。 ここでは、多くの囲碁プログラムが認識している情報を挙げておきます。 生死の状態は、生き/死に/中立(自分から先に打てば生きだが相手から先に打つと死に) のどれかです。 もしも中立ならば、どこに打てば生きてどこに打てば死ぬかという情報も 一緒に覚えておいた方が良いでしょう。 なぜなら、その情報から生きるための手/殺すための手を、 候補手として生成できるからです。

連のデータ構造のプログラム例

それぞれの連に関する情報を蓄えるために構造体を定義します。
/* 碁盤の大きさ */
#define BOARD_SIZE 21

/* 色 */
#define BLACK 1            /* 黒   */
#define WHITE 2            /* 白   */

/* 生死 */
#define ALIVE     1	   /* 生   */
#define DEAD      2	   /* 死   */
#define NEUTRAL   3	   /* 中立 */

/* 連 */
struct go_string {
  int id;                      /* 識別番号                  */
  int color;                   /* 色 (BLACK/WHITE)          */
  int size;                    /* サイズ                    */
  int dame;                    /* ダメ数                    */
  int status;                  /* 生死 (ALIVE/DEAD/NEUTRAL) */
  int offense_x;               /* 取る手のX座標            */
  int offense_y;               /* 取る手のY座標            */
  int defense_x;               /* 逃げる手のX座標          */
  int defense_y;               /* 逃げる手のY座標          */
  struct go_string *neighbors; /* 隣接する連                */
  struct go_string *next;      /* 次の連                    */
};

/* 連識別番号(ID) */
static int stringIdBoard[BOARD_SIZE][BOARD_SIZE];
連の構造体の要素に、連を構成する各石の座標を格納してもよいのですが、 ここでは連の識別番号(ID)を格納することにします。 そして、連の識別番号(ID)を格納するための2次元配列を、別に用意しました。
下の図において、IDが4番の連ならば、 色はWHITE、サイズは2、ダメ数は4、生死はALIVE、 隣接する連は2番と5番の連になります。 座標は、識別番号格納用の配列から4番を探して、(6,4)と(6,5)であることがわかります。
盤面                                      識別番号(ID)
+++++++++++++++++++    +++++++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
+++○●●+●+++++++++++    +++122+3+++++++++++
+++○+○●++++++++●+++    +++1+45++++++++6+++
+++++○+++++++++++++    +++++4+++++++++++++
+++○+++++++++++++++    +++7+++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
+++○++++++++++●++++    +++8++++++++++9++++
+++++++++++++++++++    +++++++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
+++++++++++++++++++    +++++++++++++++++++

連の生死を調べる(探索)

連の生死は読んで(探索)で求めます。 黒から先に打った場合と、白から先に打った場合の2通りの探索を行ないます。
/*------------------------------*/
/* ある黒の連の生死を調べる場合 */
/*------------------------------*/

/* (1) 白から先に打ったときに、黒石を取れるか調べる */
status = SearchStatus( 生死を調べる黒石, 次は白が打つ );

/* (2) 結果によって分類 */
if( status == ALIVE ){
  /* 黒石を取れないならば、その黒石は生き */
  return( ALIVE );
}else if( status == DEAD ){
  /* 黒石が取れるならば、黒から先に打ったときに、黒石が取られないか調べる */
  status = SearchStatus( 生死を調べる黒石, 次は黒が打つ );

  /* (3) 結果によって分類 */
  if( status == ALIVE ){
    /* 黒石が取られないならば、その黒石は中立 */
    return( NEUTRAL );
  }else if( status == DEAD ){
    /* 黒石が取られるならば、その黒石は死に */
    return( DEAD );
  }
}
以上のように、黒から先に打った場合と、白から先に打った場合の結果がわかれば、 連の生死の判定は簡単にわかるのですが、実はここには大きな問題が隠れています。

それは、「どのような状態の時、連を取れないと判断するのか?」です。

もしも、取るのはダメが無くなった時、取れないのは合法手がなくなった時、 と定義したら、どうなるでしょう。以下の図の4番の連が取れないことがわかるまでに いったい何手先を読むことになるでしょう。 きっと、10手や20手じゃわからないでしょうね。

盤面                                      識別番号(ID)
+++++++++++++++++++    +++++++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
+++○●●+●+++++++++++    +++122+3+++++++++++
+++○+○●++++++++●+++    +++1+45++++++++6+++
+++++○+++++++++++++    +++++4+++++++++++++
+++○+++++++++++++++    +++7+++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
+++○++++++++++●++++    +++8++++++++++9++++
+++++++++++++++++++    +++++++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
思考時間は無限ではありません。 ですから、「多分この連は取れないだろう。」と判断して、 読みを打ち切らなければいけません。 人間も、ある程度読んだところで、「多分この連は取れないだろう。」と判断しています。
多くのプログラムは、「多分この連は取れないだろう。」の判断基準に、 次のどれかを使用しています。

探索アルゴリズム

連の生死は読んで(探索)で求めます。 この探索のアルゴリズムについては、ここでは解説しません。 アルゴリズムに関する本がたくさん出版されていますので、 そちらを読んで勉強して下さい。 インターネットの検索エンジンで調べる時は、「ゲーム木探索」「ミニマックス法」 「アルファベータ枝刈り法」などをキーワードにして調べると良いでしょう。

石と石との接続関係(結線)

連の次に基本となる盤面の情報は、石と石との接続関係です。 多くの囲碁プログラムは石と石との間に仮想的な線を引いて石を結んでいる状態 と考え、「結線(けっせん)」と呼んでいます (海外では英語でlinkage(リンケージ)と呼ばれることが多い)。 盤上に仮想的な線が引かれているというイメージから結線と名付けられたようです。 石と石が接続していれば、その石をひとつの集合として捉えることができるので、 石と石との接続関係(結線)を認識するのです。

以下にその例を描きます。 左図の黒(一間)は接続していると考えられるので、結線と呼びます。 中図の黒は白に覗かれている(白から切断可能)ので、結線と呼びません。 覗かれているという情報付きの結線と呼ぶプログラムもあるようです。 右図の黒は白によって完全に接続が絶たれているので、結線と呼ぶプログラムはないようです。

結線                覗かれている結線    結線とは呼ばない
+++++++      +++++++      +++++++      
+++++++      +++○+++      +++○+++
++●+●++      ++●+●++      ++●○●++
+++++++      +++++++      +++○+++
+++++++      +++++++      +++++++

結線のデータ構造の例

それぞれの結線について、どんな情報を認識するとよいのでしょうか? ここでは、多くの囲碁プログラムが認識している情報を挙げておきます。 以下は、石と石の間を■で表した例です。
++++++++      +++++++
++++++++      +++++++
++●■■●++      +++■●++
++++++++      ++●■+++
++++++++      +++++++
位置関係は、一間、二間、三間、コスミやケイマなどです。 しかし、あまり石と石が離れている場合は認識する必要はないでしょう。

また、石と石だけでなく、石と盤端も結線として認識しています。 以下にその例を描きます。

二線            三線            四線
+++++      +++++      +++++
+++++      +++++      ++●++
+++++      ++●++      ++■++
++●++      ++■++      ++■++
−−■−−      −−■−−      −−■−−
盤面全体では、以下のようになります。 覗かれているかどうかの定義はプログラムによって異なるようです。 例えば、右下の(14,17)の黒と(15,17)の白のように隣あっている場合、 例では三線と認識しましたが、相手からハネる手があるから結線ではないと 認識するプログラムもあります。
盤面                                      結線
+++++++++++++++++++    +++■+■++■++++++■+++
+++●+++++++++++++++    +++●■■++■++++++■+++
++○++●++●++++++++++    □□○+■●■■●++++++■+++
+++○+++++++++++●+++    +++○+++++++++++●■■■
+++++++++++++++++++    +++□+++++++++++++++
+++○+++++++++++++++    □□□○+++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
++++++++++++++++○++    ++++++++++++++++○□□
++●++++++++++++++++    ■■●+++++++++++++□++
+++++++++++++●●+○++    +++++++++++++●●+○□□
+++○+++++●+++●○○+++    +++○+++++●■■■●○○□□□
+++++++++++++++++++    +++□+++++■+++■□□□++
+++++++++++++++++++    +++□+++++■+++■□□□++

死活の単位(群)

石の生死を考えるときの最も基本的な単位は連であると説明しましたが、 人間が囲碁を打っているときに、普通は連ごとの生死は考えていません。 縦横にはつながっていないが、非常に近くにある石をひとまとまりの 単位として、生死を考えています。 それを多くの囲碁プログラムの作者は「群」と呼んでいます (海外では英語でgroup(グループ)と呼ばれることが多い)。

以下に例を描きます。左の盤面の石の配置に対して、群ごとに識別番号を付けたのが右の図です。
1番の群について見てみましょう。 1番の群は3つの黒石と、その黒石に囲まれた部分から成っています。 普通の囲碁プレーヤーなら、この3つの石について別々に生死を考えたりはしません。 3つの石をひとまとまりとして考えるだけでなく、この3つの石が囲んでいると考えられる部分も一緒にして、生死を考えています。

盤面                                      群の識別番号
+++++++++++++++++++    +++111111++++++2222
+++●+++++++++++++++    +++111111++++++2222
++○++●++●++++++++++    333+11111++++++2222
+++○+++++++++++●+++    3333+++++++++++2222
+++++++++++++++++++    3333+++++++++++++++
+++○+++++++++++++++    3333+++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
+++++++++++++++++++    +++++++++++++++++++
++++++++++++++++○++    ++++++++++++++++444
++●++++++++++++++++    555+++++++++++++444
+++++++++++++●●+○++    +++++++++++++66+444
+++○+++++●+++●○○+++    +++7+++++6666644444
+++++++++++++++++++    +++7+++++6666644444
+++++++++++++++++++    +++7+++++6666644444

群の定義

群の定義に、決定的なものはありません。 囲碁プログラムの作者によって、いろいろな定義をしています。 群の定義によく使われるのは、結線です。 上の例のように、結線によって囲まれた部分を群の定義とするのが、 最もわかりやすいでしょう。

この定義は、石が少ないうちは人間の認識結果に近いのですが、 石が混みあってくるといろいろと問題が出てきます。 (と言うか、ここでは厳密な定義まで書いていませんが)。 盤面の認識のうち、群の定義は非常に難しい問題なのです。 この群の定義と、群の生死の認識がどのくらい正確かによって、 プログラムの強さが決まると言ってもいいくらいです。

死活以外の群の使い道

群は死活の単位に使うだけではありません。 上の例のように、結線によって囲まれた部分を群の定義としていれば、 囲まれた部分を地と考えてもよいでしょう。 つまり、群が認識できれば、地を数えられ、形勢判断ができます。 形勢判断ができるということは、手の価値が計れるということです (ただし、上の定義による群では、模様や厚みを考慮にいれていないので、 正確とは言い切れませんが)。 従って、群を正確に認識することが、プログラムを強くする最も大きな鍵となるのです。

ここまでのまとめ

中級編では、盤面の認識の改良について説明しました。 盤面を正確に認識するためによく使われる情報として、 連、結線、群について紹介しました。 この連、結線、群について認識した情報を使うと、手の価値を正確に求めることができます。 また、認識した情報を候補手生成にも利用することができます。

プログラムの例

恒例のサンプルプログラムです。 今回は、連の識別番号を割り当てるプログラムです。

このプログラムは、"figure"という名前のファイルを読んで、 連の識別番号を表示するプログラムです。

"figure"の例
++++++++++++*++++++
++O+++++O**++*OOO++
++*O++++OO+**O++**+
++O++OO+++O+*+O*+++
+++*+*+++***+O+O*++
+O+++++OO++++++*+++
++OO**+O+O+++O+++++
++*++++*O++++++*+++
++O+**+*+++++O+++++
++++*O+++++++++*+++
+++OO+++++O+++++*++
+++++++O+O+++++++++
+++++++++*O++O+O+++
+++*+++++*O++++*+++
+*+++++++*++OO+++++
+OO+**+++*OO**O*+++
+++OO*+OOO***O++*++
+++++++***+++++++++
+++++++++++++++++++
/*------------------------------------------------------------------*/
/* ファイルから盤面図を読み込み、連の識別番号を出力するプログラム   */
/*------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

#define TRUE 1
#define FALSE 0

#define SPACE 0            /* 空点(石が置かれていない) */
#define BLACK 1            /* 黒   */
#define WHITE 2            /* 白   */
#define OUT   3            /* 盤外 */

#define BOARD_SIZE 21      /* 碁盤の大きさ */

/* 相手の色 */
#define reverseColor( color ) ( color == BLACK ? WHITE : BLACK )

/*------------------------------------------------------------------*/
/* 関数                                                             */
/*------------------------------------------------------------------*/

/* メイン関数 */
void main( void );

/* 盤面図入力 */
static void InputFigure( int board[][BOARD_SIZE] );       

/* 碁盤の初期化 */
static void InitializeBoard( int board[][BOARD_SIZE] );

/* 碁盤を表示する */
static void DisplayBoard( int board[][BOARD_SIZE] );

/* 連の認識 */
static void MakeString( int board[][BOARD_SIZE] );

/* 連の認識 */
static int  ClassificationString( int board[][BOARD_SIZE], int x, int y,
				  int color, int number );

/* 連の表示 */
static void DisplayString( void );

/*------------------------------------------------------------------*/
/* 変数                                                             */
/*------------------------------------------------------------------*/

/* 碁盤の表示のための文字 */
static char stone[] = { '+', '*', 'O', '?' };

/* 連識別番号 */
static int stringIdBoard[BOARD_SIZE][BOARD_SIZE];

/*------------------------------------------------------------------*/
/* メイン関数                                                       */
/*------------------------------------------------------------------*/
void
main( void )
{
  int board[BOARD_SIZE][BOARD_SIZE];  /* 碁盤 */

  printf( "Sample Program Start\n" );

  /* 盤面図の入力 */
  InputFigure( board );

  /* 碁盤の表示 */
  DisplayBoard( board );

  /* 連の認識 */
  MakeString( board );

  /* 連の表示 */
  DisplayString();

  printf( "Sample Program End\n" );
}

/*------------------------------------------------------------------*/
/* 盤面図入力                                                       */
/* ファイル名"figure"というファイルに盤面図が書かれている           */
/*------------------------------------------------------------------*/
static void
InputFigure( int board[][BOARD_SIZE] )
{
  FILE *fp;
  char buf[256];
  int  x, y, i;

  /* ファイルを開く */
  if( (fp = fopen( "figure", "r" )) == NULL ){
    fprintf( stderr, "ERROR: file(figure) open fail\n" );
    exit(1);
  }

  /* 碁盤の初期化 */
  InitializeBoard( board );

  /* 各座標を読む */
  for( y=1; y < (BOARD_SIZE-1); y++ ){
    buf[0] = '\0';
    fgets( buf, 256, fp );
    for( x=1, i=0; x < (BOARD_SIZE-1); x++ ){
      if( buf[i] == 'O' ){
        board[y][x] = WHITE;
      }else if( buf[i] == '*' ){
        board[y][x] = BLACK;
      }else if( buf[i] == '+' ){
        board[y][x] = SPACE;
      }else{
        board[y][x] = SPACE;
      }
      i++;
    }
  }

  /* ファイルを閉じる */
  fclose( fp );
}

/*------------------------------------------------------------------*/
/* 碁盤の初期化                                                     */
/*------------------------------------------------------------------*/
static void
InitializeBoard( int board[][BOARD_SIZE] )
{
  int x, y;

  for( y=1; y < (BOARD_SIZE-1); y++ ){
    for( x=1; x < (BOARD_SIZE-1); x++ ){
      board[y][x] = SPACE;
    }
  }

  for( y=0; y < BOARD_SIZE; y++ ){
    board[y][0]= OUT;
    board[y][BOARD_SIZE-1] = OUT;
    board[0][y] = OUT;
    board[BOARD_SIZE-1][y] = OUT;
  }
}

/*------------------------------------------------------------------*/
/* 碁盤を表示する                                                   */
/*------------------------------------------------------------------*/
static void
DisplayBoard( int board[][BOARD_SIZE] )
{
  int x, y;

  printf( "    [ 1 2 3 4 5 6 7 8 910111213141516171819]\n" );
  for( y=1; y < (BOARD_SIZE-1); y++ ){
    printf( "[%2d] ", y );
    for( x=1; x < (BOARD_SIZE-1); x++ ){
      printf( " %c", stone[board[y][x]] );
    }
    printf( "\n" );
  }
}

/*------------------------------------------------------------------*/
/* 連の認識                                                         */
/*------------------------------------------------------------------*/
static void
MakeString( int board[][BOARD_SIZE] )
{
  int x, y;
  int stringId;

  for( y=0; y < BOARD_SIZE; y++ ){
    for( x=0; x < BOARD_SIZE; x++ ){
      stringIdBoard[y][x] = 0;
    }
  }

  /* string id */
  stringId = 1;
  for( y=1; y<(BOARD_SIZE-1); y++ ){
    for( x=1; x<(BOARD_SIZE-1); x++ ){
      if( board[y][x] != SPACE ){
        if( ClassificationString( board, x, y, board[y][x], stringId ) == TRUE ){
	  stringId++;
        }
      }
    }
  }

  /* 連情報を登録 */
}

/*------------------------------------------------------------------*/
/* 連を区別する                                                     */
/*------------------------------------------------------------------*/
static int
ClassificationString( int board[][BOARD_SIZE], int x, int y, int color, int number )
{
  /* 既にIDが付いているので対象外 */
  if( stringIdBoard[y][x] != 0 ){
    return( FALSE );
  }

  if( board[y][x] == color ){
    /* 同じ色なので同じ連である */
    stringIdBoard[y][x] = number;
    if( x > 1 ){
      ClassificationString( board, x-1, y, color, number );
    }
    if( x < (BOARD_SIZE-2) ){
      ClassificationString( board, x+1, y, color, number );
    }
    if( y > 1 ){
      ClassificationString( board, x, y-1, color, number );
    }
    if( y < (BOARD_SIZE-2) ){
      ClassificationString( board, x, y+1, color, number );
    }
    return( TRUE );
  }else{
    /* 同じ色ではないので終了 */
    return( FALSE );
  }
}

/*------------------------------------------------------------------*/
/* 連の表示                                                         */
/*------------------------------------------------------------------*/
static void
DisplayString( void )
{
  int x, y;

  printf( "### String ID ###\n" );
  for( y=1; y < (BOARD_SIZE-1); y++ ){
    for( x=1; x < (BOARD_SIZE-1); x++ ){
      if( stringIdBoard[y][x] == 0 ){
	printf( "  +" );
      }else{
	printf( "%3d", stringIdBoard[y][x] );
      }
    }
    printf( "\n" );
  }
}

/*---------------------- < end of program > ------------------------*/
勝也のページへ戻る