将棋のルールを*完全に*把握している人は意外に少ない。かく言う筆者も、将棋プログラムの作成に当たり、改めて文献にあたった口である。ここでは、日本将棋連盟発行の「将棋ガイドブック」に沿って完全なルールを説明する。
将棋は、二人で行なうゲームで、将棋を行なうことを「将棋を指す」「対局する」のように言う。また、将棋は交互に盤の上で駒を動かす「指し手」を指すことで対局が進む。
駒には、それぞれ、動かし方のルールがあり、そのルールに沿って駒を動かさねばならない。対局は、どちらかが負けを認めること=「投了する」か、指し手を指さなければいけない番のプレイヤーが合法的な指し手がなくなった時(この後に記述する、ルールに沿った指し手が一つもできない場合)、そのプレイヤーの負けとなる。
また、合法的でない手を指した場合、反則となり、負けとなる。通常、合法的な指し手がなくなるケースは、放置すると次に王を取りますよという手=「王手」をかけられて、どんな指し手を指しても王を取られてしまう状態になることで、これを「詰み」という。
例外的なケースとして、対局中に同一局面が4回現れた場合、「千日手」となる。千日手の場合、通常引き分けとなるが、後述する連続王手の千日手については、連続王手をかけている側の負けとなる。
また、双方の玉が相手陣内に入り(入玉という)、玉を0点、飛車角を5点、その他の駒を1点として数えた時、双方が24点以上持っている場合、「持将棋」となり、引き分けとなる。
盤の縦方向を「筋」、横方向を「段」と呼ぶ。筋は右から「1、2、3…9筋」まで算用数字であらわす。段は、上から「一、二、三、…九段」までで、漢数字であらわす。
一つの升目を表すのに、たとえば3筋の7段目ならば、「3七」のように、算用数字と漢数字を組み合わせてあらわす。(図1)
持ち駒の有無および種類、その枚数は、図面の右側に自分の持ち駒、相手側から見た右側=左側に相手の持ち駒を記述する。持ち駒がない場合には、「なし」と記述する。(図2)
自分から見て手前の三段を自陣といい、向こう側の三段を相手陣と呼ぶ。(図3)自分の駒が相手陣に入ったときには、成る(駒を裏返す)ことが出来る。また、自分の駒が相手陣内からいずれかへ動くときにも成ることが出来る。
相手陣に一度入った駒であっても、相手陣から出たら、再度相手陣に入るまで成ることは出来ないので注意。
将棋には、いくつかルール上指してはいけない手が存在する。プログラム作成上、これらのルールは正確に把握しておかないとならない。
プログラムを作るのに必ずしも必要なわけではないが、将棋の常識として覚えておきたい。
プロの棋譜を新聞などから入力する際にも、この常識を知らないと入力できないという事情もある。
2015年10月追記:連盟から出ている棋譜を見ると、入り組んだ場合だけではなく、単にまっすぐ上に進む手で、二つ以上の駒が移動可能な場合には、必ず「直」と表記しているようである。
なお、本書のプログラムでは、簡単のため、棋譜の表記法に従う代わりに、手の表示の際に、移動先の他に移動元を併記することにし、駒打ちの際には必ず「打」を表記するものとした。
これは、局面を参照しないと手の表示が出来なくなるという面倒な自体を避けるための処置である。もちろん、手が指される時の局面を参照すれば、正しい棋譜表記に対応することも可能なので、腕に自信のある方は挑戦してみて欲しい。
なにもないところ 0 歩 1 香車 2 桂馬 3 銀 4 金 5 角 6 飛車 7 玉 8 と 9(1+8) 成香 10(2+8) 成桂 11(3+8) 成銀 12(4+8) 馬 14(6+8) 龍 15(7+8) 上向き(味方)の駒につける目印 16 下向き(敵)の駒につける目印 32
ソースで表すと、以下のようになる。
enum { // 何もないところ EMPTY=0, EMP=0, // 3文字も準備しておくとソースが見やすいので(笑) // 成り駒につける目印(1ビット) PROMOTED=1<<3, // 駒をあらわす数値 FU=1, KY=2, KE=3, GI=4, KI=5, KA=6, HI=7, OU=8, TO=FU+PROMOTED, NY=KY+PROMOTED, NK=KE+PROMOTED, NG=GI+PROMOTED, UM=KA+PROMOTED, RY=HI+PROMOTED, // 自分自身の駒につける目印(1ビット) SELF=1<<4, // 敵の駒につける目印(1ビット) ENEMY=1<<5, // 敵も味方も進めないところ(盤の外)の目印 WALL=SELF+ENEMY, // 実際の駒 SFU=SELF+FU, //味方の歩 STO=SELF+TO, //味方のと金 SKY=SELF+KY, //味方の香車 SNY=SELF+NY, //味方の成り香 SKE=SELF+KE, //味方の桂馬 SNK=SELF+NK, //味方の成り桂 SGI=SELF+GI, //味方の銀 SNG=SELF+NG, //味方の成り銀 SKI=SELF+KI, //味方の金 SKA=SELF+KA, //味方の角 SUM=SELF+UM, //味方の馬 SHI=SELF+HI, //味方の飛車 SRY=SELF+RY, //味方の龍 SOU=SELF+OU, //味方の玉 EFU=ENEMY+FU, //敵の歩 ETO=ENEMY+TO, //敵のと金 EKY=ENEMY+KY, //敵の香車 ENY=ENEMY+NY, //敵の成り香 EKE=ENEMY+KE, //敵の桂馬 ENK=ENEMY+NK, //敵の成り桂 EGI=ENEMY+GI, //敵の銀 ENG=ENEMY+NG, //敵の成り銀 EKI=ENEMY+KI, //敵の金 EKA=ENEMY+KA, //敵の角 EUM=ENEMY+UM, //敵の馬 EHI=ENEMY+HI, //敵の飛車 ERY=ENEMY+RY, //敵の龍 EOU=ENEMY+OU, //敵の玉 };
また、駒自体は1バイトで表記可能なため、KomaInfという型を以下のように定義した。
typedef unsigned char KomaInf;
次に局面であるが、局面は盤面の状態と持ち駒の状態とからなる。(正確には、千日手などへの対応のため、過去の情報が必要であるが、簡単のため、ここでは触れないことにする。)
盤面は、2次元配列で表すのが簡単であるが、高速化のためには、1次元配列として、演算としては*16など2の階乗倍が使えることが望ましい。もう一つ、1次元配列とすることのメリットであるが、方向や移動を一つの数字で表せる(2次元配列なら筋と段の両方を持ったり、斜めに動くときには筋が-1、段が-1のようにもたなければならない。これが1次元配列なら、位置は一つの数字で表せるし、上記のように斜めに動く場合は、16*-1+(-1)=-17という一つの数字で表すことが出来る。)また、盤外として、盤外のコードが書き込まれていると処理の上で都合がよい。そこで、盤面は以下のように表すものとした。
KomaInf banpadding[16]; KomaInf ban[16*(9+2)];
banpaddingは、桂馬の利きが盤外のさらに外にはみ出すことを考慮して設けてある。banpaddingの中は、常にWALLである。
持ち駒は、自分の駒、相手の駒がそれぞれ何枚あるかを表す1次元配列とした。1次元配列は、最大で相手の飛車までが持ち駒となるので、大きさはEHI+1までが必要である。(成り駒がそのまま持ち駒になることはない。また、王を持ち駒にすることもない。王を取れる局面は、反則により自分の勝ちの局面である。)例を挙げると、Hand[SHI]が自分の飛車の持ち駒の枚数、Hand[EKI]が相手の金の持ち駒の枚数である。
int Hand[EHI+1];
以上は、グローバル変数とせず、Kyokumenクラスのメンバーとした。
次に、各駒の動きを、通常の動き(隣接したマスへの移動)と飛び(隣接したマスおよびさらにその向こうへ飛んで動ける移動)とに分類した。なお、桂馬飛びは、通常の動き(隣接したマスへの移動)と同様に扱う。これは、桂馬の動きは、途中の駒により遮られることがないからである。
動きとしては、8方向+桂馬による4方向で合計12方向が必要であることがわかる。
12方向を表にしたものが以下である。(この「方向」を1つの数字であらわせることが先も述べたように1次元配列にすることのメリットの一つである。)
// 方向を示す定数。 int Direct[12]={ 17,1,-15,16,-16,15,-1,-17,14,-18,18,-14 };
さらに、これを各駒の動きの表にしたものが以下となる。
// その方向に動けるか?その方向に飛んで動くものは入れてはいけない。 int CanMove[12][64]={ { // Direct[0]=17, // |/ //  ̄ // 空空空空空空空空空空空空空空空空空歩香桂銀金角飛王と杏圭全金馬龍 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,1, // 空歩香桂銀金角飛王と杏圭全金馬龍壁空空空空空空空空空空空空空空空 0,0,0,0,1,1,0,0,1,1,1,1,1,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 },{ // Direct[0]=1, // ↓ // 空空空空空空空空空空空空空空空空空歩香桂銀金角飛王と杏圭全金馬龍 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,1,1,1,1,1,1,0, // 空歩香桂銀金角飛王と杏圭全金馬龍壁空空空空空空空空空空空空空空空 0,1,0,0,1,1,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 },{ // Direct[0]=-15, // \| //  ̄ // 空空空空空空空空空空空空空空空空空歩香桂銀金角飛王と杏圭全金馬龍 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,1, // 空歩香桂銀金角飛王と杏圭全金馬龍壁空空空空空空空空空空空空空空空 0,0,0,0,1,1,0,0,1,1,1,1,1,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 },{ // Direct[0]=16, // ← // 空空空空空空空空空空空空空空空空空歩香桂銀金角飛王と杏圭全金馬龍 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,1,1,1,1,1,1,0, // 空歩香桂銀金角飛王と杏圭全金馬龍壁空空空空空空空空空空空空空空空 0,0,0,0,0,1,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 },{ // Direct[0]=-16, // → // 空空空空空空空空空空空空空空空空空歩香桂銀金角飛王と杏圭全金馬龍 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,1,1,1,1,1,1,0, // 空歩香桂銀金角飛王と杏圭全金馬龍壁空空空空空空空空空空空空空空空 0,0,0,0,0,1,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 },{ // Direct[0]=15, // __ // |\ // 空空空空空空空空空空空空空空空空空歩香桂銀金角飛王と杏圭全金馬龍 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,1,1,1,1,1,1,0,1, // 空歩香桂銀金角飛王と杏圭全金馬龍壁空空空空空空空空空空空空空空空 0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 },{ // Direct[0]=-1, // ↑ // 空空空空空空空空空空空空空空空空空歩香桂銀金角飛王と杏圭全金馬龍 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,1,0,0,1,1,1,1,1,1,1,0, // 空歩香桂銀金角飛王と杏圭全金馬龍壁空空空空空空空空空空空空空空空 0,0,0,0,0,1,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 },{ // Direct[0]=-17 // __ // /| // 空空空空空空空空空空空空空空空空空歩香桂銀金角飛王と杏圭全金馬龍 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,1,1,1,1,1,1,0,1, // 空歩香桂銀金角飛王と杏圭全金馬龍壁空空空空空空空空空空空空空空空 0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 },{ // Direct[0]=14,先手の桂馬 // 空空空空空空空空空空空空空空空空空歩香桂銀金角飛王と杏圭全金馬龍 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,0 },{ // Direct[0]=-18,先手の桂馬 // 空空空空空空空空空空空空空空空空空歩香桂銀金角飛王と杏圭全金馬龍 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,0 },{ // Direct[0]=-14,後手の桂馬 // 空空空空空空空空空空空空空空空空空歩香桂銀金角飛王と杏圭全金馬龍 0,0,0,0,0,0,0,0,0,0,0,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,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 },{ // Direct[0]=18,後手の桂馬 // 空空空空空空空空空空空空空空空空空歩香桂銀金角飛王と杏圭全金馬龍 0,0,0,0,0,0,0,0,0,0,0,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,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 } }; // その方向に飛んで動くことが出来るか? // 飛車角香車と龍と馬しかそういう駒はない int CanJump[12][64]={ // Direct[0]=17, // \| //  ̄ { // 空空空空空空空空空空空空空空空空空歩香桂銀金角飛王と杏圭全金馬龍 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0, // 空歩香桂銀金角飛王と杏圭全金馬龍壁空空空空空空空空空空空空空空空 0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 },{ // Direct[0]=1, // ↓ // 空空空空空空空空空空空空空空空空空歩香桂銀金角飛王と杏圭全金馬龍 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1, // 空歩香桂銀金角飛王と杏圭全金馬龍壁空空空空空空空空空空空空空空空 0,0,1,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 },{ // Direct[0]=-15, // \| //  ̄ // 空空空空空空空空空空空空空空空空空歩香桂銀金角飛王と杏圭全金馬龍 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0, // 空歩香桂銀金角飛王と杏圭全金馬龍壁空空空空空空空空空空空空空空空 0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 },{ // Direct[0]=16, // ← // 空空空空空空空空空空空空空空空空空歩香桂銀金角飛王と杏圭全金馬龍 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1, // 空歩香桂銀金角飛王と杏圭全金馬龍壁空空空空空空空空空空空空空空空 0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 },{ // Direct[0]=-16, // → // 空空空空空空空空空空空空空空空空空歩香桂銀金角飛王と杏圭全金馬龍 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1, // 空歩香桂銀金角飛王と杏圭全金馬龍壁空空空空空空空空空空空空空空空 0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 },{ // Direct[0]=15, // __ // |\ // 空空空空空空空空空空空空空空空空空歩香桂銀金角飛王と杏圭全金馬龍 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0, // 空歩香桂銀金角飛王と杏圭全金馬龍壁空空空空空空空空空空空空空空空 0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 },{ // Direct[0]=-1, // ↑ // 空空空空空空空空空空空空空空空空空歩香桂銀金角飛王と杏圭全金馬龍 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0,1, // 空歩香桂銀金角飛王と杏圭全金馬龍壁空空空空空空空空空空空空空空空 0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 },{ // Direct[0]=-17 // __ // /| // 空空空空空空空空空空空空空空空空空歩香桂銀金角飛王と杏圭全金馬龍 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0, // 空歩香桂銀金角飛王と杏圭全金馬龍壁空空空空空空空空空空空空空空空 0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 },{ // Direct[0]=14,先手の桂馬飛び // 空空空空空空空空空空空空空空空空空歩香桂銀金角飛王と杏圭全金馬龍 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 空歩香桂銀金角飛王と杏圭全金馬龍壁空空空空空空空空空空空空空空空 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 },{ // Direct[0]=-18,先手の桂馬飛び // 空空空空空空空空空空空空空空空空空歩香桂銀金角飛王と杏圭全金馬龍 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 空歩香桂銀金角飛王と杏圭全金馬龍壁空空空空空空空空空空空空空空空 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 },{ // Direct[0]=-14,後手の桂馬飛び // 空空空空空空空空空空空空空空空空空歩香桂銀金角飛王と杏圭全金馬龍 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 空歩香桂銀金角飛王と杏圭全金馬龍壁空空空空空空空空空空空空空空空 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 },{ // Direct[0]=18,後手の桂馬飛び // 空空空空空空空空空空空空空空空空空歩香桂銀金角飛王と杏圭全金馬龍 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 空歩香桂銀金角飛王と杏圭全金馬龍壁空空空空空空空空空空空空空空空 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 } };
非常に大きな表となったが、複雑なのはさらにこれからである。二歩や打ち歩詰め、その他の禁手を指さないように注意深くプログラムにする必要がある。他の禁手としては、駒の利きが盤上に存在しないようにする手、玉を取られる着手、連続王手の千日手、駒の動かし方の間違いがあげられる。
玉を取られる着手としては、①「王手がかかった場合には必ず避けないといけない」ことと、②「ある駒を移動したことで、角・飛車・香車などの飛び駒で王手がかかるようにしないといけない」ことの2種類が禁手として考えられる。
王手がかかった場合の対処は別途関数を作成して対応するが、飛び駒による王手がかからないようにするには、チェスにある「ピン」という概念を導入する。
「ピン」するとは、飛び駒の利きにより、相手の玉と自分の飛び駒の間の相手の駒の動きを制限することである。たとえば、図23では、7八の金が3八の飛車によりピンされているため、金は横にしか移動することができない。(例えば6七や7九に移動してしまうと、玉が飛車で取られてしまう。)
「ピン」という概念を導入するために、局面には、「利き」に関する情報と、玉の位置に関する情報も保持することとする。「利き」に関する情報は、以下のように保持する。
typedef unsinged int Kiki;
利きは、どの方向(Direct[n])から利いているか、「1<<nビット目」を立てて表わすことにする。また、飛び駒の利きは、「1<<(n+16)ビット目」を立てて表わす。
たとえば、図24における「1一」の地点の利きは、①飛車が左から利いているので「(1<<4+16)」+②桂馬が2三から利いているので「(1<<8)」+③歩が1二から利いているので「(1<<6)」となり、2進数で「10000000000010100000」、16進なら、「0x000800C0」となる。
Kiki controlS[16*11]; // 味方の駒の利き Kiki controlE[16*11]; // 敵の駒の利き int kingS; int kingE;
これらも局面クラスのメンバーである。
ピンを用いながら、駒を移動する手を生成するには、たとえば以下のようになる。
// pinが設定されていない、またはピンを同じか逆方向なら移動可能として手のリストに加える if(pin[i]==0 || pin==dir || pin==-dir) { teBuf[teNum++]=Te(from,from+dir,koma,ban[from+dir],0); }
手のクラスは、移動元、移動先、移動する駒、取った駒、成り・不成りを表せる必要がある。さらに、将来の拡張用として、手の種類と手の価値を加え、手クラスは以下のように定義した。
// 手のクラス class Te { public: // どこから・どこへはそれぞれ1Byteであらわせます。 // 詳しくは局面クラスを参照して下さい。 unsigned char from,to; // 動かした駒 KomaInf koma; // 取った駒 KomaInf capture; // 成/不成り unsigned char promote; // これは、手の生成の際に種別を用いたい時に使います(将来の拡張用) unsigned char Kind; // その手の仮評価(手の価値)です short value; public: inline bool IsNull() { return from==0 && to==0; } // Teの配列を宣言することが多いので、空のコンストラクタを用意します。 // こうすることで、Teの配列を宣言するたびに無駄な初期化処理が走ることを避けられます。 inline Te() {}; // 本当にTeを空のデータで初期化したい時のためのコンストラクタです。 inline Te(int i) { from=to=koma=capture=promote=Kind=0; value=0; } inline Te(int f,int t,KomaInf k,KomaInf c,int p=0,int K=0,int v=0) { from=f; to=t; koma=k; capture=c; promote=p; Kind=K; value=v; } // 手を表示したい時に使います。 void Print() { FPrint(stdout); } // 同上 void FPrint(FILE *fp); // 手の同一性を比較したいときに使います。KindやValueが違っても同じ手です。 int operator==(const Te &a) { return a.from==from && a.to==to && a.koma==koma && a.promote==promote; } };
局面クラスの全体は、手の生成に必要な関数を含め、以下のように定義した。
// 局面のクラス class Kyokumen { public: // 桂馬の利きがbanからはみ出すので、はみ出す分を確保しておきます。 // C++では、構造体の内部の変数の並び順は宣言した順になることを利用しています。 // 普通はあまり使わない「汚い」テクニックですけど、こういうテクニックもあるということで。 KomaInf banpadding[16]; // 2次元配列を使うと遅いので、1次元配列を使います。また、掛け算の際に、*9とかを用いるよりも、 // 2の階乗を掛け算に使うと掛け算が早くなるので、*16を使います。 // 駒の位置は、例えば7七なら、7*16+七のようにあらわします。 // つまり、7七なら16進数で0x77になるわけです。 KomaInf ban[16*11]; // 駒の利きを保持します。敵の駒と自分の駒の利きは別々に保持します。 Kiki controlS[16*11]; Kiki controlE[16*11]; // 持ち駒です。王が持ち駒になることはないので、EHIまでで十分です。 // Hand[SFU]が1なら先手の持ち駒に歩が1枚、Hand[EKI]が3なら敵の持ち駒に金が3枚という要領です。 int Hand[EHI+1]; // この局面の手数です。 int Tesu; // 互いの玉の位置です。 int kingS; int kingE; protected: void InitControl(); int Utifudume(int SorE,int to,int *pin); void MoveKing(int SorE,int &teNum,Te *teTop,Kiki kiki); //玉の動く手の生成 void MoveTo(int SorE,int &teNum,Te *teTop,int to,int *pin); //toに動く手の生成 void PutTo(int SorE,int &teNum,Te *teTop,int to,int *pin); //toに駒を打つ手の生成 Kiki CountControlS(int pos); Kiki CountControlE(int pos); Kiki CountMove(int SorE,int pos,int *pin); void AddMoves(int SorE,int &teNum,Te *teTop,int from,int pin,int Rpin=0); void AddStraight(int SorE,int &teNum,Te *teTop,int from,int dir,int pin,int Rpin=0); void AddMove(int SorE,int &teNum,Te *teTop,int from,int diff,int pin,int Rpin=0); public: inline int search(int pos,int dir) const { do { pos+=dir; }while(ban[pos]==EMPTY); return pos; } Kyokumen() {}; Kyokumen(int tesu,KomaInf ban[9][9],int Motigoma[]); void Print() { FPrint(stdout); } void FPrint(FILE *fp); void MakePinInf(int *pin) const; int MakeLegalMoves(int SorE,Te *tebuf,int *pin=NULL); int AntiCheck(int SorE,Te *tebuf,int *pin,Kiki control); void Move(int SorE,const Te &te); }; // ピン(動かすと王を取られてしまうので動きが制限される駒)の状態を設定する void Kyokumen::MakePinInf(int *pin) const { int i; // ピン情報を設定する for (i = 0x11; i <= 0x99; i++) { // 0はピンされていない、という意味 pin[i] = 0; } if (kingS) { //自玉が盤面にある時のみ有効 for (i = 0; i < 8; i++) { int p; p = search(kingS, Direct[i]); if ((ban[p] != WALL) && !(ban[p] & ENEMY)) { //味方の駒が有る if (controlE[p]&1<<(16+i)) { pin[p]=Direct[i]; } } } } if (kingE) { //敵玉が盤面にある時のみ有効 for (i = 0; i < 8; i++) { int p; p = search(kingE, -Direct[i]); if ((ban[p] != WALL) && (ban[p] & ENEMY)) { //敵の駒が有る if (controlS[p]&1<<(16+i)) { pin[p]=Direct[i]; } } } } } // 駒の動きとして正しい動きを全て生成する。 int Kyokumen::MakeLegalMoves(int SorE,Te *teBuf,int *pin) { int pbuf[16*11]; int teNum=0; if (pin==NULL) { MakePinInf(pbuf); pin=pbuf; } if (SorE==SELF && controlE[kingS]!=0) { return AntiCheck(SorE,teBuf,pin,controlE[kingS]); } if (SorE==ENEMY && controlS[kingE]!=0) { return AntiCheck(SorE,teBuf,pin,controlS[kingE]); } int suji,dan; int StartDan,EndDan; // 盤上の駒を動かす for(suji=0x10;suji<=0x90;suji+=0x10) { for(dan=1;dan<=9;dan++) { if (ban[suji+dan]&SorE) { AddMoves(SorE,teNum,teBuf,suji+dan,pin[suji+dan]); } } } // 歩を打つ if (Hand[SorE|FU]>0) { for(suji=0x10;suji<=0x90;suji+=0x10) { // 二歩チェック int nifu=0; for(dan=1;dan<=9;dan++) { if (ban[suji+dan]==SorE|FU) { nifu=true; break; } } if (nifu) continue; //(先手なら2段目より下に、後手なら8段目より上に打つ) if (SorE==SELF) { StartDan=2; EndDan=9; } else { StartDan=1; EndDan=8; } for(dan=StartDan;dan<=EndDan;dan++) { // 打ち歩詰めもチェック if (ban[dan+suji]==EMPTY && !Utifudume(SorE,dan+suji,pin)) { teBuf[teNum++]=Te(0,suji+dan,SorE|FU,EMPTY); } } } } // 香を打つ if (Hand[SorE|KY]>0) { for(suji=0x10;suji<=0x90;suji+=0x10) { //(先手なら2段目より下に、後手なら8段目より上に打つ) if (SorE==SELF) { StartDan=2; EndDan=9; } else { StartDan=1; EndDan=8; } for(dan=StartDan;dan<=EndDan;dan++) { if (ban[dan+suji]==EMPTY) { teBuf[teNum++]=Te(0,suji+dan,SorE|KY,EMPTY); } } } } //桂を打つ if (Hand[SorE|KE]>0) { //(先手なら3段目より下に、後手なら7段目より上に打つ) for(suji=0x10;suji<=0x90;suji+=0x10) { if (SorE==SELF) { StartDan=3; EndDan=9; } else { StartDan=1; EndDan=7; } for(dan=StartDan;dan<=EndDan;dan++) { if (ban[dan+suji]==EMPTY) { teBuf[teNum++]=Te(0,suji+dan,SorE|KE,EMPTY); } } } } // 銀~飛車は、どこにでも打てる for(int koma=GI;koma<=HI;koma++) { if (Hand[SorE|koma]>0) { for(suji=0x10;suji<=0x90;suji+=0x10) { for(dan=1;dan<=9;dan++) { if (ban[dan+suji]==EMPTY) { teBuf[teNum++]=Te(0,suji+dan,SorE|koma,EMPTY); } } } } } return teNum; } // 盤面のfromにある駒を動かす手を生成する。 void Kyokumen::AddMoves(int SorE,int &teNum,Te *teTop,int from,int pin,int Rpin) { switch(ban[from]) { case SFU: AddMove(SorE,teNum,teTop,from,-1,pin,Rpin); break; case EFU: AddMove(SorE,teNum,teTop,from,+1,pin,Rpin); break; case SKY: AddStraight(SorE,teNum,teTop,from,-1,pin,Rpin); break; case EKY: AddStraight(SorE,teNum,teTop,from,+1,pin,Rpin); break; case SKE: AddMove(SorE,teNum,teTop,from,+14,pin,Rpin); AddMove(SorE,teNum,teTop,from,-18,pin,Rpin); break; case EKE: AddMove(SorE,teNum,teTop,from,-14,pin,Rpin); AddMove(SorE,teNum,teTop,from,+18,pin,Rpin); break; case SGI: AddMove(SorE,teNum,teTop,from, -1,pin,Rpin); AddMove(SorE,teNum,teTop,from,-17,pin,Rpin); AddMove(SorE,teNum,teTop,from, 15,pin,Rpin); AddMove(SorE,teNum,teTop,from, 17,pin,Rpin); AddMove(SorE,teNum,teTop,from,-15,pin,Rpin); break; case EGI: AddMove(SorE,teNum,teTop,from, +1,pin,Rpin); AddMove(SorE,teNum,teTop,from, 17,pin,Rpin); AddMove(SorE,teNum,teTop,from,-15,pin,Rpin); AddMove(SorE,teNum,teTop,from,-17,pin,Rpin); AddMove(SorE,teNum,teTop,from, 15,pin,Rpin); break; case SKI:case STO:case SNY:case SNK:case SNG: AddMove(SorE,teNum,teTop,from, -1,pin,Rpin); AddMove(SorE,teNum,teTop,from,-17,pin,Rpin); AddMove(SorE,teNum,teTop,from, 15,pin,Rpin); AddMove(SorE,teNum,teTop,from, +1,pin,Rpin); AddMove(SorE,teNum,teTop,from,-16,pin,Rpin); AddMove(SorE,teNum,teTop,from, 16,pin,Rpin); break; case EKI:case ETO:case ENY:case ENK:case ENG: AddMove(SorE,teNum,teTop,from, +1,pin,Rpin); AddMove(SorE,teNum,teTop,from, 17,pin,Rpin); AddMove(SorE,teNum,teTop,from,-15,pin,Rpin); AddMove(SorE,teNum,teTop,from, -1,pin,Rpin); AddMove(SorE,teNum,teTop,from,-16,pin,Rpin); AddMove(SorE,teNum,teTop,from, 16,pin,Rpin); break; case SRY:case ERY: AddMove(SorE,teNum,teTop,from, 17,pin,Rpin); AddMove(SorE,teNum,teTop,from,-15,pin,Rpin); AddMove(SorE,teNum,teTop,from,-17,pin,Rpin); AddMove(SorE,teNum,teTop,from,+15,pin,Rpin); case SHI:case EHI: AddStraight(SorE,teNum,teTop,from,+1,pin,Rpin); AddStraight(SorE,teNum,teTop,from,-1,pin,Rpin); AddStraight(SorE,teNum,teTop,from,-16,pin,Rpin); AddStraight(SorE,teNum,teTop,from,+16,pin,Rpin); break; case SUM:case EUM: AddMove(SorE,teNum,teTop,from,+1,pin,Rpin); AddMove(SorE,teNum,teTop,from,+16,pin,Rpin); AddMove(SorE,teNum,teTop,from,-16,pin,Rpin); AddMove(SorE,teNum,teTop,from,-1,pin,Rpin); case SKA:case EKA: AddStraight(SorE,teNum,teTop,from,+17,pin,Rpin); AddStraight(SorE,teNum,teTop,from,-17,pin,Rpin); AddStraight(SorE,teNum,teTop,from,+15,pin,Rpin); AddStraight(SorE,teNum,teTop,from,-15,pin,Rpin); break; case SOU:case EOU: MoveKing(SorE,teNum,teTop,0); // 王手がかかっている時には、AntiCheckの方が呼ばれるから、Kikiは0です。 } } // ある場所の利き情報を作成して返す。普段は使わない関数(差分計算しているから)だが、 // 打ち歩詰めのチェックなど、駒を仮に置いてみて何かするようなときに使用する。 Kiki Kyokumen::CountControlS(int pos) { Kiki ret=0; int i,b,bj; b=1; bj=1<<16; for(i=0;i<12;i++,b<<1,bj<<1) { if (CanMove[i][ban[pos-Direct[i]]] && (ban[pos-Direct[i]]&SELF)) { ret|=b; } else if (CanJump[i][ban[search(pos,-Direct[i])]] && (ban[search(pos,-Direct[i])]&SELF)) { ret|=bj; } } return ret; } // ある場所の利き情報を作成して返す。普段は使わない関数(差分計算しているから)だが、 // 打ち歩詰めのチェックなど、駒を仮に置いてみて何かするようなときに使用する。 Kiki Kyokumen::CountControlE(int pos) { Kiki ret=0; int i,b,bj; b=1; bj=1<<16; for(i=0;i<12;i++,b<<1,bj<<1) { if (CanMove[i][ban[pos-Direct[i]]] && (ban[pos-Direct[i]]&ENEMY)) { ret|=b; } else if (CanJump[i][ban[search(pos,-Direct[i])]] && (ban[search(pos,-Direct[i])]&ENEMY)) { ret|=bj; } } return ret; } // ある場所に移動できる駒を全部集めて、Kiki情報にして返す。 // このとき、pinされている駒はpinの方向にしか動けない。 Kiki Kyokumen::CountMove(int SorE,int pos,int *pin) { Kiki ret=0; int i,b,bj; b=1; bj=1<<16; for(i=0;i<12;i++,b<<1,bj<<1) { if (CanMove[i][ban[pos-Direct[i]]] && (ban[pos-Direct[i]]&ENEMY) && (pin[pos-Direct[i]]==0||pin[pos-Direct[i]]==Direct[i]||pin[pos-Direct[i]]==-Direct[i])) { ret|=b; } else if (CanJump[i][ban[search(pos,-Direct[i])]] && (ban[search(pos,-Direct[i])]&ENEMY)&& (pin[search(pos,-Direct[i])]==0||pin[search(pos,-Direct[i])]==Direct[i]||pin[search(pos,-Direct[i])]==-Direct[i])) { ret|=bj; } } return ret; } // 打ち歩詰めの判定 int Kyokumen::Utifudume(int SorE,int to,int *pin) { if (SorE==SELF) { // まず、玉の頭に歩を打つ手じゃなければ打ち歩詰めの心配はない。 if (kingE+1!=to) { return 0; } } else { // まず、玉の頭に歩を打つ手じゃなければ打ち歩詰めの心配はない。 if (kingS-1!=to) { return 0; } } //実際に歩を打って確かめてみる。 ban[to]=FU|SorE; if (SorE==SELF) { // 自分の利きがあったら相手は玉で取れない & 取る動きを列挙してみたら玉で取る手しかない if (controlS[to] && (CountMove(ENEMY,to,pin)==1<<2)) { // 玉に逃げ道があるかどうかをチェック for(int i=0;i<8;i++) { KomaInf koma=ban[kingE+Direct[i]]; if (!(koma & ENEMY) && !CountControlS(kingE+Direct[i])) { // 逃げ道があったので、盤面を元の状態に戻して、 ban[to]=EMPTY; // 打ち歩詰めではなかった。 return 0; } } // 玉の逃げ道もないのなら、打ち歩詰め。盤面の状態は元に戻す。 ban[to]=EMPTY; return 1; } // 玉以外で取る手があるので打ち歩詰めではない。 ban[to]=EMPTY; return 0; } else { // 自分の利きがあったら相手は玉で取れない & 取る動きを列挙してみたら玉で取る手しかない if (controlE[to] && (CountMove(SELF,to,pin)==1<<6)) { // 玉に逃げ道があるかどうかをチェック for(int i=0;i<8;i++) { KomaInf koma=ban[kingS+Direct[i]]; if (!(koma & SELF) && !CountControlE(kingS+Direct[i])) { // 逃げ道があったので、盤面を元の状態に戻して、 ban[to]=EMPTY; // 打ち歩詰めではなかった。 return 0; } } // 玉の逃げ道もないのなら、打ち歩詰め。盤面の状態は元に戻す。 ban[to]=EMPTY; return 1; } // 玉以外で取る手があるので打ち歩詰めではない。 ban[to]=EMPTY; return 0; } } // ある場所(to)に駒を打つ手の生成 void Kyokumen::PutTo(int SorE,int &teNum,Te *teTop,int to,int *pin) { int dan=to &0x0f; if (SorE==ENEMY) { dan=10-dan; } if (Hand[SorE|FU]>0 && dan>1) { // 歩を打つ手を生成 // 二歩チェック int suji=to & 0xf0; int nifu=0; for(int d=1;d<=9;d++) { if (ban[suji+d]==(SorE|FU)) { nifu=1; break; } } // 打ち歩詰めもチェック if (!nifu && !Utifudume(SorE,to,pin)) { teTop[teNum++]=Te(0,to,SorE|FU,EMPTY); } } if (Hand[SorE|KY]>0 && dan>1) { // 香を打つ手を生成 teTop[teNum++]=Te(0,to,SorE|KY,EMPTY); } if (Hand[SorE|KE]>0 && dan>2) { teTop[teNum++]=Te(0,to,SorE|KE,EMPTY); } for(int koma=GI;koma<=HI;koma++) { if (Hand[SorE|koma]>0) { teTop[teNum++]=Te(0,to,SorE|koma,EMPTY); } } } // 王手を受ける手の生成 int Kyokumen::AntiCheck(int SorE,Te *teBuf,int *pin,Kiki kiki) { int king; int teNum=0; if ((kiki & (kiki-1))!=0) { //両王手は玉を動かすしかない MoveKing(SorE,teNum, teBuf, kiki); } else { if (SorE==SELF) { king=kingS; } else { king=kingE; } unsigned int id; int check; for (id = 0; id <= 31; id++) { if (kiki == (1u << id)) break; } if (id < 16) { check = king - Direct[id]; } else { check = search(king,-Direct[id-16]); } //王手駒を取る MoveTo(SorE,teNum, teBuf, check, pin); //玉を動かす MoveKing(SorE,teNum, teBuf, kiki); if (id >= 16) { //合駒をする手を生成する int i; for (i = king - Direct[id-16]; ban[i] == EMPTY; i -= Direct[id-16]) { MoveTo(SorE,teNum, teBuf, i, pin); //移動合 } for (i = king - Direct[id-16]; ban[i] == EMPTY; i -= Direct[id-16]) { PutTo(SorE,teNum, teBuf, i, pin); //駒を打つ合 t} } } return teNum; } // 玉を動かす手の生成 // 普通の駒と違い、相手の利きのあるところには動けないので、そのための特殊な処理をしています。 // そのために、最後にkikiという変数を入れています。 void Kyokumen::MoveKing(int SorE,int &teNum,Te *teTop,Kiki kiki) { int i; int id = -1; //隣接王手駒の位置のid // 両王手でないなら王手駒の位置を探す for (i = 0; i < 8; i++) { if (kiki & (1 << i)) { id = i; break; } } if (id >= 0) { // 隣接の王手 最初に取る手を生成するのだ if (SorE==SELF) { KomaInf koma=ban[kingS-Direct[id]]; if (( koma==EMPTY || (koma & ENEMY)) && !CountControlE(kingS - Direct[id]) //敵の駒が効いていない && !(kiki & (1 << (23-id)))) //敵の飛駒で貫かれていない AddMove(SorE,teNum, teTop, kingS, -Direct[id], 0); } else { KomaInf koma=ban[kingE-Direct[id]]; if (( koma==EMPTY || (koma & SELF)) && !CountControlS(kingE - Direct[id]) //敵の駒が効いていない && !(kiki & (1 << (23-id)))) //敵の飛駒で貫かれていない AddMove(SorE,teNum, teTop, kingE, -Direct[id], 0); } } for (i = 0; i < 8; i++) { if (i == id) continue; if (SorE==SELF) { KomaInf koma=ban[kingS-Direct[i]]; if (( koma==EMPTY || (koma & ENEMY)) && !CountControlE(kingS - Direct[i]) //敵の駒が効いていない && !(kiki & (1 << (23-i)))) //敵の飛駒で貫かれていない AddMove(SorE,teNum, teTop, kingS, -Direct[i], 0); } else { KomaInf koma=ban[kingE-Direct[i]]; if (( koma==EMPTY || (koma & SELF)) && !CountControlS(kingE - Direct[i]) //敵の駒が効いていない && !(kiki & (1 << (23-i)))) //敵の飛駒で貫かれていない AddMove(SorE,teNum, teTop, kingE, -Direct[i], 0); } } } // 手の生成:成り・不成りも意識して、駒の動く手を生成する。 void Kyokumen::AddMove(int SorE,int &teNum,Te *teTop,int from,int diff,int pin,int Rpin) { if (Rpin==diff||Rpin==-diff) { return; } int to=from+diff; int dan=to&0x0f; int fromDan=from&0x0f; if ((pin==0 || pin==diff || pin==-diff) && !(ban[to]&SorE)) { if (ban[from]==SKE && dan<=2) { // 必ず成る teTop[teNum++]=Te(from,to,ban[from],ban[to],1); } else if ((ban[from]==SFU || ban[from]==SKY) && dan<=1) { // 必ず成る teTop[teNum++]=Te(from,to,ban[from],ban[to],1); } else if (ban[from]==EKE && dan>=8) { // 必ず成る teTop[teNum++]=Te(from,to,ban[from],ban[to],1); } else if ((ban[from]==EFU || ban[from]==EKY) && dan>=9) { // 必ず成る teTop[teNum++]=Te(from,to,ban[from],ban[to],1); } else { if (SorE==SELF && (fromDan<=3 || dan <=3) && CanPromote[ban[from]]) { teTop[teNum++]=Te(from,to,ban[from],ban[to],1); } else if (SorE==ENEMY && (fromDan>=7 || dan>=7) && CanPromote[ban[from]]){ teTop[teNum++]=Te(from,to,ban[from],ban[to],1); } // 成らない手も生成する。 teTop[teNum++]=Te(from,to,ban[from],ban[to],0); } } } // 飛車角香車がまっすぐに進む手の生成 void Kyokumen::AddStraight(int SorE,int &teNum,Te *teTop,int from,int dir,int pin,int Rpin) { if (dir==Rpin || dir==-Rpin) { return; } int i; if (pin==0 || pin==dir || pin==-dir) { // 空白の間、動く手を生成する for(i=dir;ban[from+i]==EMPTY;i+=dir) { AddMove(SorE,teNum,teTop,from,i,0); } // 味方の駒でないなら、そこへ動く if (!(ban[from+i] & SorE)) { AddMove(SorE,teNum,teTop,from,i,0); } } } //toに動く手の生成 void Kyokumen::MoveTo(int SorE,int &teNum,Te *teTop,int to,int* pin) { int p; KomaInf koma; for(int i=0;i<12;i++) { if ((koma=ban[to-Direct[i]]) == EMPTY) { p=search(to,-Direct[i]); if ((ban[p]&SorE) && CanJump[i][ban[p]]) { AddMove(SorE,teNum,teTop,p,to-p,pin[p]); } } else { if ((koma&~SorE)!=OU && (koma&SorE) && (CanMove[i][koma]||CanJump[i][koma])) { AddMove(SorE,teNum,teTop,to-Direct[i],Direct[i],pin[to-Direct[i]]); } } } }
将棋プログラムで、一番つらいのは実際にはこの辺りをプログラムしている時なので、ここまで読み終えたあなたは、プログラムの前半の最大の山場を越えたと思ってもらって良い。なお、この章では持ち越しの課題とした、千日手および連続王手の千日手については、第5章にて対応する。
さて、このままでは、指し手の生成ができるだけなので、いよいよ次の章では実際に今までに作成した関数群を使って、対戦可能なプログラムに仕上げてみたい。