序盤での定跡と駒組の問題

先の章までで、一応将棋らしきものを指すプログラムにはなった。

しかし、序盤での「定跡(じょうせき)」や、「作戦」「駒組み」などについてまったく評価関数で評価していないため、序盤がデタラメで、序盤の定跡を知っている人間相手にはまったくと言ってよいほど歯が立たないと思う。

そこで、序盤の組み立てについて考えてみる。

定跡を組み込む

序盤では、

・玉を固める
・攻撃の態勢を作る
の二つの行動をバランス良く行なう必要がある。

人間は、これを、定跡を覚える(あるいは、高段者ならば定跡を研究・開発する)、そして実戦で応用するという手段で行っている。

コンピュータは暗記は得意なので、定跡を丸覚えして、同じ局面だったら定跡にある手を指すといった処理は得意で、プログラムを組むのも非常に簡単である。

しかし、その一方で、「定跡を外された」(=自分の知らない局面にされた)ときに、人間ならある程度定跡を踏まえながら応用して進めることができるが、コンピュータは知っている知識を応用するという処理が非常に苦手である。

そこで、定跡と駒組みに対しては以下の2つの方法でアプローチする。

・定跡データを用意し、同じ局面があれば定跡データの中にある手を指す。
・定跡から外れたときには、評価関数できちんと駒組みができるように、評価関数をチューニングする。
さらに、
・定跡から外れたときに、どういう戦形なのかきちんと把握して、その戦形ごとに評価関数を作る。
そのための方法としては、ルール・ベースのもの(if文で書く)と、定跡データに戦形を入れておく方法と両方が考えられる。

評価関数のチューニングと戦形の判別のほうは、きちんとやると限りがないが、本書では、「相手と自分の飛車の位置に合わせて「金」「銀」「玉」の形を作る」程度までを行なうことにする。

この方法だと、ほぼ玉を固める方法しかできないので、攻撃の態勢を作ることはできない。しかし、基本的には本書の方法の応用で攻撃の駒組みを行なうこともできるはずだし、定跡データに戦形を入れることで、もっときちんと戦形を判別することもできる。「攻撃用の駒組み表を作ること」や「戦形をきちんと判別すること」は、より強いプログラムを作る際の読者の課題としてほしい。

実際のプログラミング

では、さっそくプログラムに取り掛かろう。

ここでは、Kyokumenクラスを派生させて、「KyokumenKomagumi」クラスを作ることにする。これに伴って、NegaAlphaBetaの中で宣言しているKyokumenクラスは、「KyokumenKomagumi」クラスに変更する。

今回は、守りの駒ということで、玉と金と銀の位置だけ評価する。(本当は、先ほども述べたように、守りの桂馬や香車、歩なども調整したほうがよいに決まっているが、あまりにも手間がかかるので、“手抜き”をさせてもらうことにした)。

プログラムで定数を定義するときに分かりやすいよう、いったん「9×9」の表を用意して、SenkeiInit()関数で実際の駒組み定数値に入れる際に、インデックスなどを調整している。SenkeiInit()関数を呼ぶタイミングは、定跡に手がなくなったときでいいだろう。 (相手の飛車が移動したりすることを考慮して、先読みで1手進めるたびに毎回呼ぶことも考えられるが、あまりにも処理が遅くなるので、先読みの手数が浅くなってしまうだろう。あるいは、自分に手番が回ってくるたびに、SenkeiInit()を呼び出す方法でも本章で紹介した方法よりもフレキシブルになるのでよいかもしれない。)

このように表を用意しておくと、駒が評価点の低いところから評価点の高いところに徐々に移動していく。YSS※5では、この様子を「落とし穴」に例えて、「落とし穴法」と呼んでいる。

Kyokumen.h追加分

// 7章で追加。駒組みのボーナス点を与える。
class KyokumenKomagumi: public Kyokumen {
public:
  static int KomagumiValue[ERY+1][16*11];
public:
  KyokumenKomagumi(int tesu,KomaInf ban[9][9],int Motigoma[]) : Kyokumen(tesu,ban,Motigoma) {
  }
  void Initialize();
  void InitKanagomaValue();
  void SenkeiInit();
  void Move(int SorE,const Te &te);
  void narabe(int tesu,const char *t[]);
};

Kyokumen.cpp追加分

int KyokumenKomagumi::KomagumiValue[ERY+1][16*11];

