今回は、デバッグ用のUSI拡張コマンドの説明から、段・筋・升まで。
USI拡張コマンド “user”
やねうら王miniではUSI拡張コマンドとして”user”というコマンドを追加してある。これを用いると、user.cppというファイルの次の関数が呼び出される。
1 2 3 4 5 6 7 |
#include "all.h" // USI拡張コマンド"user"が送られてくるとこの関数が呼び出される。実験に使ってください。 void user_test(Position& pos) { // cout << pos; } |
この関数をユーザーに開放してある。しばらくの間、ここを用いてやねうら王miniの指し手生成などの機能について説明をしていく。
初期局面の表示
さきほどuser_test()のなかでコメントアウトしてあった部分を有効にしてみよう。
※ 細かいことだが、この連載で「局面」と言う場合、盤面(盤上の駒) + 手駒 + 手番 + そこまでの手順 というような概念である。「局面」と「盤面」とこの意味において使い分けることがある。なお、「盤面」に手駒や手番を含めることもある。そのへんは前後の文脈から理解してもらいたい。
1 2 3 4 |
void user_test(Position& pos) { cout << pos; } |
この状態でやねうら王miniをビルドして、起動して、”user”コマンドを入力するとさきほどと同様に平手の初期盤面が表示される。
ここで見て欲しいのはPositionというのは現在の局面が格納されているクラスである。
やねうら王miniでは、ほとんどすべてのenum、クラスに対してストリーム演算子へのリダイレクトを定義してある。要するに、” cout << 何か; ” のようにすれば、標準出力に表示できるということである。
基本的には、このとき、USI形式で出力する。USIプロトコルで用いないものに関しては、独自の形式で出力する。
posには現在の局面(起動時には平手の開始局面)が入っているので、標準出力に平手の開始局面が出力されたわけである。
手番
手番は、ソースコード上、次のように書いてある。
1 2 |
// 手番 enum Color { BLACK=0/*先手*/,WHITE=1/*後手*/,COLOR_NB /* =2 */ , COLOR_ALL = 2 /*先後共通の何か*/ , COLOR_ZERO = 0,}; |
このへんは、Stockfishに倣っている。先手が黒(BLACK)、後手が白(WHITE)なので、手番は色(Color)だということのようである。
やねうら王miniでは、このソースコードで学習した学習者が、AperyやStockfishのソースコードを読もうとするときに苦難なく進めるように配慮し、なるべく関数名などをAperyとStockfishに合わせてある。AperyとStockfishとで呼び名が異なる場合は、Stockfishに倣うようにしてある。教育的な配慮をするのも、なかなか大変である。
手番の表示テスト
さきほど書いたようにやねうら王miniではほぼすべてのenumに対してストリーム演算子での出力をサポートしている。試しに手番が正しく表示されるかどうか調べてみよう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
void user_test(Position& pos) { Color c = BLACK; cout << c; } > user 先手 void user_test(Position& pos) { Color c = BLACK; c = ~c; cout << c; } > user 後手 |
operator ~()は、手番を反転させる。すなわち先手なら後手に、後手なら先手になる。
enumについているZEROとNBについて
さきほど、COLOR_NBとCOLOR_ZEROという定数が定義されていた。_ZERO のほうは、ゼロを意味する定数であり、_NBはNumberの略であり、末尾 + 1を表す。これらは配列を宣言するときや、ループにおいて用いられる。
例えば、次のような配列。
Color array[COLOR_NB];
例えば、次のようなループ。
for(Color c = COLOR_ZERO ; c < COLOR_NB ; ++c) { … }
このようにほとんどのenumにはXXX_ZEROという定数とXXX_NBという定数が定義されている。このようにするメリットは、
・配列が宣言しやすい
・ループを書きやすい
ということである。
「ループを書きやすい」については説明を要するかも知れない。
将棋プログラムにおいては、enumのレイアウト(定数の順番)を変更したりしたいことが多々ある。たとえば、いま、BLACKは0であるが、これをBLACKを1に変更した場合、どうだろうか。
このとき、次のようにループを書いていたのでは、意図通りループを回らなくなってしまう。
for(Color c = BLACK ; c <= WHITE ; ++c) { … }
ゆえに、forで要素すべてに対して何らかの処理をしないといけないような場合、XXX_ZEROとXXX_NBを用いて回る必要があるのだ。
enumに対する演算子について
上のように定義されたenumに対してインクリメントやデクリメント、加減算をしたいとこは多々ある。そのときに、都度、型のcastが発生するのは面倒くさい。そこで適切にoperatorを定義すべきであるが、すべてのenumに対して定義するのは面倒くさい。
そこで、Stockfishに倣い、enumに対して次のようなマクロを用いて一括してoperatorを定義してある。(Stockfishのものからは若干変更してある)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
shogi.h // +,-,*など標準的なoperatorを標準的な方法で定義するためのマクロ // enumで定義されている型に対して用いる。Stockfishのアイデア。 #define ENABLE_OPERATORS_ON(T) \ inline T operator+(const T d1, const T d2) { return T(int(d1) + int(d2)); } \ inline T operator-(const T d1, const T d2) { return T(int(d1) - int(d2)); } \ inline T operator*(const int i, const T d) { return T(i * int(d)); } \ inline T operator*(const T d, const int i) { return T(int(d) * i); } \ inline T operator-(const T d) { return T(-int(d)); } \ inline T& operator+=(T& d1, const T d2) { return d1 = d1 + d2; } \ inline T& operator-=(T& d1, const T d2) { return d1 = d1 - d2; } \ inline T& operator*=(T& d, const int i) { return d = T(int(d) * i); } \ inline T& operator++(T& d) { return d = T(int(d) + 1); } \ inline T& operator--(T& d) { return d = T(int(d) - 1); } \ inline T operator++(T& d,int) { T prev = d; d = T(int(d) + 1); return prev; } \ inline T operator--(T& d,int) { T prev = d; d = T(int(d) - 1); return prev; } \ inline T operator/(const T d, const int i) { return T(int(d) / i); } \ inline T& operator/=(T& d, const int i) { return d = T(int(d) / i); } ENABLE_OPERATORS_ON(Color) ENABLE_OPERATORS_ON(File) ENABLE_OPERATORS_ON(Rank) ENABLE_OPERATORS_ON(Square) … |
本来ならばstrong_typedefのような形名を作り出すためのtypedefを使うべきなのだが、C++11には存在しないので仕方がない。enum + 上のようなoperator定義マクロで凌ぐのは、わりと現実的でかつ、シンプルなソースコードになるようである。
筋・段・升
筋と段は次のように定義されている。筋はFile、段はRankと呼ぶ。これはチェスの流儀に従っている。
1 2 3 4 5 |
// 例) FILE_3なら3筋。 enum File { FILE_1, FILE_2, FILE_3, FILE_4, FILE_5, FILE_6, FILE_7, FILE_8, FILE_9 , FILE_NB , FILE_ZERO=0 }; // 例) RANK_4なら4段目。 enum Rank { RANK_1, RANK_2, RANK_3, RANK_4, RANK_5, RANK_6, RANK_7, RANK_8, RANK_9 , RANK_NB , RANK_ZERO = 0}; |
盤面上の升目はSquare型として次のように定義されている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// 盤上の升目に対応する定数。 // 盤上右上(1一が0)、左下(9九)が80 enum Square : int32_t { // 以下、盤面の右上から左下までの定数。 // これを定義していなくとも問題ないのだが、デバッガでSquare型を見たときに // どの升であるかが表示されることに価値がある。 SQ_11, SQ_12, SQ_13, SQ_14, SQ_15, SQ_16, SQ_17, SQ_18, SQ_19, SQ_21, SQ_22, SQ_23, SQ_24, SQ_25, SQ_26, SQ_27, SQ_28, SQ_29, SQ_31, SQ_32, SQ_33, SQ_34, SQ_35, SQ_36, SQ_37, SQ_38, SQ_39, SQ_41, SQ_42, SQ_43, SQ_44, SQ_45, SQ_46, SQ_47, SQ_48, SQ_49, SQ_51, SQ_52, SQ_53, SQ_54, SQ_55, SQ_56, SQ_57, SQ_58, SQ_59, SQ_61, SQ_62, SQ_63, SQ_64, SQ_65, SQ_66, SQ_67, SQ_68, SQ_69, SQ_71, SQ_72, SQ_73, SQ_74, SQ_75, SQ_76, SQ_77, SQ_78, SQ_79, SQ_81, SQ_82, SQ_83, SQ_84, SQ_85, SQ_86, SQ_87, SQ_88, SQ_89, SQ_91, SQ_92, SQ_93, SQ_94, SQ_95, SQ_96, SQ_97, SQ_98, SQ_99, // ゼロと末尾 SQ_ZERO = 0, SQ_NB = 81, … |
盤面の右上(1一)がSQ_11(= 0)、右下(1九)がSQ_19(= 8)、左上(9一)がSQ_91(=72)、左下(9九)がSQ_99(= 80)となっている。
このレイアウトはAperyに倣っている。ただ、Aperyでは、Stockfishに倣い、RANK_1が9段目を表すので、上の定義とは、上下逆のように見える。(実際は盤面上の同じ升を指している) 「AperyのRankの定義は(Stockfishに倣ったものだと思うけど)、わかりにくいので修正したほうがいいのでは」と平岡さんに言ったら、「いま修正を考えてます」とのことだった。
・2015/12/24 3:00 追記。平岡さん、この件、修正されたようです。
AperyのソースコードのSquare, File, Rank を将棋の筋と段に合わせました。今までチェスに合わせてて、Bonanzaもそうなってたのだけれど、やっぱ見難いから直しました(`・_・´)
— 平岡 拓也 (@HiraokaTakuya) December 23, 2015
筋・段→升の変換
1 2 3 4 5 6 7 8 9 10 |
void user_test(Position& pos) { File f = FILE_2; Rank r = RANK_8; Square sq = f | r; cout << sq; } > user 2h |
2筋と8段目を合成したので、sqは盤面の2八の升を指すはずである。
実行すると、2hと表示された。これはUSIプロトコルでの表示である。USIプロトコルでは、段をアルファベットで表す。(チェスは、筋をアルファベットで表すのでそこから来ているのか?) 1段目ならa、2段目ならb、…、8段目ならh、9段目ならiというわけである。
つまり、2hとは2八の升を表すので、これはこれで正しく動作していることがわかる。
Square型のoperator <<()は、USIプロトコルで表示するときのために、USI表記の文字列を表示させるようになっている。
ほとんどの場合において升目はこのように表示させているので、この升目表現に早く慣れて欲しい。
pretty()について
しかし、このようにUSIプロトコルに出力する表現形式をoperator << ()では第一に考えて出力するわけであるが、人間に見た目わかりやすい表現も別途用意している場合がある。例えば次のようにしてみよう。
1 2 3 4 5 6 7 8 9 10 |
void user_test(Position& pos) { File f = FILE_2; Rank r = RANK_8; Square sq = f | r; cout << pretty(sq); } > user 2八 |
このように、pretty()という関数が定義されている場合、それはenumの内容を人間に少しだけわかりやすい形式で表現する。
逆に、USI形式で出力する必要がないものに関しては、pretty()関数は用意してなくて、operator <<()が、pretty()の代わりとなっている。
※ デバッグのために何かのenumを表示させる場合は、そのへん(USI形式で出力するものか、しないものか)を考えながらpretty()で表示するかoperator <<()で表示するかを選ばなくてはならないが、基本的にはなんであれoperator <<()で表示することをお勧めする。USI形式に慣れていない人への救済策としてpretty()があるだけなのだから…。
enumに対するメンバー関数について
enumはメンバー関数を持てない。そこでglobal scopeに関数を置くしかない。
例えば、Rankのenumのメンバー(的なもの)については、rank_XXXという名前にしてある。
1 2 3 4 5 6 7 8 9 |
void user_test(Position& pos) { Square sq = SQ_28; Rank r = rank_of(sq); // 28の升の段は8段目 == (チェス表記で)h段目 cout << r; } > user h |
ここまでのまとめ
以上で、やねうら王miniではclassやstructではなくほとんどの場合においてenumが基本構造体の代わりと成すことがわかったと思う。これはこれで合理的な側面もあるので、慣れてもらいたい。
次の記事に続く。
すごい作りこんでますね。その情熱のコピーください。
生半可な覚悟ではできなそうだ。
自分は探索部に非常に興味があって、どうやって1Mnpsとかって数字になるかが気になっています。
それは置いといて、屋根さんの妙技が詰まったソースを早く読みたいです。
まさにお宝ですよ。鉱脈ですよ。
序盤で1MnpsはAperyでも出てるので、普通に(いまどきのコードで)作れば出るような…。
いやー、3流に何も見ないでそれ作れってのは無理がありますよ。
大体、データの持ち方をどうしようか位から不明なんですから。
木で持つべきなのか行列で持つべきなのか、それとも??
手を付ける前に詰まっています。Orz
>基本構造体の代わりと成すことがかわったと思う
いつもの間違いさがしです。
かわったーー>わかった・・・でしょうか?
修正しました!(`・ω・´)ゞ
> Aperyでは、Stockfishに倣い、RANK_1が9段目を表すので、
チェスではRANK1が盤面最下段なのでStokfishがそれに倣い、更にAperyがそれに倣った感じでしょうか?
> チェスに倣い、段をアルファベットで表す
チェスは、列がアルファベットではないかと。
どっかで聞いた話だと、1980年前後くらいに英語圏で将棋の普及を図った方が、将棋棋譜を英語表記する際、チェス式を基本に段を示す漢数字をアルファベットにする記法にしたそうで(例:7六歩→P-7f)、USIがそれに倣ったんではないかと。
…って、やねうら王miniとあまり関係ないですね。連載、楽しんで読ませていただいています。やねうら王miniの公開も心待ちしています。よろしくお願いします。
修正しときました!(`・ω・´)ゞ
本筋でない事項なのに修正していただきありがとうございます。
修正していただいた後>チェスは、段をアルファベットで表すのでそこから来ているのか?
私がわかりづらい書き方をしてしまったようで申し訳ないです。「チェスは、筋をアルファベットで表す」が正しいです。以下、チェス棋譜の座標について箇条書きします。
* チェス棋譜について(日英共通)
— 筋(file)がアルファベット、段(rank)が数字で、筋を先に記します。
— 筋は左から数え、段は下から数えます。
— 盤面の左下、左上、右上、右下が順に、a1,a8,h8,h1です。
んで、英語圏での将棋棋譜記法は、チェス記法をベースにしつつも日本語の将棋棋譜記法となるべく合わせようとしていて、筋を右から数え段を上から数え、筋を数字表記、段をアルファベット表記にしてまして(つまり、座標についての日本語との違いは段の漢数字をアルファベット化しただけ)、USIがそれに倣ったんでないかと。
とか、散々コメントしておいてアレですが、チェスと将棋の記法の違いは、Stokfishなどのチェスプログラムを見ながら将棋プログラムを作る人だけがわかってればいいことなので、教育的効果に優れるやねうら王miniが公開されれば、将棋プログラムを作る時にStokfishを参照する頻度が世界的に激減するでしょうから、「詳しいことはggrks」とかで良いかも。
> 「チェスは、筋をアルファベットで表す」が正しいです。
何度もスミマセン。修正しました(`・ω・´)ゞ
>今日はAperyのバグらしきものを一個見つけたのでまた平岡さんに報告しといたが、・・・
ソースリストを公開する蛮勇の持ち主であっても、それぐらいのメリットが無いとつまらないかとも思います。
Aperyがどんどん育っていく感…。
やねうら王の、main.cppと、usi.h、usi.cppあたりについての質問です。
main.cppは仮引数コマンドラインになってますが、それは私の知る限り一度しかargvは受け取る事が出来なかったと思うのですが、コードを見る限りUSIプロトコルを受けっとているように見えます。
結局の所USIプロトコルは毎回どこで受け取っているのでしょうか?
1. 標準入力からコマンドが送られてくるというのがUSIプロトコルで規定されています。
2. usi.cppのgetline()で標準入力から受け取っています。
3. argvから受け取るのは、事前にコマンドを渡したい時のためのもので、やねうら王の独自拡張です。(なぜかその後、Stockfishにも同等の機能が追加された)
ありがとうございます。