2012年7月21日土曜日

はじめてのコンピュータ将棋入門(1)

今回から数回に分けて、コンピュータ将棋プログラムを作りたい人向けに簡単な文章を提供したいと思います。メカウーサー将棋は弱いので、これを読んで強い将棋プログラムを作って下さる方が現れるとうれしいなと思っています。長文すみません。

 ※分かりやすく書いたら長文で分かりにくくなりました。。。(T_T)

1回目はまず、将棋プログラムとはなんぞや、というところから始めたいと思います。プログラミングの知識のある人にとっては「いらね」という説明が続きますが、そのときはスキップしちゃってください。よろしくお願いします。

まずプログラムとはなんぞやというところから話を始めたいと思います(えーw)
プログラムとはなんぞや、という問いに答えられる人は「※普通はここから読んでください」から読み始めてください。

まず、プログラムは通常機械語と呼ばれる数字の集まりからなるデータの塊です。
で、これを人間がしこしこ書くのは辛いし効率が悪いので、あたまの切れる人が、
「人間に馴染みのある人工言語をつくって、そっから一気に機械語に直すプログラム
を作れば楽勝じゃん?」ということをひらめいて、実際そういうものをつくりました。
この、「人工言語→機械語」の変換プログラムはコンパイラと呼ばれ、あたまのいい
人が作るすごいプログラムの一種です。で、私たちはそれを作らないと変換できない
のではなく、もう出来上がったコンパイラを場合によってはタダで使えます。
素晴らしい世の中です。

多くのエンジニアによってたくさんの人工言語(プログラミング言語といいます)が
発明されてきました。今回はその中でも長い間よく使われて来たC言語という言語
を使って説明していきたいと思います。

 ※普通はここから読んでください

で、ここでやっと将棋プログラムとはなんぞやという説明に入ります。
将棋には先手と後手があり、その内のどちらかをコンピュータが指すとします。
先手番の場合、第1手を「考えて」指し、後手の手を待って後手が指したら
その手を受け取って、第3手を指し、以下どちらかが投了するまで同じことを
続けます(将棋に詳しい人は話をはしおりすぎると思うかもしれませんが、
まずは千日手や入玉のことは忘れてくださいませ)。

そこで、この手順をC言語(っぽい)プログラミング言語で書いてみたいと思います(図1)。

C言語が分かる人は図1を直接見て察してください。分からない人のために説明を以下に書きました。//はコメントと言って、//のあとの文字は無かった事にするという意味を持ちます。

まず、intは32bitの符号付き整数で、、、つまりプラスとかマイナスになる数ですね。
簡単のため、数を色んな場所で使っていますが、それぞれに約束ごとがあります。

int game(int teban) {...} というのを関数といい、実行する内容をまとめて一つにしたもの
です。最初のintは数字が返ってくることを表し、(int teban)は数字を引数に与える
ことを指します。引数は関数専用の入力だと(ここでは)思ってください。

つまり関数gameは将棋対戦をして自分が負けたら0を、勝ったら1を返す関数です。

{}で囲まれた中が関数の中身です。ここに何をしたいかを書きます。

引数のtebanはintの変数といいます。つまり数字を入れる箱です。
自分が先手の時には0が、後手の時は1が入ります。

関数の一番最初にifというキーワードがあります。これはC言語で決まっている
キーワードと言って、特殊な単語です。この単語は変数の名前には使えません。

ifキーワードはelseキーワードと一緒に使い、「CだったらAを、さもなければBを」
という風に条件により異なる内容を実行します。
if (C) { A} else {B}
という形をしていて、Cを条件式と呼び、Aをthenパート、Bをelseパートと呼びます。

ここで、{}の本当の働きを説明します。{}は、一つ以上の文をまとめたものです。
一つの文は「文;」(;はセミコロンと云います)と書きます。これを続けて書くと
{ 文1;文2; } のようになります。つまり文の最後にはセミコロンが入ります。
プログラムは文を使って構成します。詳しくは後々の例を見て行きましょう。

先手番、後手番、両方ともやる事が似ています。
特に、先手番後手番ともに内容がwhileキーワードによって囲まれています。

while(C) { 文; ...} は、Cだったら {}内を実行し続けます。
ここではCに1(条件一致)を与えているので、常に実行を続け、この文は
永遠に返ってきません! でもそれでは困るので、脱出する手段をこの{}
内に書きます。これについては後述します。