int DanValue[ERY+1][10]={
//空
  {0,0,0,0,0,0,0,0,0,0},
//空
  {0,0,0,0,0,0,0,0,0,0},
//空
  {0,0,0,0,0,0,0,0,0,0},
//空
  {0,0,0,0,0,0,0,0,0,0},
//空
  {0,0,0,0,0,0,0,0,0,0},
//空
  {0,0,0,0,0,0,0,0,0,0},
//空
  {0,0,0,0,0,0,0,0,0,0},
//空
  {0,0,0,0,0,0,0,0,0,0},
//空
  {0,0,0,0,0,0,0,0,0,0},
//空
  {0,0,0,0,0,0,0,0,0,0},
//空
  {0,0,0,0,0,0,0,0,0,0},
//空
  {0,0,0,0,0,0,0,0,0,0},
//空
  {0,0,0,0,0,0,0,0,0,0},
//空
  {0,0,0,0,0,0,0,0,0,0},
//空
  {0,0,0,0,0,0,0,0,0,0},
//空
  {0,0,0,0,0,0,0,0,0,0},
//空
  {0,0,0,0,0,0,0,0,0,0},
//歩
  { 0,  0,15,15,15,3,1, 0, 0, 0},
//香
  { 0, 1,2,3,4,5,6,7,8,9},
//桂
  { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
//銀
  { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
//金
  { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
//角
  { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
//飛
  { 0,10,10,10, 0, 0, 0,  -5, 0, 0},
//王
  { 0,1200,1200,900,600,300,-10,0,0,0},
//と
  { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
//杏
  { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
//圭
  { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
//全
  { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
//金
  { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
//馬
  { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
//龍
  { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
//空
  { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
//歩
  { 0, 0, 0, 0, -1, -3,-15,-15,-15, 0},
//香
  { 0,-9,-8,-7, -6, -5, -4, -3, -2,-1},
//桂
  { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
//銀
  { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
//金
  { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
//角
  { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
//飛
  { 0, 0, 0, 5, 0, 0, 0,-10,-10,-10},
//王
  { 0, 0, 0, 0,10,-300,-600,-900,-1200,-1200},
//と
  { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
//杏
  { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
//圭
  { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
//全
  { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
//金
  { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
//馬
  { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
//龍
  { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
};

enum {
  IvsFURI,    // 居飛車対振り飛車
  IvsNAKA,    // 居飛車対中飛車
  FURIvsFURI,    // 相振り飛車
  FURIvsI,    // 振り飛車対居飛車
  NAKAvsI,    // 中飛車対居飛車
  KAKUGAWARI,    // 角換り
  AIGAKARI,    // 相掛かり(または居飛車の対抗系)
  HUMEI      // 戦形不明
};

int JosekiKomagumiSGI[HUMEI+1][9][9]=
{
  {  // IvsFURI 舟囲い、美濃、銀冠
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10, -7,-10,-10,-10,-10,-10,  7,-10},
    {-10,  7, -8, -7, 10,-10, 10,  6,-10},
    {-10, -2, -6, -5,-10,  6,-10,-10,-10},
    {-10, -7,  0,-10,-10,-10,-10,-10,-10}
  },{  // IvsNAKA 舟囲い
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10, -7,-10,-10, -7,-10,-10,  7,-10},
    {-10, -5, -8, -7, 10,-10, 10,  6,-10},
    {-10, -2, -3,  0,-10,  6,-10,-10,-10},
    {-10, -7, -5,-10,-10,-10,-10,-10,-10}
  },{ // FURIvsFURI 矢倉(逆)、美濃、銀冠
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10, -7, -7,-10},
    {-10,-10,-10,-10,-10,  5, 10, 10,-10},
    {-10,-10,-10,-10,-10,-10,  0,-10,-10},
    {-10,-10,-10,-10,-10,-10, -5,-10,-10}
  },{ // FURIvsI 美濃囲い、銀冠
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10, -3, -7,-10,-10,-10,-10,-10},
    {-10, -7,  4,  6,-10,-10,-10,  6,-10},
    {-10,  2,  3,  3,-10,-10,  4,-10,-10},
    {-10,-10,-10,  0,-10,-10,  0,-10,-10}
  },{ // NAKAvsI 中飛車
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,  8,  5,  8,-10,-10,-10},
    {-10,-10,  4,  4,  3,  4,  4,-10,-10},
    {-10,-10,  0,-10,-10,-10,  0,-10,-10}
  },{ // KAKUGAWARI
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,  7,  5, -3,-10,-10},
    {-10,  8, 10,  7,  4,  0, -4,-10,-10},
    {-10,  0,-8,  -4,-10,-10, -5,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10}
  },{ // AIGAKARI
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,  0,-10,-10,-10,-10,-10,-10},
    {-10, -5,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10}
  },{ // HUMEI
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,  5,-10,-10},
    {-10,-10,-10,-10,-10,-10, -4,  0,-10},
    {-10,-10,  0,-10,-10,-10, -4, -3,-10},
    {-10, -5,-10, -5,-10,-10, -5,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10}
  }
};

int JosekiKomagumiSKI[HUMEI+1][9][9]=
{
  {  // IvsFURI
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,  1,  2,-10,-10,-10,-10},
    {-10,-10,-10,  0,-10, -4,-10,-10,-10}
  },{  // IvsNAKA
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,  1,  2,-10,-10,-10,-10},
    {-10,-10,-10,  0,-10, -4,-10,-10,-10}
  },{ // FURIvsFURI
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,  7, -3,-10,-10},
    {-10,-10,-10,-10,  5,  3,  6,-10,-10},
    {-10,-10,-10,-10,-10,  5,  4,-10,-10}
  },{ // FURIvsI
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,  5,  1,-10,-10},
    {-10,-10,-10,-10,  4,  3,  7, -3,-10},
    {-10,-10,-10,  0,  1,  5,  2, -7,-10}
  },{ // NAKAvsI
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10, -7, -4, -4,-10, -4, -4, -7,-10},
    {-10, -5, 10,  6,-10,  8, 10, -5,-10},
    {-10, -7, -6, -3, -6, -3, -6, -7,-10}
  },{ // KAKUGAWARI
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,  6, -4, -4, -4, -8,-10},
    {-10,-10, 10,-10,  3,  0,  0, -7,-10},
    {-10,-10,-10,  0,-10,  0, -5, -7,-10}
  },{ // AIGAKARI
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,  6,-10,-10,-10,-10,-10},
    {-10,-10, 10,-10,  3,-10,-10,-10,-10},
    {-10,-10,-10,  0,-10,  0,-10,-10,-10}
  },{ // HUMEI
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,  3,-10,  5,-10,-10,-10,-10},
    {-10,-10,-10,  0,-10,  0,-10,-10,-10}
  }
};

int JosekiKomagumiSOU[HUMEI+1][9][9]=
{
  {  // IvsFURI
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {- 7,  9,-10,-10,-10,-10,-10,-10,-10},
    {  5,  7,  8,  4,-10,-10,-10,-10,-10},
    { 10,  5,  3,-10,-10,-10,-10,-10,-10}
  },{  // IvsNAKA
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {- 7,  9,-10,-10,-10,-10,-10,-10,-10},
    {  5,  7,  8,  4,-10,-10,-10,-10,-10},
    { 10,  5,  3,-10,-10,-10,-10,-10,-10}
  },{ // FURIvsFURI
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,  4,  6, 10,  6},
    {-10,-10,-10,-10,-10,  4,  6,  5, 10}
  },{ // FURIvsI
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,  4,  6, 10,  6},
    {-10,-10,-10,-10,-10,  4,  6,  5, 10}
  },{ // NAKAvsI
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,  4,  6, 10,  6},
    {-10,-10,-10,-10,-10,  4,  6,  5, 10}
  },{ // KAKUGAWARI
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {- 3, -4, -3,-10,-10,-10,-10,-10,-10},
    {  6,  8, -2,  0, -3,-10,-10,-10,-10},
    { 10,  6, -4,- 6,- 7,-10,-10,-10,-10}
  },{ // AIGAKARI
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {- 3, -4, -3,-10,-10,-10,-10,-10,-10},
    {  6,  8,  0,- 4,-10,-10,-10,-10,-10},
    { 10,  6, -4,- 6,- 7,-10,-10,-10,-10}
  },{ // HUMEI
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {-10,-10,-10,-10,-10,-10,-10,-10,-10},
    {- 3, -4, -3,-10,-10,-10,-10,-10,-10},
    {  6,  8,  0,- 4,-10,-10,-10,-10,-10},
    { 10,  6, -4,- 6,- 7,-10,-10,-10,-10}
  }
};

int JosekiKomagumi[9][ERY+1][16*11];

void KyokumenKomagumi::Initialize()
{
  int suji,dan,koma;
  for(suji=0x10;suji<=0x90;suji+=0x10) {
    for(dan=1;dan<=9;dan++) {
      for(koma=SFU;koma<=ERY;koma++) {
        KomagumiValue[koma][suji+dan]=0;
        JosekiKomagumi[0][koma][suji+dan]=DanValue[koma][dan];
      }
    }
  }
}

void KyokumenKomagumi::SenkeiInit()
{
  int SHI1,SHI2;
  int EHI1,EHI2;
  int SKA1,SKA2;
  int EKA1,EKA2;
  int suji,dan,koma;
  SHI1=SHI2=EHI1=EHI2=SKA1=SKA2=EKA1=EKA2=0;
  for(suji=0x10;suji<=0x90;suji+=0x10) {
    for(dan=1;dan<=9;dan++) {
      if (ban[suji+dan]==SHI) {
        if (SHI1==0) SHI1=suji+dan; else SHI2=suji+dan;
      }
      if (ban[suji+dan]==EHI) {
        if (EHI1==0) EHI1=suji+dan; else EHI2=suji+dan;
      }
      if (ban[suji+dan]==SKA) {
        if (SKA1==0) SKA1=suji+dan; else SKA2=suji+dan;
      }
      if (ban[suji+dan]==EKA) {
        if (EKA1==0) EKA1=suji+dan; else EKA2=suji+dan;
      }
    }
  }
  if (Hand[SHI]==1) if (SHI1==0) SHI1=1; else SHI2=1;
  if (Hand[SHI]==2) SHI1=SHI2=1;
  if (Hand[EHI]==1) if (EHI1==0) EHI1=1; else EHI2=1;
  if (Hand[EHI]==2) EHI1=EHI2=1;
  if (Hand[SKA]==1) if (SKA1==0) SKA1=1; else SKA2=1;
  if (Hand[SKA]==2) SKA1=SKA2=1;
  if (Hand[EKA]==1) if (EKA1==0) EKA1=1; else EKA2=1;
  if (Hand[EKA]==2) EKA1=EKA2=1;

  int Senkei,GyakuSenkei;
  if (SHI1<=0x50 && EHI1<=0x50) {
    Senkei=IvsFURI;
    GyakuSenkei=FURIvsI;
  } else if (0x50<=EHI1 && EHI1<=0x5f && SHI1<=0x50) {
    Senkei=IvsNAKA;
    GyakuSenkei=NAKAvsI;
  } else if (SHI1<=0x5f && EHI1<=0x5f) {
    Senkei=FURIvsFURI;
    GyakuSenkei=FURIvsFURI;
  } else if (EHI1>=0x60 && SHI1>=0x60) {
    Senkei=FURIvsI;
    GyakuSenkei=IvsFURI;
  } else if (0x50<=SHI1 && SHI1<=0x5f && EHI1<=0x50) {
    Senkei=NAKAvsI;
    GyakuSenkei=IvsNAKA;
  } else if (SKA1==1 && EKA1==1) {
    Senkei=KAKUGAWARI;
    GyakuSenkei=KAKUGAWARI;
  } else if (0x20<=SHI1 && SHI1<=0x2f && 0x80<=EHI1 && EHI1<=0x8f) {
    Senkei=AIGAKARI;
    GyakuSenkei=AIGAKARI;
  } else {
    Senkei=HUMEI;
    GyakuSenkei=HUMEI;
  }
  for(suji=0x10;suji<=0x90;suji+=0x10) {
    for(dan=1;dan<=9;dan++) {
      value-=KomagumiValue[ban[suji+dan]][suji+dan];
      for(koma=SFU;koma<=ERY;koma++) {
        if (koma==SGI) {
          JosekiKomagumi[Senkei][koma][suji+dan]=JosekiKomagumiSGI[Senkei][dan-1][9-(suji/0x10)];
        } else if (koma==EGI) {
          JosekiKomagumi[Senkei][koma][suji+dan]=-JosekiKomagumiSGI[GyakuSenkei][9-dan][suji/0x10-1];
        } else if (koma==SKI) {
          JosekiKomagumi[Senkei][koma][suji+dan]=JosekiKomagumiSKI[Senkei][dan-1][9-(suji/0x10)];
        } else if (koma==EKI) {
          JosekiKomagumi[Senkei][koma][suji+dan]=-JosekiKomagumiSKI[GyakuSenkei][9-dan][suji/0x10-1];
        } else if (koma==SOU) {
          JosekiKomagumi[Senkei][koma][suji+dan]=JosekiKomagumiSOU[Senkei][dan-1][9-(suji/0x10)];
        } else if (koma==EOU) {
          JosekiKomagumi[Senkei][koma][suji+dan]=-JosekiKomagumiSOU[GyakuSenkei][9-dan][suji/0x10-1];
        } else {
          JosekiKomagumi[Senkei][koma][suji+dan]=DanValue[koma][dan];
        }
        KomagumiValue[koma][suji+dan]=JosekiKomagumi[Senkei][koma][suji+dan];
      }
      value+=KomagumiValue[ban[suji+dan]][suji+dan];
    }
  }
}

void KyokumenKomagumi::Move(int SorE,const Te &te)
{
  value-=KomagumiValue[te.koma][te.from];
  if (te.capture) {
    value-=KomagumiValue[te.capture][te.to];
  }
  if (!te.promote) {
    value+=KomagumiValue[te.koma][te.to];
  } else {
    value+=KomagumiValue[te.koma|PROMOTED][te.to];
  }
  Kyokumen::Move(SorE,te);
}

将棋の分かる人ならある程度分かってもらえるかと思うが、相手と自分の飛車・角・玉の位置などを見て、こういう形に組みたい、というデータを入れている。

将棋の強い人であれば、もっと良い『落とし穴』も作れるかと思うが、筆者はとっても将棋が弱いので、こんな程度にしか出来なかった、という事情もある。

定跡データのフォーマット

定跡データのフォーマットであるが、大きく二通りが考えられる。

一つは、初手からツリー状に展開されるデータを何らかのフォーマットでもつ方法である。「初手からどの手が指されたから、次はこのノード」というように定跡データを追っていき、そこで定跡として登録されている手を指すという形式になる。

この方法は、ツリーに評価点をもたせるなどの応用がしやすく、定跡の編集も容易であるものの、実装に手が掛かるという欠点もある。

もう一つは、単なる棋譜データを大量に保存しておき、その棋譜を初手から現在までの手数分だけ並べて、現在の局面と比較し、局面が一致したならば、そこで指された手を指す、という方法である。この方法は、作成は容易であるが、「定跡の編集がしにくい」「評価点などを与えにくい」といった欠点がある。

本書では、欠点には目をつぶって、実装が簡単な、「棋譜を大量に保存しておいて必要に応じて並べる」形式をとることとする。

CSA将棋(※7)のフォーマット

※7 CSA将棋 …増本智宣氏の手による将棋プログラム。UI・通信部分だけを提供し、思考ルーチンは決められたインターフェースに沿った形のsikou.dllを作成することになる。

筆者は、CSA将棋をユーザー・インターフェイスに用いて大会に参加している。CSA将棋の棋譜のフォーマット形式と定跡データ内の棋譜のフォーマットが一致していると、CSA将棋から渡される棋譜を並べる関数と、定跡データ内の棋譜を並べる関数に同じものが使えるので、都合が良い。

そこで、棋譜フォーマットには、CSA将棋の棋譜のフォーマット形式を用いることとした。

CSA将棋の棋譜の形式は、図1で示す座標形式を基にして、駒の移動元、移動先を表わしている。

図1

さらに、移動元が駒台、つまり駒を打つ場合には、歩なら「101」、香なら「102」、桂なら「103」、銀なら「104」、金なら「105」、角なら「106」、飛車なら「107」を用いる。

また、移動先で成る場合には、移動先の数字に100を加える、という形式になっている。例えば、7七の歩を7六に動かす場合、「61,52」となり、24の銀が23に成る場合、「29,120」となり、飛車を1九に打つ場合には、「107,73」となる。

定跡データを用意する方法

定跡データを用意する方法であるが、フリーソフトの「棋泉」がデータ形式を公開しており、同じくフリーソフトの「将棋道場」がツリー状のデータをCSA形式の拡張形式で出力する機能を備えている。

ここでは、将棋道場のツリー状のデータを棋譜の並びに変換する方法と、棋泉のデータを棋譜の並びに変換する方法を研究してみる。(筆者は面倒くさがりなので、定跡データを自分で入れることはあまり考えていない。プロの棋譜を買ってきて、大量の棋譜データを溜め込めば、ある程度信頼にたる定跡データができると考えている。もっとも、プロ間で新手が発見され、どちらかの負けが明らかになったので指されなくなったデータを取り除いたりする処理は本来は必要なのだが。)

PCL形式のデータ

まず、将棋道場のpclデータ形式だが、以下のようになっている。

(以下、「将棋道場」のヘルプより抜粋)

拡張CSAファイル(PCL 形式)

通常のCSA形式では一通り(1次元)の手順しか記述できません。ところが定跡や詰将棋の読み筋空間などを表現するには一つの局面から枝分かれした複数の手順を記述する必要があります。そこで将棋道場ではオリジナルのCSA形式を踏襲しつつ拡張を施し、指し手空間を表現できる拡張CSA形式(PCL形式と呼ぶことにします)を定義しています。

詰将棋ドキュメントはPCL形式、対局棋譜ドキュメントはCSA形式が標準ファイルです。定跡登録ではこれらのどちらの形式でも読み込むことができますが、定跡のファイル出力はPCL形式で出力されます。

以下、定跡空間を例に、拡張CSA(PCL)形式について説明します。

指し手の表記はCSA形式と同じ表記方法とします。指し手は先ず局面をすすめる方向(指し手が深くなる方向)に出力します。すすめる指し手がもっとも深いところまで行きつくと、そこから今すすめてきた指し手をさかのぼって最初に分岐する指し手から再び指し手をすすめて出力します。

分岐する指し手はその前にキーワードとして「手数目」指定を出力します。手数目指定はその直前に出力されたリストの該当手数目から分岐することを示しています。以下のようなイメージです。

①先手7六歩→ ②後手3四歩→ ③先手2六歩→ ④後手4四歩→ ⑤先手4八銀
                       ⑥手数目4…ここで今辿ってきた手順の4手目を指定
                       ⑦後手8四歩→ ⑧先手2五歩

①~⑧の順にCSA形式の指し手を出力します。手数目キーワードは、

‘手数目:4

というようにCSA形式のコメント表記を利用し、「‘手数目:」をキーワードとしてそれに続く値を指定値とします。手数目は初手を1として数えます。千日手など、指し手をすすめて同一局面が現れた場合はそこで指し手をすすめることをストップします。

将棋道場の定跡では、「局面の形勢評価値」や「定跡手の評価値」を登録することできます。そこで拡張CSA形式では上記手数目指定も含め、CSA棋譜形式のコメント記述記号"'"につづいて、以下の項目キーワードを用いて、定跡の登録に関する種々の指定をすることができるようになっています。

項目キーワード項目値

'手数目: 分岐する指し手の手数目指定。
'形勢評価値: 形勢評価値(int)。局面指定の直後に指定します。(初期値100)
'属性: 定跡手の属性(>=0)。指し手の直後に指定します。(初期値100)
'注釈: 注釈文。局面または定跡手の直後に指定します。

評価値、属性値は省略すると初期値が採用されます。


さて、実際に、PCL形式はほとんどCSA形式なわけであるが、CSA形式に手戻りを加えた形式と言える。

手の戻りを考慮しつつ、これを定跡データに変換するプログラムは、以下のようになる。

pcl2bin.cpp

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

void UsageExit()
{
  fprintf(stderr,"Usage:pcl2bin pclfile binfile");
  exit(1);
}

int ban[256][256];
unsigned char joseki[512];
char buf[1024];

const char *komakind[16] = {
  "  ","FU","KY","KE","GI","KI","KA","HI",
  "OU","TO","NY","NK","NG","  ","UM","RY"
};


int main(int argc,char *argv[])
{
  if (argc!=3) UsageExit();
  
  FILE *pcl=fopen(argv[1],"r");
  if (pcl==NULL) {
    fprintf(stderr,"can't open file \"%s\".",argv[1]);
    UsageExit();
  }
  FILE *bin=fopen(argv[2],"wb");
  if (bin==NULL) {
    fprintf(stderr,"can't open file \"%s\".",argv[2]);
    UsageExit();
  }
  
  int t=0;
  for(int i=0;i<256;i++) {
    ban[t][i]=0;
  }
  while(fgets(buf,sizeof(buf),pcl)!=NULL) {
    char komastr[256];
    char dummy[2];
    int sx,sy,dx,dy;
    int tt;
    if (sscanf(buf,"'手数目:%d",&tt)==1) {
      // 手数目を読み込んだ
      if (tt<=t) {
        // 今までの分をファイルに吐き出して
        joseki[t*2]=0xff;
        joseki[t*2+1]=0xff;
        for(t++;t<256;t++) {
          joseki[t*2]=0x00;
          joseki[t*2+1]=0x00;
        }
        fwrite(joseki, 1, 512, bin);
        // 手数を戻す
        t=tt-1;
      }
    } else if (sscanf(buf,"%1s%1d%1d%1d%1d%2s",dummy,&sx,&sy,&dx,&dy,komastr)==6) {
      if (t>0) {
        memcpy(ban[t],ban[t-1],sizeof(ban[0]));
      }
      int koma = -1;
      for (i = 0; i < 16; i++) {
        if (strcmp(komakind[i], komastr) == 0) {
          koma = i;
          break;
        }
      }
      int to = (dy-1)*9 + dx;
      int from = (sy-1)*9 + sx;
      if (from == -9) {
        joseki[t*2] = to;
        joseki[t*2+1] = 0x64 + koma;
      } else {
        if (koma>8) ban[t][to] = 1;
        if (koma>8 && ban[t][from] == 0) to += 100;
        joseki[t*2] = to;
        joseki[t*2+1] = from;
        ban[t][from] = 0;
      }
      t++;
    }
  }
  // 最後に残ったものを書き出す
  joseki[t*2]=0xff;
  joseki[t*2+1]=0xff;
  for(t++;t<256;t++) {
    joseki[t*2]=0x00;
    joseki[t*2+1]=0x00;
  }
  fwrite(joseki, 1, 512, bin);
  
  fclose(bin);
  return 0;
}

棋泉(きせん)※8形式のデータ

※8 棋泉 …将棋プログラムのひとつ。対局プログラムではなく、棋譜再生に使われる。

次に、棋泉のデータ形式だが、以下のようになっている。

棋泉のデータ形式

これはたまたま筆者の用いている定跡データ(CSA将棋形式とでも呼べようか)に形式がそっくりである。もしかすると、CSA将棋の作者の方が開発に関わっているのかもしれない。

違いは持ち駒を打つところだけなので、持ち駒を打つ手だけを変更すればよい。

持ち駒を打つ時は、移動先に「持っている持ち駒の価値の高い方から数えて何番目の持ち駒を打つか+100」を入れる。

なお、棋泉形式については、筆者がプログラムを書いた2002年前後には開発元と思われるところから公開されていたのだが、現在は正式には公開されていないようである。

以下のようなプログラムで変換できる。

なお、プログラム名の24toBinの名前の由来であるが、『24万局集』のデータを変換するのに使ったからである。

24toBin.cpp

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

#define EMPTY 0

#define FU 7
#define KY 6
#define KE 5
#define GI 4
#define KI 3
#define KA 2
#define HI 1
#define OU 8

#define EFU 7
#define EKY 6
#define EKE 5
#define EGI 4
#define EKI 3
#define EKA 2
#define EHI 1
#define EOU 8


int ban[82];
int hand[2][10];

int main(int argc, char *argv[])
{
  if (argc < 2) {
    printf("24toBin infile outfile\n");
    return 1;
  }
  unsigned char buff[512];

  FILE *src = fopen(argv[1], "rb");
  if (src == NULL) exit(2);
  FILE *dst = fopen(argv[2], "wb");
  if (dst == NULL) exit(2);

  int ct=0;
  while (fread(buff, 512, 1, src) > 0) {
    printf("%d局目…\n",++ct);
    int i;
    // 盤面初期化
    memset(hand,0,sizeof(hand));
    memset(ban,EMPTY,sizeof(ban));
    // 歩
    for(i=55;i<=63;i++) ban[i]=FU;
    for(i=19;i<=27;i++) ban[i]=EFU;
    // 香
    ban[73]=ban[81]=KY;
    ban[01]=ban[9]=EKY;
    // 桂
    ban[74]=ban[80]=KE;
    ban[02]=ban[8]=EKE;
    // 銀
    ban[75]=ban[79]=GI;
    ban[03]=ban[07]=EGI;
    // 金
    ban[76]=ban[78]=KI;
    ban[04]=ban[06]=EKI;
    // 角
    ban[71]=KA;
    ban[11]=EKA;
    // 飛
    ban[65]=HI;
    ban[17]=EHI;
    // 王
    ban[77]=OU;
    ban[05]=EOU;

    for (i = 0; i < 256; i ++) {
      int f=buff[i*2+1];
      int t=buff[i*2];
      if (f==0 || t==0) break;

      int ff=f;
      int tt=t;

//      printf("%3d %3d\t",f,t);


      if (f>100) {
        // 持ち駒を使う手
        // 飛車、角、金、銀、桂、香、歩の枚数?
        for(int j=1,sum=hand[i%2][j];sum<f-100;sum+=hand[i%2][++j]) {
//          printf("j=%d sum=%d",j,sum);
        }
        // jが 1:飛車 2:角 3:金 4:銀 5:桂 6:香 7:歩
        buff[i*2+1]=100+(8-j);
        ban[t]=j;
        hand[i%2][j]--;
//        printf("%d%d%s打",tt%9,(tt-1)/9+1,"  \0飛\0角\0金\0銀\0桂\0香\0歩\0"+j*3);
      } else {
        if (tt>100) tt-=100;
//        printf("%1d%1d%1d%1d%s(%s)",ff%9,(ff-1)/9+1,tt%9,(tt-1)/9+1,"  \0飛\0角\0金\0銀\0桂\0香\0歩\0"+ban[f]*3,"  \0飛\0角\0金\0銀\0桂\0香\0歩\0"+ban[t]*3);
//        if (t>100) {
//          printf("成");
//        }

        if (t>100) t=t-100;
        hand[i%2][ban[t]]++;
        ban[t]=ban[f];
        ban[f]=EMPTY;
      }

//      printf("\n");
//      printf("先手:飛車 %d 角 %d 金 %d 銀 %d 桂 %d 香 %d 歩 %d\n",hand[0][1],hand[0][2],hand[0][3],hand[0][4],hand[0][5],hand[0][6],hand[0][7]);
//      printf("後手:飛車 %d 角 %d 金 %d 銀 %d 桂 %d 香 %d 歩 %d\n",hand[1][1],hand[1][2],hand[1][3],hand[1][4],hand[1][5],hand[1][6],hand[1][7]);
    }
    fwrite(buff, 512, 1, dst);
  }
  fclose(src);
  fclose(dst);
  return 0;
}

さて、「棋泉」も「将棋道場」もフリーで手に入るので、定跡データはこれらに手で入力していくことでいくらでも大きくできる。

また、棋泉形式でプロ棋士の棋譜データが発売されているので、それらを購入してもよいだろう。※9

※9 たとえば、「順位戦パーフェクトデータ」や、「升田幸三全局集」「竜王戦パーフェクトデータ」「24万局集」などが棋泉形式のデータを含んでいる。基本的に㈱棋泉が作成するデータ集は、ビューアは棋泉そのもので、棋泉形式のデータを含んでいるようである。

ただし、フリーソフトとして配布する場合などには、棋譜データの著作権に注意する必要があるので、できれば自分で定跡書などから入力したほうが好ましい。

一般に配布せず、世界コンピュータ将棋選手権に参加する場合などは、個人的な利用の範囲を超えていないと解釈できる。

定跡通りに指す(定跡データから同じ局面を見付け、そこでの指し手を返す)

定跡データが出来たところで、定跡データから同じ局面を見付けて、そこでの指し手を返すルーチンが必要になる。

定跡データを手数ぶんだけ並べて見て、現在の局面と一致していたら、そこでの指し手を返す差し手に加えていく、というような簡単なプログラムでよいので、書き下してみる。

kyokumen.h 追加分

class Joseki {
  unsigned char **JosekiData;  // 定跡の棋譜データ
  int JosekiSize;    // この定跡のサイズ(棋譜が何枚あるか)
  Joseki *child;    // 定跡ファイルを複数選択した場合、子クラスが別の定跡ファイルを持っている。
public:
  // 初期化。filenamesには、,区切りで複数のファイル名を与えることが出来る。
  Joseki(char *filenames);
  // shokiは、初期局面。(駒落ちの場合などに対応するため。)
  // 現在の局面kが定跡データ内にあった場合、その局面でどんな手がどれくらいの頻度で指されたかを返す。
  // ただし、自分自身の定跡内で見つけた場合は、childまで定跡を参照はしない。
  // これにより、優先して指させたい定跡をファイルの先頭に置き、統括的な定跡を最後に置くことで、
  // ある指し方を優先しつつ、その定跡から外れた場合に大きな定跡でカバーすることが行える。
  void fromJoseki(Kyokumen &shoki,int shokiTeban,Kyokumen &k,int tesu,int &teNum,Te te[],int hindo[]); 
};

Joseki.cpp

#include "kyokumen.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

Joseki::Joseki(char *filenames)
{
  // filenamesは、,区切りとする。
  // JosekiDataは1エントリー512バイトとする。
  // JosekiSizeはファイルの長さ/512となる。
  char *filename=filenames;
  char *nextfile=strchr(filenames,',');
  if (nextfile!=NULL) {
    *nextfile='\0';  // ,を\0で置き換え
    nextfile++;    // 次のファイル名の先頭へ
    child=new Joseki(nextfile);
  } else {
    child=NULL;
  }
  FILE *fp=fopen(filename,"rb");
  JosekiSize=0;
  if (fp!=NULL) {
    for(;;) {
      char buf[512];
      if (fread(buf,1,512,fp)<=0) break;
//      JosekiData[JosekiSize]=(unsigned char*)malloc(512);
//      memcpy(JosekiData[JosekiSize],buf,512);
      JosekiSize++;
    }
    JosekiData=(unsigned char **)malloc(sizeof(unsigned char *)*JosekiSize);
    fseek(fp,0,SEEK_SET);
    for(int i=0;i<JosekiSize;i++) {
      JosekiData[i]=(unsigned char*)malloc(512);
      if (fread(JosekiData[i],1,512,fp)<=0) break;
    }
    fclose(fp);
  }
}

void Joseki::fromJoseki(Kyokumen &shoki,int shokiTeban,Kyokumen &k,int tesu,int &teNum,Te te[],int hindo[])
{
  teNum=0;
  int i;
  for(i=0;i<JosekiSize;i++) {
    Kyokumen kk(shoki);
    int teban=shokiTeban;
    int j;
    for (j=0;j<tesu;j++) {
      if (JosekiData[i][j*2]==0 || JosekiData[i][j*2]==0xff) break;
      Te te=Te(teban,JosekiData[i][j*2+1],JosekiData[i][j*2],kk);
      kk.Move(teban,te);
      if (teban==ENEMY) {
        teban=SELF;
      } else {
        teban=ENEMY;
      }
    }
    if (j==tesu && k==kk) {
      Te ret=Te(teban,JosekiData[i][j*2+1],JosekiData[i][j*2],kk);
      if (JosekiData[i][j*2]==0 || JosekiData[i][j*2]==0xff) continue;
      int l;
      for(l=0;l<teNum;l++) {
        if (ret==te[l]) {
          hindo[l]++;
          break;
        }
      }
      if (l==teNum) {
        te[l]=ret;
        hindo[l]=1;
        teNum++;
      }
    }
  }
  if (child!=NULL && teNum==0) {
    child->fromJoseki(shoki,shokiTeban,k,tesu,teNum,te,hindo);
  }
}

ここまでのプログラムは、「chapter07-01」に入れた。定跡データがあれば、序盤で今までよりもずっと将棋らしい指し方をするので、試してほしい。

また、これで定跡から外れた際にも、それなりにそれっぽい駒組みを進めるようになったはずだ。

中終盤での評価関数の問題

中終盤での駒の評価

すでにプログラム同士の対戦などの動きでお分かりかと思うが、中終盤では、相手の玉を詰ませることが目的で、駒得(こまどく)はそれほど重視しても仕方がない(かと言って、まったく無視できるものでもないが)。むしろ、駒の働き-自分の金銀が自玉をしっかり守っているか、相手の玉に攻撃の駒が迫っているか、といった点を重視しなければならない。

そこで、終盤においては、金銀及び成り駒の価値について、相手の玉との位置関係、自分の玉との位置関係によって、評価点を調整するという手法が山下氏の「YSS」の実装で提案されている。(参考文献:コンピュータ将棋の進歩2)

後手の玉に対する先手の駒の相対位置による増減割合、及び 先手の玉に対する後手の駒の相対位置による増減割合。
X軸については対称である。

ここでは、参考文献の表をそのまま借用して、金銀および成り駒の評価点を調整してみよう。

中終盤の判定であるが、序盤から中盤、終盤と、ある程度滑らかにつながる必要がある※10。そこで、以下のパラメータを用いた関数を使うことにする。

※10 そうでないと、先読みの途中で突然終盤になったためにおかしな手を指すなどの現象が起きてくる

この関数の返す値を、「終盤度」と呼ぶことにする。「終盤度」は、先手・後手について独立させ、どちらか片方だけ終盤になってしまっているような状態を正しく評価できるように考えてみた。

評価の基準には、以下の項目を使うことにした。

・お互いの歩以外の持ち駒
・お互いの相手陣4段目以内に侵入した駒

同じ駒の侵入でも、龍の侵入と金銀の侵入ではかなり危険度が違ってくると思われるので、評価にはそれらを加味するようにプログラムしてみた。

また、持ち駒でも、歩を持たれているのと飛車を持たれているのでは危険度がまったく違うので、これらも評価に加えてみることにする。

さらに欲を言えば、以下のようなことも評価できるとなおよいだろう。

・玉の危険度
  玉の周囲に敵の利きがどの程度あるか、敵の駒がどの程度の距離に迫っているか
・自陣の危険度
  自陣に相手の駒がどの程度近づいているか、自陣に相手の駒が打ち込まれているか

ただし、プログラム例では、上記2点は評価していない。これらは読者の課題としてほしい。

これらを評価しない場合の問題点としては、たとえば居飛車対振り飛車の終盤で、自玉頭に香車を打って相手の玉を責めるような手が相手の終盤度をあげない(かつ自分の終盤度を下げない)ということが挙げられる。※9

※9 もっとも、これらをやろうとすると1手指すごとの差分計算が面倒になると思うが、できないことはないだろう。

一方だけ終盤で、もう一方はまったく攻められていないといった事態も考えられる。

このような場合には、攻められていないほうの守りの金銀には特に加点をせず、攻められているほうの守りの金銀には上記の表による加点をしたうえで、一方的に攻められているということよる適当なペナルティーを与えることにする。こうすることで、一方的に攻められることを嫌い、攻め合いを好むようになるはずだ。

実際のプログラム

先ほどのKyokumenKomagumiクラスに、「終盤度」と「終盤度による駒の位置の価値」を実装してみよう。

終盤度は0~16の値を取る値で、先の終盤度計算関数で求める。

KyokumenKomagumiクラスでの駒の位置による得点を、「終盤度*終盤での駒の位置による価値/16」とすることで、序盤から終盤を滑らかにつなぐことができる。※11

※ 11 と言っても0から16の17段階で、あまり細かくないのだが。

さらに、先手・後手で別の終盤度をもたせることで、先手の駒の位置による価値は、「先手の終盤度*守り駒の位置による価値/16」と「後手の終盤度*終盤での攻め駒の位置による価値/16」とすることにする。

同様に、後手の駒の位置による価値は、「後手の終盤度*守り駒の位置による価値/16」と「先手の終盤度*攻め駒の位置による価値/16」の和とする。

これで、「先手だけ終盤」状態になった場合には、後手は先手の玉に迫る手を選びやすくなるはずで、「先手・後手ともに終盤」状態になった場合には、駒組みによる価値に拘らず、「玉に迫る手」「玉に迫るのを防ぐ手」を指すはずである。※12

※12と言っても、この辺りで述べていることは机上の空論で、本格的な調整を経ていないものである。真に実戦的な処理は、実際の指し手を見ながら読者に考えてみてほしい。

2015年10月追記:上記、机上の空論でした…。そっぽに「と金」を作る手がやたらと好きになってしまっていましたorz

kyokumen.h 追加・変更分

class KyokumenKomagumi: public Kyokumen {
public:
  static int KomagumiValue[ERY+1][16*11];
  // 攻め駒、守り駒の相手玉・自玉との相対位置による評価点
  static int SemegomaValueS[16*11][16*11];
  static int SemegomaValueE[16*11][16*11];
  static int MamorigomaValueS[16*11][16*11];
  static int MamorigomaValueE[16*11][16*11];
public:
  // 終盤度:序盤から終盤に向かって上がっていく数値。大きいほど詰みが近い(はず)
  int Shuubando[2];
  // 駒組みによるボーナス点
  int KomagumiBonus[2];
  // 攻め駒、守り駒によるボーナス点
  int SemegomaBonus[2];
  int MamorigomaBonus[2];

  KyokumenKomagumi(int tesu,KomaInf ban[9][9],int Motigoma[]) : Kyokumen(tesu,ban,Motigoma) {
    InitShuubando();
    InitBonus();
  }
  void Initialize();
  static void InitKanagomaValue();
  void InitShuubando();
  void InitBonus();
  void SenkeiInit();
  void Move(int SorE,const Te &te);
  void narabe(int tesu,const char *t[]);
  void Print();
  // 今までのvalueの代わり。
  int Evaluate();
};

kyokumen.cpp 追加・変更分

:
int KyokumenKomagumi::SemegomaValueS[16*11][16*11];
int KyokumenKomagumi::SemegomaValueE[16*11][16*11];
int KyokumenKomagumi::MamorigomaValueS[16*11][16*11];
int KyokumenKomagumi::MamorigomaValueE[16*11][16*11];
:
static int Mamorigoma[17][9]={
  { 50, 50, 50, 50, 50, 50, 50, 50, 50},
  { 56, 52, 50, 50, 50, 50, 50, 50, 50},
  { 64, 61, 55, 50, 50, 50, 50, 50, 50},
  { 79, 77, 70, 65, 54, 51, 50, 50, 50},
  {100, 99, 95, 87, 74, 58, 50, 50, 50},
  {116,117,101, 95, 88, 67, 54, 50, 50},
  {131,129,124,114, 90, 71, 59, 51, 50},
  {137,138,132,116, 96, 76, 61, 53, 50},
  {142,142,136,118, 98, 79, 64, 52, 50},
  {132,132,129,109, 95, 75, 60, 51, 50},
  {121,120,105, 97, 84, 66, 54, 50, 50},
  { 95, 93, 89, 75, 68, 58, 51, 50, 50},
  { 79, 76, 69, 60, 53, 50, 50, 50, 50},
  { 64, 61, 55, 51, 50, 50, 50, 50, 50},
  { 56, 52, 50, 50, 50, 50, 50, 50, 50},
  { 50, 50, 50, 50, 50, 50, 50, 50, 50},
  { 50, 50, 50, 50, 50, 50, 50, 50, 50},
};

static int Semegoma[17][9]={
  { 50, 50, 50, 50, 50, 50, 50, 50, 50},
  { 50, 50, 50, 50, 50, 50, 50, 50, 50},
  { 50, 50, 50, 50, 50, 50, 50, 50, 50},
  { 54, 53, 51, 51, 50, 50, 50, 50, 50},
  { 70, 66, 62, 55, 53, 50, 50, 50, 50},
  { 90, 85, 80, 68, 68, 60, 53, 50, 50},
  {100, 97, 95, 85, 84, 71, 51, 50, 50},
  {132,132,129,102, 95, 71, 51, 50, 50},
  {180,145,137,115, 91, 75, 57, 50, 50},
  {170,165,150,121, 94, 78, 58, 52, 50},
  {170,160,142,114, 98, 80, 62, 55, 50},
  {140,130,110,100, 95, 75, 54, 50, 50},
  {100, 99, 95, 87, 78, 69, 50, 50, 50},
  { 80, 78, 72, 67, 55, 51, 50, 50, 50},
  { 62, 60, 58, 52, 50, 50, 50, 50, 50},
  { 50, 50, 50, 50, 50, 50, 50, 50, 50},
  { 50, 50, 50, 50, 50, 50, 50, 50, 50},
};

void KyokumenKomagumi::InitKanagomaValue()
{
  for(int kingSdan=1;kingSdan<=9;kingSdan++) {
    for(int kingSsuji=0x10;kingSsuji<=0x90;kingSsuji+=0x10) {
      for(int kingEdan=1;kingEdan<=9;kingEdan++) {
        for(int kingEsuji=0x10;kingEsuji<=0x90;kingEsuji+=0x10) {
          for(int suji=0x10;suji<=0x90;suji+=0x10) {
            for(int dan=1;dan<=9;dan++) {
              int DiffSujiS=abs(kingSsuji-suji)/0x10;
              int DiffSujiE=abs(kingEsuji-suji)/0x10;
              int DiffDanSS=8+(dan-kingSdan);
              int DiffDanES=8+(dan-kingEdan);
              int DiffDanSE=8+(-(dan-kingSdan));
              int DiffDanEE=8+(-(dan-kingEdan));
              int kingS=kingSsuji+kingSdan;
              int kingE=kingEsuji+kingEdan;

              SemegomaValueS[suji+dan][kingE]=Semegoma[DiffDanES][DiffSujiE]-100;
              MamorigomaValueS[suji+dan][kingS]=Mamorigoma[DiffDanSS][DiffSujiS]-100;
              SemegomaValueE[suji+dan][kingS]=-(Semegoma[DiffDanSE][DiffSujiS]-100);
              MamorigomaValueE[suji+dan][kingE]=-(Mamorigoma[DiffDanEE][DiffSujiE]-100);
            }
          }
        }
      }
    }
  }
}
            //空歩香桂銀金角飛王と杏圭全金馬龍
int ShuubandoByAtack[]={0,1,1,2,3,3,3,4,4,3,3,3,3,3,4,5};
            //空歩香桂銀 金角飛王 と 杏 圭 全 金 馬 龍
int ShuubandoByDefence[]={0,0,0,0,-1,-1,0,-1,0,-1,-1,-1,-1,-1,-2,-1};
          //空歩香桂銀金角飛王と杏圭全金馬龍
int ShuubandoByHand[]={0,0,1,1,2,2,2,3,0,0,0,0,0,0,0,0};

void KyokumenKomagumi::InitShuubando()
{
  // 終盤度を求めると同時に、終盤度によるボーナスの付加、駒の加点も行う。
  int suji,dan;
  Shuubando[0]=0;
  Shuubando[1]=0;
  for(suji=0x10;suji<=0x90;suji+=0x10) {
    for(dan=1;dan<=4;dan++) {
      if (ban[suji+dan] & SELF) {
        Shuubando[1]+=ShuubandoByAtack[ban[suji+dan] & ~SELF];
      }
      if (ban[suji+dan] & ENEMY) {
        Shuubando[1]+=ShuubandoByDefence[ban[suji+dan] & ~ENEMY];
      }
    }
    for(dan=6;dan<=9;dan++) {
      if (ban[suji+dan] & ENEMY) {
        Shuubando[0]+=ShuubandoByAtack[ban[suji+dan] & ~ENEMY];
      }
      if (ban[suji+dan] & SELF) {
        Shuubando[0]+=ShuubandoByDefence[ban[suji+dan] & ~SELF];
      }
    }
  }
  int koma;
  for(koma=FU;koma<=HI;koma++) {
    Shuubando[0]+=ShuubandoByHand[koma]*Hand[ENEMY|koma];
    Shuubando[1]+=ShuubandoByHand[koma]*Hand[SELF|koma];
  }
}

int IsKanagoma[]={
//  空空空空空空空空空空空空空空空空空歩香桂銀金角飛王と杏圭全金馬龍
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1,1,1,1,1,0,0,
//  空歩香桂銀金角飛王と杏圭全金馬龍壁空空空空空空空空空空空空空空空
  0,0,0,0,1,1,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
};

void KyokumenKomagumi::InitBonus()
{
  int suji,dan;
  SemegomaBonus[0]=SemegomaBonus[1]=0;
  MamorigomaBonus[0]=MamorigomaBonus[1]=0;

  for(suji=0x10;suji<=0x90;suji+=0x10) {
    for(dan=1;dan<=9;dan++) {
      if (IsKanagoma[ban[suji+dan]]) {
        if (ban[suji+dan] & SELF) {
          SemegomaBonus[0]+=SemegomaValueS[suji+dan][kingE];
          MamorigomaBonus[0]+=MamorigomaValueS[suji+dan][kingS];
        } else {
          SemegomaBonus[1]+=SemegomaValueE[suji+dan][kingS];
          MamorigomaBonus[1]+=MamorigomaValueE[suji+dan][kingE];
        }
      }
    }
  }
}

void KyokumenKomagumi::Move(int SorE,const Te &te)
{
  int self,enemy;
  if (SorE==SELF) {
    self=0;
    enemy=1;
  } else {
    self=1;
    enemy=0;
  }
  if (te.koma==OU || te.koma==EOU) {
    // 駒を動かした後で、全面的にBonusの計算しなおしが必要。
    // Semegoma,Mamorigomaなど全て変わるので。
  } else {
    if (IsKanagoma[te.koma] && te.from>0) {
      if (SorE==SELF) {
        SemegomaBonus[0]-=SemegomaValueS[te.from][kingE];
        MamorigomaBonus[0]-=MamorigomaValueS[te.from][kingS];
      } else {
        SemegomaBonus[1]-=SemegomaValueE[te.from][kingS];
        MamorigomaBonus[1]-=MamorigomaValueE[te.from][kingE];
      }
    }
    if (te.capture) {
      if (IsKanagoma[te.capture]) {
        if (SorE==SELF) {
          SemegomaBonus[1]-=SemegomaValueE[te.to][kingS];
          MamorigomaBonus[1]-=MamorigomaValueE[te.to][kingE];
        } else {
          SemegomaBonus[0]-=SemegomaValueS[te.to][kingE];
          MamorigomaBonus[0]-=MamorigomaValueS[te.to][kingS];
        }
      }
    }
    if (!te.promote) {
      if (IsKanagoma[te.koma]) {
        if (SorE==SELF) {
          SemegomaBonus[0]+=SemegomaValueS[te.to][kingE];
          MamorigomaBonus[0]+=MamorigomaValueS[te.to][kingS];
        } else {
          SemegomaBonus[1]+=SemegomaValueE[te.to][kingS];
          MamorigomaBonus[1]+=MamorigomaValueE[te.to][kingE];
        }
      }
    } else {
      if (IsKanagoma[te.koma|PROMOTED]) {
        if (SorE==SELF) {
          SemegomaBonus[0]+=SemegomaValueS[te.to][kingE];
          MamorigomaBonus[0]+=MamorigomaValueS[te.to][kingS];
        } else {
          SemegomaBonus[1]+=SemegomaValueE[te.to][kingS];
          MamorigomaBonus[1]+=MamorigomaValueE[te.to][kingE];
        }
      }
    }
  }
  KomagumiBonus[self]-=KomagumiValue[te.koma][te.from];
  if (te.from>0 && (te.from&0x0f)<=4) {
    // 4段目以下・終盤度の計算
    if (SorE==SELF) {
      Shuubando[1]-=ShuubandoByAtack[te.koma & ~SELF];
    } else {
      Shuubando[1]-=ShuubandoByDefence[te.koma & ~ENEMY];
    }
  }
  if (te.from>0 && (te.from&0x0f)>=6) {
    // 6段目以上・終盤度の計算
    if (SorE==SELF) {
      Shuubando[0]-=ShuubandoByDefence[te.koma & ~SELF];
    } else {
      Shuubando[0]-=ShuubandoByAtack[te.koma & ~ENEMY];
    }
  }
  if (te.from==0) {
    // 打つことによる終盤度の減少
    if (SorE==SELF) {
      Shuubando[1]-=ShuubandoByHand[te.koma&~SELF];
    } else {
      Shuubando[0]-=ShuubandoByHand[te.koma&~ENEMY];
    }
  }
  if (te.capture) {
    if ((te.to&0x0f)<=4) {
      // 4段目以下・終盤度の計算
      if (SorE==SELF) {
        Shuubando[1]-=ShuubandoByDefence[te.capture & ~ENEMY];
      } else {
        Shuubando[1]-=ShuubandoByAtack[te.capture & ~SELF];
      }
    }
    if ((te.to&0x0f)>=6) {
      // 6段目以上・終盤度の計算
      if (SorE==SELF) {
        Shuubando[0]-=ShuubandoByAtack[te.capture & ~ENEMY];
      } else {
        Shuubando[0]-=ShuubandoByDefence[te.capture & ~SELF];
      }
    }
    // Handに入ったことによる終盤度の計算
    if (SorE==SELF) {
      Shuubando[1]+=ShuubandoByHand[te.capture&~ENEMY&~PROMOTED];
    } else {
      Shuubando[0]+=ShuubandoByHand[te.capture&~SELF&~PROMOTED];
    }
    KomagumiBonus[enemy]-=KomagumiValue[te.capture][te.to];
  }
  if (!te.promote) {
    if ((te.to&0x0f)<=4) {
      // 4段目以下・終盤度の計算
      if (SorE==SELF) {
        Shuubando[1]+=ShuubandoByAtack[te.koma & ~SELF];
      } else {
        Shuubando[1]+=ShuubandoByDefence[te.koma & ~ENEMY];
      }
    }
    if ((te.to&0x0f)>=6) {
      // 6段目以上・終盤度の計算
      if (SorE==SELF) {
        Shuubando[0]+=ShuubandoByDefence[te.koma & ~SELF];
      } else {
        Shuubando[0]+=ShuubandoByAtack[te.koma & ~ENEMY];
      }
    }
    KomagumiBonus[self]+=KomagumiValue[te.koma][te.to];
  } else {
    if ((te.to&0x0f)<=4) {
      // 4段目以下・終盤度の計算
      if (SorE==SELF) {
        Shuubando[1]+=ShuubandoByAtack[(te.koma|PROMOTED) & ~SELF];
      } else {
        Shuubando[1]+=ShuubandoByDefence[(te.koma|PROMOTED) & ~ENEMY];
      }
    }
    if ((te.to&0x0f)>=6) {
      // 6段目以上・終盤度の計算
      if (SorE==SELF) {
        Shuubando[0]+=ShuubandoByDefence[(te.koma|PROMOTED) & ~SELF];
      } else {
        Shuubando[0]+=ShuubandoByAtack[(te.koma|PROMOTED) & ~ENEMY];
      }
    }
    KomagumiBonus[self]+=KomagumiValue[te.koma|PROMOTED][te.to];
  }
  Kyokumen::Move(SorE,te);
  if (te.koma==SOU || te.koma==EOU) {
    // 全面的に金駒のBonusの計算しなおし。
    InitBonus();
  }
}

int KyokumenKomagumi::Evaluate()
{
  // 終盤度を0~16の範囲に補正する。
  int Shuubando0,Shuubando1;

  if (Shuubando[0]<0) {
    Shuubando0=0;
  } else if (Shuubando[0]>16) {
    Shuubando0=16;
  } else {
    Shuubando0=Shuubando[0];
  }
  if (Shuubando[1]<0) {
    Shuubando1=0;
  } else if (Shuubando[1]>16) {
    Shuubando1=16;
  } else {
    Shuubando1=Shuubando[1];
  }

  // 終盤度の差を評価する。とりあえず、1違うと200点違うことにする。
  int ret=(Shuubando1-Shuubando0)*200;

  ret+=SemegomaBonus[0]*Shuubando1/16;
  ret+=MamorigomaBonus[0]*Shuubando0/16;
  ret+=SemegomaBonus[1]*Shuubando0/16;
  ret+=MamorigomaBonus[1]*Shuubando1/16;

  ret+=KomagumiBonus[0]+KomagumiBonus[1];

//  printf("ret=%d\n",ret);
  if (abs(ret)>10000) {
    Print();
    printf("ret=%d\n",ret);
  }

  // 最後に、駒得の点数と合わせて評価値とする。
  return ret+value;
//  return value;
}

void KyokumenKomagumi::Print()
{
  Kyokumen::Print();
  printf("終盤度 先手 %2d 後手 %2d\n",Shuubando[0],Shuubando[1]);
}

ここまでのプログラムは、「chapter07-02」に入れてあるので、確認してほしい。


前ページ
次ページ
索引ページへ