局面の画面上での表現は、本来先手の持ち駒は盤の左側に、後手の持ち駒は盤の右側に記述すべきものなのだが、さすがにそれは面倒なので、先手の持ち駒は盤の下に、後手の持ち駒は盤の上に表示することにした。
この関数については、表示だけなので細かい説明は省略する。
ここで、1文字で表示するため、成桂は「圭」、成り香は「杏」、成り銀は「全」と表示する。
また、文字を反対向きに表示することもできないので、後手側の駒は、「v王」のように、「v」という文字を追加して下向きであることを表わした。
// 表示のための定数 // 駒を盤面に表示するための文字列 char *komaStr[]={ " "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," ", " "," 歩"," 香"," 桂"," 銀"," 金"," 角"," 飛"," 王"," と"," 杏"," 圭"," 全"," 金"," 馬"," 龍", " ","v歩","v香","v桂","v銀","v金","v角","v飛","v王","vと","v杏","v圭","v全","v金","v馬","v龍", " 壁"," "," "," "," "," "," "," "," "," "," "," "," "," "," "," ", }; // 駒の種類を表示するための文字列 char *komaStr2[]={ " "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," ", " ","歩","香","桂","銀","金","角","飛","王","と","杏","圭","全","金","馬","龍", " ","歩","香","桂","銀","金","角","飛","王","と","杏","圭","全","金","馬","龍", " 壁"," "," "," "," "," "," "," "," "," "," "," "," "," "," "," ", };
// 標準出力に向かって表示する。 void Kyokumen::Print() { Fprint(stdout); } // それっぽく表示する void Kyokumen::FPrint(FILE *fp) { int x,y; y = 0; fprintf(fp,"持ち駒:"); for (x = EHI; x >=EFU; x--) { if (Hand[x] > 1) { y = 1; fprintf(fp,"%s%2.2s", komaStr2[x], "一二三四五六七八九101112131415161718"+2*Hand[x]-2); } else if (Hand[x] == 1) { y = 1; fprintf(fp,"%s", komaStr2[x]); } } if (y) { fprintf(fp,"\n"); } else { fprintf(fp,"なし\n"); } fprintf(fp," 9 8 7 6 5 4 3 2 1 \n"); fprintf(fp,"+---------------------------+\n"); for(y=1;y<=9;y++) { fprintf(fp,"|"); for(x=9;x>=1;x--) { fprintf(fp,komaStr[ban[x*16+y]]); } fprintf(fp,"|%2.2s","一二三四五六七八九" + y*2-2); fprintf(fp,"\n"); } fprintf(fp,"+---------------------------+\n"); fprintf(fp,"持ち駒:"); y = 0; for (x = SHI; x >= SFU; x--) { if (Hand[x] > 1) { y = 1; fprintf(fp,"%s%2.2s", komaStr2[x], "一二三四五六七八九101112131415161718"+2*Hand[x]-2); } else if (Hand[x] == 1) { y = 1; fprintf(fp,"%s", komaStr2[x]); } } if (y) { fprintf(fp,"\n"); } else { fprintf(fp,"なし\n"); } }
指し手の表示については、前章で説明したとおり、5二金右のように書くのが正しいのであるが、これも簡単のために、52金(41)のように表示させてもらうことにする。また、デバッグのために、一度にたくさん手を表示することを考え、どんな手でも同じ文字数で表示することにした。そのため、空白が開くことがあるが、これもご容赦願いたい。
// 指し手を画面(標準出力)に表示する void Te::Print() { Fprint(stdout); } // 指し手の表示 void Te::FPrint(FILE *fp) { fprintf(fp,"%02x",to); fprintf(fp,"%02s",komaStr2[koma]); if (promote) { fprintf(fp,"成"); } if (from<OU) { fprintf(fp,"打 "); } else { fprintf(fp,"(%02x)",from); } if (!promote) { fprintf(fp," "); } }
ユーザの指し手の入力は、以下のルールで行ってもらうことにした。
まず、移動元の駒の位置を2桁の整数で入力してもらい、次に移動先の駒の位置を2桁の整数で入力してもらう。
▲7六歩なら、7776という要領である。
次に、「成る」時には、最後に*をつけてもらう。例えば、2三歩成なら、2423*という要領である。
これを標準入力から入力してもらい、sscanfで16進数として解釈すると、ほとんど手と同じデータ(from,to)が得られるので、後は駒に関するデータ(移動する駒、取った駒)を付加するだけである。
駒を打つときには、歩を1、香車を2、…飛車を7として、例えば3三角打なら、0633のように入力してもらうものとした。この場合も、16進数として解釈すれば、容易に手の内部形式のfrom,to,komaが得られる。
結局、mainの部分は以下のようになる。
// メインです。 int main() { // 平手の初期配置です。見やすいでしょ?変換はその分複雑ですけど。 KomaInf HirateBan[9][9]={ {EKY,EKE,EGI,EKI,EOU,EKI,EGI,EKE,EKY}, {EMP,EHI,EMP,EMP,EMP,EMP,EMP,EKA,EMP}, {EFU,EFU,EFU,EFU,EFU,EFU,EFU,EFU,EFU}, {EMP,EMP,EMP,EMP,EMP,EMP,EMP,EMP,EMP}, {EMP,EMP,EMP,EMP,EMP,EMP,EMP,EMP,EMP}, {EMP,EMP,EMP,EMP,EMP,EMP,EMP,EMP,EMP}, {SFU,SFU,SFU,SFU,SFU,SFU,SFU,SFU,SFU}, {EMP,SKA,EMP,EMP,EMP,EMP,EMP,SHI,EMP}, {SKY,SKE,SGI,SKI,SOU,SKI,SGI,SKE,SKY} }; // こちらは面倒でもEHIまで0を並べないといけません。 int MotigomaNasi[EHI+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手目で、平手の局面で、持ち駒なしから開始しましょう。 Kyokumen k(0,HirateBan,MotigomaNasi); // これはまだ簡単な思考部なので、初期化も簡単です。 Sikou sikou; // 将棋の局面で、最大の手数は579手だそうです。 Te teBuf[600]; int teNum; // 手前のプレイヤーから開始します。 int SorE=SELF; // もしも合法手がなくなったら、詰み=負けです。 // 合法手がある間はゲームを継続します。 while((teNum=k.MakeLegalMoves(SorE,teBuf))>0) { k.Print(); Te te; if (SorE==SELF) { // 手を入力します。 char buf[80]; gets(buf); // 入力の方法:from,to,promote // ただし、歩を打つときはfromを01、香を打つときはfromを02…とする。 // promoteは、成るときに*を付ける。 int from,to; int koma,capture; char promote[2]; promote[0]='\0'; int ss=sscanf(buf,"%02x%02x%1s",&from,&to,&promote); if (ss<2) continue; if (from<OU) { koma=SELF|from; from=0; } else { koma=k.ban[from]; } capture=k.ban[to]; if (ss=3 && promote[0]=='*') { te=Te(from,to,koma,capture,1); } else { te=Te(from,to,koma,capture,0); } // 入力された手が、おかしかったら、 if (IsIllegal(te,teNum,teBuf)) { // もう一回盤面を表示して入力しなおしです。 printf("入力された手が異常です。入力しなおしてください。\n"); continue; } } if (SorE==ENEMY) { te=sikou.Think(SorE,k); } te.Print(); k.Move(SorE,te); if (SorE==SELF) { SorE=ENEMY; } else { SorE=SELF; } } if (SorE==SELF) { printf("後手の勝ち。\n"); } else { printf("先手の勝ち。\n"); } return 0; }
ここで、局面のコンストラクタとして、新しく駒の配列と持ち駒の配列を引数とするものを用意した。
// 局面のコンストラクタ:盤面の状態と手数、持ち駒から生成 Kyokumen::Kyokumen(int tesu,KomaInf board[9][9],int Motigoma[]) { kingS=0; kingE=0; Tesu=tesu; // 盤面をWALL(壁)で埋めておきます。 memset(banpadding,WALL,sizeof(banpadding)); memset(ban,WALL,sizeof(ban)); // boardで与えられた局面を設定します。 for(int dan=1;dan<=9;dan++) { for(int suji=0x10;suji<=0x90;suji+=0x10) { // 将棋の筋は左から右なので、配列の宣言と逆になるため、筋はひっくり返さないとなりません。 ban[suji+dan]=board[dan-1][9-suji/0x10]; if (ban[suji+dan]==SOU) { kingS=suji+dan; } if (ban[suji+dan]==EOU) { kingE=suji+dan; } } } // 持ち駒はそのまま利用します。 for(int i=0;i<=EHI;i++) { Hand[i]=Motigoma[i]; } // controlS/controlEを初期化します。 InitControl(); } // controlS,controlEの初期化 void Kyokumen::InitControl() { int dan, suji; int i, j, b, bj; memset(controlS,0,sizeof(controlS)); memset(controlE,0,sizeof(controlE)); for (suji = 0x10; suji <= 0x90; suji += 0x10) { for (dan = 1 ; dan <= 9 ; dan++) { if (ban[suji + dan] & ENEMY) { //敵の駒 //駒の効きを追加する for (i = 0, b = 1, bj = (1<<16); i < 12; i++, b<<=1, bj<<=1) { if (CanJump[i][ban[dan + suji]]) { j = dan + suji; do { j += Direct[i]; controlE[j] |= bj; } while (ban[j] == EMPTY); } else if (CanMove[i][ban[dan + suji]]) { controlE[dan + suji + Direct[i]] |= b; } } } else if (ban[suji + dan] & SELF) { //味方の駒が有る //駒の効きを追加する for (i = 0, b = 1, bj = (1<<16); i < 12; i++, b<<=1, bj<<=1) { if (CanJump[i][ban[dan + suji]]) { j = dan + suji; do { j += Direct[i]; controlS[j] |= bj; } while (ban[j] == EMPTY); } else if (CanMove[i][ban[dan + suji]]) { controlS[dan + suji + Direct[i]] |= b; } } } } } }
また、一手進める関数は、以下のようになる。
一手進める関数の中では、盤面・持ち駒の実際の変化の他に、kingS,kingEの位置の更新、利き情報の更新を行うことにした。これは、毎回計算するよりも差分計算した方が高速に動作するからである。今はまだ高速に動作する必要性はあまりないのだが、先の章でMinMaxなどのアルゴリズムを導入した場合、非常に多くの回数呼ばれる関数となるため、高速であればあるほど望ましい。
// 手で局面を進める void Kyokumen::Move(int SorE,const Te &te) { int i,j,b,bj; if (te.from>0x10) { // 元いた駒のコントロールを消す int dir; for(dir=0,b=1,bj=1<<16;dir<12;dir++,b<<=1,bj<<=1) { if (SorE==SELF) { controlS[te.from+Direct[dir]]&=~b; } else { controlE[te.from+Direct[dir]]&=~b; } if (CanJump[dir][te.koma]) { int j = te.from; do { j += Direct[dir]; if (SorE==SELF) { controlS[j] &= ~bj; } else { controlE[j] &= ~bj; } } while (ban[j] == EMPTY); } } // 元いた位置は空白になる ban[te.from]=EMPTY; // 飛び利きを伸ばす for (i = 0, bj = (1<<16); i < 8; i++, bj<<=1) { int Dir=Direct[i]; if (controlS[te.from] & bj) { j = te.from; do { j += Dir; controlS[j] |= bj; } while (ban[j] == EMPTY); } if (controlE[te.from] & bj) { j = te.from; do { j += Dir; controlE[j] |= bj; } while (ban[j] == EMPTY); } } } else { // 持ち駒から一枚減らす Hand[te.koma]--; } if (ban[te.to]!=EMPTY) { // 相手の駒を持ち駒にする。 // 持ち駒にする時は、成っている駒も不成りに戻す。(&~PROMOTED) Hand[SorE|(ban[te.to]&~PROMOTED&~SELF&~ENEMY)]++; //取った駒の効きを消す for (i = 0, b = 1, bj = (1<<16); i < 12; i++, b<<=1, bj<<=1) { int Dir=Direct[i]; if (CanJump[i][ban[te.to]]) { j = te.to; do { j += Dir; if (SorE==SELF) { controlE[j] &= ~bj; } else { controlS[j] &= ~bj; } } while (ban[j] == EMPTY); } else { j=te.to + Dir; if (SorE==SELF) { controlE[j] &= ~b; } else { controlS[j] &= ~b; } } } } else { // 移動先で遮った飛び利きを消す for (i = 0, bj = (1<<16); i < 8; i++, bj<<=1) { int Dir=Direct[i]; if (controlS[te.to] & bj) { j = te.to; do { j += Dir; controlS[j] &= ~bj; } while (ban[j] == EMPTY); } if (controlE[te.to] & bj) { j = te.to; do { j += Dir; controlE[j] &= ~bj; } while (ban[j] == EMPTY); } } } if (te.promote) { ban[te.to]=te.koma|PROMOTED; } else { ban[te.to]=te.koma; } // 移動先の利きをつける for (i = 0, b = 1, bj = (1<<16); i < 12; i++, b<<=1, bj<<=1) { if (CanJump[i][ban[te.to]]) { j = te.to; do { j += Direct[i]; if (SorE==SELF) { controlS[j] |= bj; } else { controlE[j] |= bj; } } while (ban[j] == EMPTY); } else if (CanMove[i][ban[te.to]]) { if (SorE==SELF) { controlS[te.to+Direct[i]] |= b; } else { controlE[te.to+Direct[i]] |= b; } } } // 王様の位置は覚えておく。 if (te.koma==SOU) { kingS=te.to; } if (te.koma==EOU) { kingE=te.to; } Tesu++; }
肝心の、思考部分であるが、もっとも簡単に、乱数で指し手を選ぶことにした。
// 簡単な思考ルーチンです。 // 要するに、合法な手の中から、適当な手を乱数で選んで指すだけです。 Te Sikou::Think(int SorE,Kyokumen k) { Te teBuf[600]; int teNum=k.MakeLegalMoves(SorE,teBuf); #ifdef _DEBUG // デバッグの際には、合法手を一覧表示します。 for(int i=0;i<teNum;i++) { teBuf[i].Print(); } printf("\n"); #endif int r=rand()%teNum; return teBuf[r]; }
なお、乱数は、srand()関数を用いない限り、必ずいつも同じ乱数列が生成され、いつも同じ手を指すことになってしまうのだが、今回は構わないものとさせてもらった。必要だと思えば、srand(time());などの手段で初期化して欲しい。
ここまでの内容は、付録CD-ROM内のchapter02フォルダ内に収録している。
VC++であれば、dswファイルをダブルクリックすることで、プロジェクトが開くはずである。また、それ以外のコンパイラを使っている方は、付属のmakefileを適宜変更することでmakeしてみて欲しい。なお、付属のmakefile.bccはBorland C++向けに、makefile.gccはLinux/FreeBSDなどのGCC向けに作成している。
それぞれ、make -f makefile.bccやmake -f makefile.gccとしてビルドしてみて欲しい。
2015年10月 追記:文字列についてなのだが、ここではターゲットはSJIS及びEUCとしていた。Unicodeを使う場合には、%2.2s等複数の箇所で、文字列が2バイトであることを前提にしているため、適宜変更して欲しい。