500万局の詰将棋

やねうらおさんの233万局面の定跡ファイルを最近使わせてもらったのだが、そのとき、彼のブログで5年も前に3手詰めから11手詰めまでの実戦詰将棋局面を500万例ほど生成して、これをテキストファイルを圧縮した形で無償公開されていることに気が付いた。で、 将棋盤に表示できるような機能が欲しい旨のコメントをしている読者もいるみたいだし, これも早速自分のサイトのページに応用させてもらおうかと思った次第である。

で、まずは提供されている圧縮ファイルだが、解凍するとmate3.sfen, mate5.sfen, mate7.sfen, mate9.sfen, mate11.sfen の5つのファイルがでてくる。 それぞれテキストファイルであり、中身はsfen 書式で書かれた局面情報が1行1局面で記述されている。

これをweb siteのページにランダム、あるいは手数と行番号を指定して将棋盤のグラフィックスとして表示させたいわけである。

すべてのデータをブラウザに読み込ませるわけにはいかないので、データ自体はサーバー側に持たせ、ブラウザからは手数と行番号を指定してサーバーに問い合わせ、該当するSFEN一行を取得させて将棋盤上の駒として描画させる、という手法で実現させる。 サーバーでのデータ検索を最適化させるためにmysqlデータベースのテーブルにデータを格納して使う。

データテーブルは一つとして、コラムは手数と問題、各手数の問題カウント、および 通し番号として記録するとしてテーブル構成は以下となる。

FieldTypeNullKeydefaultextra
idINTNOPRIMARYnullauto increment
sfenvarchar(120)YESnull
moveintYESMULnull
rowintYESMULnull

このテーブルをまず作っておき、ここにテキストファイルからデータを流し込むわけだ。 で、今度こそPythonを使って見ようとPyCharmを立ち上げ(個人で使う分には無料なんだよね)ブラウザの検索機能で情報を拾いまくりながら、以下のプログラムを作った。

move と counterで検索することになるので、これらのフィールドはインデックスを作成しておく。

20行以上のパイソンを書いたのはおそらく初めてだが、 なるほどユーザーが多いわけだ。

このパイソンスクリプトを走らせてみる。 上のコードでは各行にスラッシュが8個ある、のと、スペースで分解したアレイ素子の因子が4つになる、というのを valid SFENの簡易判定基準とし、それ以外の行はスキップする構造にしてあったのだが、なんとここで10件近くのスキップが発生した。 テキスト行の情報も表示するようになっていたので ファイルをのぞいてみると、sfenが2行くっついた、つまり改行が抜けている個所が何件かあるのが分かった。 プログラムで生成したはずのファイルに何故にこのようなイレギュラリティが発生するのか不思議ではある。 とにかく手作業でこれらの行を分割し、テーブルをいったん削除して再度の挑戦を行った。Pythonのmysql connectorが優秀なのか、パソコンのハードを更新したからなのかはよくわからないが、500万件10分もかからず終わってしまった。

実は500万件なかったのである。mysqlのデータベースが言うには実際には以下の件数の局面が入っていた。ただこれで目くじら立てるのは野暮というものだ。

3手 998,405件
5 手998,827件
7 手999,071件
9 手999,673件
11 手999,998件

サーバーにPHPでAPIポイントをつくり、例えば /api/mate/3/1245というようなHTTPリクエストをサーバーに送れば、3手詰め、1245行目のsfenがjson formatでかえってくるような仕組みを作る。

ブラウザ側のほうはReactとほぼ同じように使えるPREACTをViteとTypeScriptのTool chainを使用しバタバタと書き上げて、出来上がったのがこのページ。 上の数字をもう一度よく見てもらいたいが、 200問くらいの問題集でも一冊全部解くのはなかなか根気がいるのに、いったい誰がこの圧倒的な数の暴力に立ち向かうというのだ?(ちなみに私は高校時代に買った内藤詰将棋200選をまだ全部解いてない)

やねうらおさんのブログにもかいてあるのだが、これは妙手が入った詰将棋とは違って実戦詰将棋的な問題集になる。 ほとんど並べ詰めのようなものもあれば、少し考える必要のあるものもある。 しかしなのだ。 例えば、この局面 ランダムにピックアップしたものだが、

9手詰め 第88,592問目

最初に2二金打ち、同金としてから4二桂成 同玉以下は 尻にと金をすべらせていってからの飛車成りまでの送り詰めで9手と言っても難しい詰めではない。 詰将棋だと言って出されれば級位者でも解ける問題だ。 ただ実戦となるとこの局面で4五の桂馬が5三と3三を封鎖しており、さらに金を捨てて、3一の金をどかせれば尻金で詰む、そして4一と金のとき5二玉と逃げれば6一飛成で早く詰む、と局面の駒配置がよく見えなければ詰めようとも思わないわけで、実際これくらいの局面で3切れのShogi warsでは初段、下手すると2段レベルとの対戦でも手駒の金は温存したいと、初手から4二桂成と角を取ってくる可能性は自分の経験から言ってかなり高い。

だとすると、この類の局面を大量にながめて勘を鍛えておくことは棋力向上の役に立つのかもしれない。

それはさておき、ここまでできたなら将棋エンジンもつけて回答も導きだせるようにしたらどうだと言われそうだが、残念ながらそちらのほうの知見は自分はまったく持っていないのであった。思うにサーバーにエンジンを実装してというかバーチャルマシンを一台、それもパーフォーマンスの高いやつを立ち上げて将棋エンジンを常駐させ、これのAPIをたたいて正解を求めるというようなイメージなんだが、いかにもリソース費用がかかりそうだ。lishogiのサイトなどは実現しているみたいだが、どうしているのだろうか。 そのうちだれかが無料のAPIを提供してくれないかと無想するわけである。

