将棋の棋譜を自動解析して詰み逃しを見つける
将棋の棋譜から、詰みがあったのにも関わらず詰ませ損ねた局面を集め、詰将棋集を作って練習したいと思った。そこで、棋譜内に詰み逃しがあった時に Jupyter Notebook 上で表示されるようなコードを実装した。例として著作権の切れた古い棋譜を使って、どのように結果が得られるのか簡単に紹介する。
ここでの詰み逃しの定義は、相手玉に詰みが発生している場合でかつ、自分が最善手あるいは次善手(次善手も詰みになる場合)であるということにする。詰みが発生していることの基準は、エンジンが実行した結果で mate
が得られていることであるため、厳密な詰将棋としての王手の連続ではなく、相手玉が必至の状態で相手が王手ラッシュをかけてくるという場合や、無駄合いも含まれている。
ここでは江戸時代の棋譜から、1808年の大橋柳雪(中村喜多次郎)と大橋宗英(九世名人)の対局を対象にしてみたいと思う*1。この棋譜を解析すると、以下のように出力された。
Best Eval_value: mate 11 11手詰 Move No.: 101 Your Move: 5445UM 4五馬 Best Move: 0037GI 3七銀 ['S*3g', '4f4g', 'N*3i', '4g3g', 'R*3h', '3g4f', '4d4e', '4f5g', '3h5h', '5g6g', '8g7h']
上記のように出力されて、実際は何手詰であるか(これは探索時間によって余詰の方を出力することがあるので、必ずしも最短の手数とは限らない)と、最善手、また実際に指した手が表示される。この場合では、投了直前の101 手目の4五馬という手が詰みを逃しており、3七銀と指していれば11手詰めであったことを示している。また、表示される画像が詰将棋の出題図であるため、冷静な気持ちでこの局面を眺めてみて、再度指し手を考え直してみるのも良いだろう。
実際に解析に用いたコードは以下である。
from tqdm import tqdm import Ayane.source.shogi.Ayane as ayane from cshogi import CSA, KIF, Board, move_to_csa, move_to_usi import glob import pandas as pd import requests from IPython.display import Image,display_png usi = ayane.UsiEngine() usi.set_engine_options({"Hash":"128","Threads":"4","NetworkDelay":"0","NetworkDelay2":"0"}) usi.connect("/content/YaneuraOu-6.00/source/YaneuraOu-by-gcc") for (_, kifu) in df.iterrows(): board = Board(kifu.sfen) for (idx, move) in enumerate(tqdm(kifu.moves)): usi.usi_position("sfen {}".format(board.sfen())) if kifu.types == "kif": board.push_usi(move) else: board.push(move) url = "http://sfenreader.appspot.com/sfen?sfen={}".format(board.sfen()).replace("+", "%2B") r = requests.get(url, params={}, stream=all) display_png(Image(r.content)) usi.send_command("multipv 2") usi.usi_go_and_wait_bestmove("btime 0 wtime 0 byoyomi 2000") if ayane.UsiEvalValue.is_mate_score(usi.think_result.pvs[0].eval) and (turn == (idx2 % 2 == 0)): bestmove_usi = usi.think_result.bestmove board.pop() prev_move = move_to_csa(board.peek()) url = "http://sfenreader.appspot.com/sfen?sfen={}&lm={}".format(board.sfen(), prev_move[2:4]).replace("+", "%2B") r = requests.get(url, params={}, stream=all) board.push_usi(bestmove_usi) bestmove = move_to_csa(board.peek()) board.pop() if kifu.types == "kif": board.push_usi(move) else: board.push(move) move = move_to_csa(board.peek()) move_usi = move_to_usi(board.peek()) next_hand_mate = ayane.UsiEvalValue.is_mate_score(usi.think_result.pvs[1].eval) if not (bestmove == move_usi or (next_hand_mate and move_usi == next_hand_mate)): display_png(Image(r.content)) print("Best Eval_value: ", usi.think_result.pvs[0].eval.to_string()) print("{}手詰".format(usi.think_result.pvs[0].eval.to_string().split(" ")[1])) print("Move No.: ", idx) print("Your Move: ", move, human_readable_move(move)) print("Best Move: ", bestmove, human_readable_move(bestmove), usi.think_result.pvs[0].pv.split(" "))
CSA フォーマットと USI フォーマットについて
出力の指し手を人間が読みやすい形に整形することを考える。まず、将棋エンジンとの通信では USI フォーマットと呼ばれる記法を用いて指し手を通信している。これは、 3c3d, 7g7f, 4c4d, 4g4f, 4a3b, 3i4h, 3a4b
のように座標のみで表現している。それに対して CSA フォーマットでは、7776FU
のように移動前後の座標と、その駒の名前も載っている。この情報をもとに、上で用いているような human-readable な指し手を表示することを考えたい。
def human_readable_move(csa): NUMBER_JAPANESE_KANJI_SYMBOLS = [ None, "一", "二", "三", "四", "五", "六", "七", "八", "九", "十" ] PIECE_JAPANESE_SYMBOLS = [ '', '歩', '香', '桂', '銀', '角', '飛', '金', '玉', 'と', '成香', '成桂', '成銀', '馬', '龍' ] PIECE_SYMBOLS = [ '', 'FU', 'KY', 'KE', 'GI', 'KA', 'HI', 'KI', 'OU', 'TO', 'NY', 'NK', 'NG', 'UM', 'RY' ] piece = PIECE_SYMBOLS.index(csa[4:6]) return csa[2] + NUMBER_JAPANESE_KANJI_SYMBOLS[int(csa[3])] + PIECE_JAPANESE_SYMBOLS[piece]
しかしこのコードでは、例えば銀が4八と6八にいるときに「5七銀」が「5七銀右」なのか、「5七銀左」なのかの区別をつけることができない。あるいは、「78成桂」があるときに、その駒が元々成駒であったか、今ちょうど成ったかを区別することができない。これは、1手前の盤面情報がないと再現できないためである。したがって、あくまで上記のコードはパッチワークであることに注意されたい。厳密な指し手を得るためには、盤面情報を利用することとなるが、それは今後の課題としたい。
*1:平手ではなく飛車落ちである