ステイルメイトという特殊なルール
以前の記事(「将棋の評価関数とチェスの評価関数-1」、「将棋の評価関数とチェスの評価関数-2」、「将棋の評価関数とチェスの評価関数-3」)で自作の将棋AIをチェスAIに書き換える作業をしようとして、チェスのステイルメイトという特殊なルールに対応するために将棋AIを書き換える必要が出てきたことを書きましたが、分かりづらかったと思うので実際の開発の過程でどういう作業が必要になったか、辿った経緯をもう少し詳しく書こうと思います。
そしてこの開発を通じて、テスト駆動開発という開発手法の有用性を身を以て感じたということを書いてみます。
2種類の詰み判定(ゲーム終了判定)メソッド
詰み判定(ゲーム終了判定)のやり方が2通りあって、チェスのステイルメイトのルールに対応するにはやり方を変えなければならなかったということは以前の記事に書いたとおりですが、なぜ「相手玉を取ったらゲーム終了」という将棋では問題がなかった方法を、チェスだと変える必要が出てきたのでしょうか。
ゲームの局面(ゲーム木)を辿っていって(先読みしていって)、先手が着手したAという局面に行き着いたとします。その局面から後手の手番でまたB1、B2、B3…と幾つもの局面が枝分かれしていくわけですが、その後手の着手したBの全ての局面でもう一手先のCの局面で相手玉(後手玉)を取ることが出来ることが分かって初めて評価値が最大値(先手勝ち)になる訳ですが、この相手玉を取ることが出来ると判明したCの局面の中には後手が非合法手を着手したケースも混じっているわけです。非合法手というのは角の効き筋を止めていた味方の駒を動かしたケース(王手放置)や、後手玉が先手の駒の効いている場所に動いたケース(自爆)です。将棋なら合法手であっても非合法手であっても、後手玉を取ってしまえばいいのでCの局面で先手勝ちの評価は変わりません。しかし、チェスの場合はBの局面で非合法手しか着手できなかった場合は引き分け(ステイルメイト)と判定しなければなりません。将棋では局面Bで選んだ後手の手が合法手だろうが非合法手だろうが、Cの局面で先手が玉を取れれば先手勝ちと判断出来るのに対して、チェスの場合はBの局面で非合法手しかない場合はステイルメイト、合法手と非合法手が混じっている場合(=合法手は存在するけど後手玉が取られる)は先手の勝ちと場合分けする必要があるということです。つまり、局面Bから先読みして後手玉が取れるから先手勝ちと判断することは出来ないので、一手先読みするのではなく局面Bの段階で手番と駒の効きの情報を使って後手に非合法手しか存在しない(ステイルメイト)かどうかを判断する必要があります。と言うことでチェスではプログラムを書き換える必要がありました。
将棋の場合は「相手玉を取ったらゲーム終了」とするやり方と「手番と局面情報からゲーム終了判定する」やり方、どちらのやり方を採用しても対応できるので、将棋のソースコードを使って比較してみます。
現在githubに公開している3三将棋(簡易版)の詰み判定メソッド(check_tumiメソッド)は以下のようになっています。
check_tumi = (board) ->
first = 0; second = 0
kings = (v for v in board.pieces when v.kind() == 'Ou' && v.turn == Const.FIRST)
switch kings.length
when 2
return Const.MAX_VALUE
when 0
return Const.MIN_VALUE
else
for v in board.pieces
first += v.omomi() if v.turn == Const.FIRST
second += v.omomi() if v.turn == Const.SECOND
return (first - second)
先手の玉が2枚あれば先手が勝ちの評価値、0枚なら後手の勝ちの評価値を返して、それ以外は局面の静的評価値を返してゲームを続行します。
つまり玉が取られたかどうかでゲームの終了判定をしています。
一方で3三将棋を除く将棋関連アプリとチェスアプリでは以下のようになっています。
check_tumi: (turn, utifudume_flg = null) ->
#:
# 前処理省略
#:
# 先手が王手掛かっていて指したのが先手なら後手勝ち
if f_oute && turn == Const.FIRST
return [true, Const.MIN_VALUE]
# 後手が王手掛かっていて指したのが後手なら先手勝ち
else if s_oute && turn == Const.SECOND
return [true, Const.MAX_VALUE]
# 先手玉が動けなくて王手が掛かっていたら後手勝ち
else if f_canmove == false && f_oute
# 後手が王手放置していて指したのが後手なら先手勝ち
if s_oute && turn == Const.SECOND
return [true, Const.MAX_VALUE]
else if utifudume_flg
return [true, Const.UTIFUDUME]
else
return [true, Const.CHECKMATE]
# 後手玉が動けなくて王手が掛かっていたら先手勝ち
else if s_canmove == false && s_oute
# 先手が王手放置していて指したのが先手なら後手勝ち
if f_oute && turn == Const.FIRST
return [true, Const.MIN_VALUE]
else if utifudume_flg
return [true, Const.UTIFUDUME]
else
return [true, Const.CHECKMATE]
# 上記条件に当てはまらなければ、ゲーム継続で評価値を返す
for v in @pieces
first += v.omomi() if v.turn == Const.FIRST
second += v.omomi() if v.turn == Const.SECOND
return [false, (first - second)]
チェスの場合はこのソースに加えてステイルメイトのケース(else if文)を追加しており、打ち歩詰めのif文はありません、でもその他の部分は将棋と同じです。
現在の手番と局面の情報だけを使ってゲーム終了したかを判断し、ゲーム終了していなければ局面の静的評価値を返します。玉を取る一手前の局面でゲームの終了判定をしようとすると前述のソースとはかなり違ってくることが分かると思います。それにしても10個足らずの条件分岐(if文)で済んだのは予想外に少なかったなぁと思ってます。
実際の開発の順番としては、まず将棋(3三将棋アプリ以外)の詰み判定(ゲーム終了判定)を書き換えてテストした後、それをチェスアプリに適応しました。なぜ3三将棋は変更しなかったかについては以前の記事で書いたように局面の評価より深く読むことに注力したかったからです。
もうアプリをリリースしてから何年も経つので、このコード自体には問題(バグ)はないと思っています。ただ、ここにソースを載せていないステイルメイトの判定部分に関しては、まだちょっと不安があったりするのですが、それに関しては別の記事を書くかもしれません。
演繹法と帰納法
将棋というゲームが終了したかどうかを判断する場合、一番単純なやり方としては、頭金で詰んだケース、吊るし桂で詰んだケース等、詰みの形を一つ一つ定義して行き、それ以外はゲーム継続とするやり方を思いつきますが、そんなことをしていけばif文が幾つになるか見当もつかない程大変な作業を強いられます。とても人間が出来る作業ではないと思います。でも、機械学習型のAIの場合はその作業をやっているわけです。一つ一つの詰みのケースを学習して帰納的に法則性を見出してゲームが終了したかどうかを判断するわけです。
では、自分は人間なんだからもっと演繹的にゲーム終了の条件を定義してみようと思って、上の10個足らずのif文からなるゲーム終了条件をパッと思いつく人がいるでしょうか?頭金や吊るし桂の詰み形をどんどん抽象化していき、頭の中だけでその条件文を構成できる人は少ないと思います。自分の場合もいきなり上で書いた10個足らずの条件文を思いついたわけではなく、一つ一つテストしていきながら条件を付け加えたり削除したりを繰り返しながら所謂スパイラルモデルで作り上げた感じです。決してウォーターフォールモデルのように上から下に流れ作業のように開発が進んだわけではありません。徐々に抽象度を上げていくという感じではなく、アイデアを出しては試していくの繰り返しという感じでした。将棋の世界に直感精読という言葉がありますが、この言葉プログラミングにも通じると思います。論理的思考というのは精読の部分に当たりますが、アイデアを出す直感の部分が結構大事だと思っています。将棋では「見えないと読めない」なんて言葉もありますよね。いくら局面を先に読んでいく力があっても、手が見えないと好手は指せません。
プログラミングは設計行為
TDD(テスト駆動開発)とはアジャイル開発における一つの開発手法とされていて、全開発工程の後半にテスト工程を設けるウォータフォールモデルとは全然違う代物です。まずテストを書いて実行し、リファクタリングしてまたテストの繰り返しです。ソフトウェア開発に於いて、巷ではいきなりコードを書き出す人を批判する人がいますが、コードを書きながら設計しているので極々自然な成り行きだと思います。業務に精通しているから設計書を書ける訳ではなく、将棋に精通しているからと言ってコンピュータへの指示書をいきなり書ける訳でもないのです。やってみなければ分からないという部分を、効率よく実装する手段としてTDDは非常に優れていると思います。誤解のないように付け加えておきますが、将棋のプログラムを作る際は、将棋に精通している人の方がそうじゃない人よりも効率良く作れるとは思っています。そういう人でも試行錯誤が必要だってことです。
ただテスト駆動開発と言っても、先程の例で言えば、頭金の詰み判定のテストを書く、吊るし桂の詰み判定のテストを書くというのは設計行為とは言えないでしょう。これは所謂普通のテストであって、TDD(テスト駆動開発)とはもっと上流工程にテストを活用していくと言うことだと思います。メディアや情報誌に振り回されるのではなく、実体験としてアジャイル開発をやってみる人が増えることを望んでます。
それとTDDは、効率よく設計・開発を進められるだけではなく、何か新しい機能を追加する際にも「以前は上手く動いていたのに、おかしくなった〜」という所謂デグレード(リグレッション)の防止にも書き溜めたテストが役立ちます。実際に将棋関連アプリを「相手玉を取ったらゲーム終了」というロジックで作っていたテストを、「手番と局面情報からゲーム終了判定する」ロジックに書き換えた後も、書き換える前に使っていたテストスクリプトをそのまま動作確認に使えました。
現在リリース中のマンカラアプリは結構長い期間バグのあるバージョンがリリースされていたのですが、これはDartからC++に書き換える時にテストコードを省略したために起きたことです現在はGoogleTestを導入して、C++でもテストコードを書いているので今後は大丈夫だと思います
スマホアプリについて
もう何年も前の話ですが、現在Amazonで公開しているチェスアプリは以前はGoogle Playにも公開していましたが、よく分からない理由でGoogleから削除されました。また、現在Google Playで公開しているミニチェスアプリは、これもよく分からない理由でAmazonの審査で落とされました。
去年はこういうニュースもありましたし、AmazonもGoogleもAppleもアプリ公開のプラットフォーム利用に対する規制が数年前よりさらに強くなってきてる気がするので、全部PWA(Progressive Web Apps)化しようかなんて考えていますが、スマホアプリでさえそれほど多くの利用者がいるわけでもないのに、PWA化してもユーザーが増えることはないでしょう。
でも自分のアプリの多くはJavaScript製なのでPWA化=ソース公開にもなるのでニーズがあるかもしれませんし、PWA化すればスマホよりハイスペックなPCで動かせるのでソースを書き換えて読みの深さの数値を変えたりするとスマホより深く読めるようになったりします。もちろんユーザーがダウンロードして自分用に作り変えることも出来ます。自分でも少し試したりしたのですが、PWA化したらその辺りの話も書いてみようと思います。