つまり先手も後手も一定の処理を条件が整うまで繰り返します。
その条件とは「投了」です。

処理の内容を先手番で見てみましょう。

まず、int te = kouryo(); という記述があります。これは関数呼び出し
と言います。つまり関数を実行します。
鋭いひとはkouryo()関数の中身がここでは書かれていないことに気がつく
でしょう。実はそれこそが私たちの欲しい思考ルーチンなのです。
思考ルーチンの説明は後日します。今日は将棋プログラムがどんなものなのか
まずは感覚で感じてください。

ここでも一つ約束をします。teは先手の手で、0が手がない、それ以外が
何らかの手です。つまり全ての可能な将棋の手に番号を付けておいて、その
番号をこのkouryo()関数が返します。

次のif は {} も elseもありませんが、立派な条件分岐です。
teが0だったら、return 0 を実行します。
条件式の中で同じということを判断するには=ではなく==を使うことに
注意してください。

return 0;というものが現れました。これは0をgame()関数の返すものとして、
game()関数を終了する、という意味になります。0は投了だったので、
この場合はもう先手に指せる手が無くて、投了した、ということになります。

ここで、teに入っているのは可能な手であり、関数gameの値として返すのは
勝敗であることに注意してください。本来は型というものを作って区別するの
ですが、まずは簡単のためすべて数で表現しています。

次のsasu(te)は、コンピュータが持っている盤面で、先手の手を進めます。
詳細は略ですが、盤面の表し方には色々あるので、その方法次第で何を
するかが決まります。

printf("%d¥n", te);  はプリント文と云って、何かをスクリーンに出力する
関数です。これはコンパイラと一緒にすでに用意されています。
暗号みたいな記号が書かれていますが、いまはteの数字をスクリーンに
出す、と覚えてください。

次にaite=gote();とあります。これは後手が手の数字を入力して、その内容を
aiteに入れる事を指します。詳細は略ですが、コンピュータ将棋選手権などでは、
通信して相手の手をもらいます。

これで相手の手がわかりました。もし相手が投了ならreturn 1をして自分が勝ちで終了です。

コンピュータ将棋プログラムは相手が指すときは何もしないと思うかもしれませんが、
一つだけ仕事があって、それは自分の盤面で相手の手を一手動かすことです。
sasu(aite)が相手の手を指して一つ進めます。

さて、これを先手、後手どちらかが投了するまでつづける、というのがコンピュータが
先手番の場合でした。


後手の説明は簡単にしてしまうと、相手の手を待って投了か判断した後に
相手の手を指し、次に自分の手を考えて投了か判断した後に自分の手を
一手進めます。コンピュータが後手番の場合はこれを投了まで繰り返します。


int game(int teban)
{
    if (teban == 0) { // 先手
        while (1) { // 無限繰り返し
            int te = kouryo();   // 先手の手を「考える」
            if (te == 0) return 0;  // 先手投了
            sasu(te);                // 自分の心の中の盤面で先手の手を指す
            printf("%d¥n", te); // 先手の手を相手に知らせる
            int aite = gote();     // 後手の手を受け取る
            if (aite == 0) return 1;  // 後手投了
            sasu(aite);             // 自分の心の中の盤面で後手の手を指す
        } // while
     } else {
        while (1) { // 無限繰り返し
            int aite = sente();   // 先手の手を受け取る
            if (aite == 0) return 1;  // 先手投了
            sasu(aite);                // 自分の心の中の盤面で先手の手を指す
            int te = kouryo();     // 後手の手を「考える」
            if (te == 0) return 0;  // 後手投了
            sasu(te);             // 自分の心の中の盤面で後手の手を指す
            printf("%d¥n", te); // 後手の手を相手に知らせる
        } // while
     }
}

ここで中身を書いていないすべての関数に中身を与える必要があります(printf除く)。
特にkouryo()が難しくて、これのことを思考ルーチンと呼びます。

最後に、game()を誰が呼んでいる、という疑問があると思います。mainが呼びます。

int teban = 0; // コンピュータ先手番

int main(int ac, char *av[])
{
    return game(teban);
}

だめだー、自分文才なさすぎ。すみません。。。

0 件のコメント:

コメントを投稿