AIにコードを書いてもらって失敗した話

将棋の局面を記述する書式にSFENという記述方式がある。
これは盤上の駒の配置、手番、先手と後手の持ち駒、手数などがコンパクトに文字列にまとめられたもので、Kyokumen.jpの局面検索に使われていたり、将棋エンジンの定跡ファイルにも使われていたりする。

SFENの書式についてはこの記事にうまく説明されている。

今回、YaneuraouさんがMIT Licenseで新たに公開された233万手の定跡ファイルをみたところ、後手番の局面がなく、先手後手盤面を反転して検索する方法がとられている。つまり、 例えば初期画面から後手が3四歩と指した局面が先手番として登録されている。 屋根裏王のエンジンにはflippedBoardというパラメーターがあり、これをOnするだけで対応するよう設計されているのだが、定跡ファイルをブラウザで将棋盤上に表示させる自分のWebページに追加するためには、後手の手番の時に検索をかけるSFENの局面を反転させてサーバーに問い合わせる必要が生じた。サーバーから帰ってくる候補手も同様に、例えば2六歩を8四歩などと反転させる必要がある

最初の局面を反転させる部分、TypeScript関数として用意すると以下のようになる。

 

このコードで何をやっているかというと

1.まずは SFEN文字列をスペースをデリミターとして文字列アレイに分解する。
2.それぞれのアレイ因子を board, turn, hands, moveCountという変数にわりあてる。 ちなみにMoveCountは今回の定跡ファイルに使用するにはゼロに置き換える必要がある。(上のコードでは未反映)
3.board(駒の盤面位置情報)をさらにdelimeter ‘/’ で各列に分解したアレイを生成
4. array.reverse()を使って列順をひっくり返す。上段の列が下段に、下段の列が上段になる。
5.さらに、さらに各列の文字列を一文字づつに分解してアレイを作る。
6.大文字の駒(後手の駒)を小文字(先手の駒)、小文字の駒(先手の駒)を大文字に置換する
7.array.reduce()を使って、+シンボル(成り駒)因子と次の駒の因子を繋げて一つのアレー因子にまとめる

8. これを.reverse()で左右反転
9. 各列のアレイを文字列に変換
10。各列の文字列アレーをを’デリミター/’で接合し、一つの文字列を生成。→flippedBoard
11. 手番もひっくり返す (flippedTurn)
12. 持ち駒を先手と後手入れ替える (pivotHands) この部分、使ったロジックは以下のとおり

B3Ps14pを S14Pb3pとしたい。
まずは大文字小文字を入れ替える
b3p2S14P (flipped)
最初の大文字のインデックスを取ればよいと思えるが、数字がついた場合(2S)となっていた場合はNG
なので文字列を逆転させてみる
P41S2p3b reversed
これで最初の小文字のインデックスを取る。上の例だと5。文字列長さ8なので 8-5=3
secondPart =flipped.slice(0,3) = b3p
firstPart = flipped.slice(3) =2S14P
これを組み立てる
final = firstPart+secondPart = 2S14Pb3p
うまくいっているのでは?

先手の持ち駒だけの場合 例えば 2P
小文字のインデックスはマイナス1 この場合は処理をスキップ
後手の持ち駒だけの場合 例えば14p 逆転文字列での小文字のインデックスは0 この場合も処理をスキップ

13 .flifppedboard flippedTurn flippedHands moveCountを一つの文字列につなぎ合わせ、この値を返す
とまあ、書いてしまえば簡単な作業の連続だが、実はこのコード、最初AIに聞いて提示されたものが動かなくて、修正を入れ始めたら、結局七割がたは書き直してしまった、というものになる。

最初、Brave Brouserの検索窓に ”Given sfen string, write a javascript function that returns flipped sfen” と打ったらあっさり回答をよこしたのでこれをTypeScriptに書き直し、そのまま定跡ファイルに使ってみたのだた、局面検索がまったくヒットしなかった。  コードを解析して分かったことはAIがくれた回答は上記のステップから 7,8そして12の処理が抜けていた。その結果盤面の上下反転はしたが、そのあとが左右反転をしていないため、ミラーイメージになってしまい、さらに持ち駒があった場合は先手と後手が逆のまま。 Webページの仕組みはこのsfenをサーバーに送ってサーバーのデータベースからこのsfenに対応するmoves情報を返してもらい、次の指し手の候補をリストアップするというものなので、ミラーイメージの局面照会にはヒットする候補手はありませんという答えしか返ってこない。

AIはSFENが何たるかをあまり理解できないまま、WEBに落ちている情報をかき集めて使ったようで、プロンプトで、SFENの定義から始め、期待されるアウトプットのイメージをしっかり伝えておかなければだめだったか。

逆に良いねと思ったのはアレイやオブジェクトの関数をチェイニングして記述する、いわゆるFunctionalな書き方になっていてスマートである。 これは真似したい。

とにかく、AIに仕事をさせたら、今の時点では検証が大事なわけだが、それなら最初から提供されたコードは参考までに留めておいて、実際に使うコードは自分で書いたほうが楽しいかもね(仕事でプログラミングしている人はこうは言えまい)。

追記:Yaneuraouさんからflipsfenをpythonで作成した時の顛末のリンクを教えていただいた。 https://yaneuraou.yaneu.com/2023/12/15/chatgpt-wrote-a-program-to-flip-a-shogi-board/  同じようなところでハマっているんだ、と可笑しくなった半面。 ちょっと待て、これは2年前の記事。 これを読んでいたらこんなに時間を使わなくてもよかったじゃん とも思った次第。