(連続企画)Pythonで将棋ソフト制作2のその1-1手先読みの補足
将棋ソフト作成の連続企画の一つです。
上記の動画で作成したソースコードです。
ソースコード内の「★」の部分は、上記の動画で説明できていません。
今後、ライブ配信で説明する予定です。
(ライブ配信をしたら、この部分の文章を書き直します)
main.py
import cshogi import random # 文字列の検索のために正規表現を使う。 import re # 多次元リストの並べ替え用 from operator import itemgetter import time # 定数 TSUMI_SCORE = 30000 # 評価値の最大値。(指し手がない時の評価値) # グローバル変数 global_my_turn = "先手" # 自分(このエンジン)の手番。"先手"か"後手"の文字列。 def koma_count(board): """ 局面の先手、後手の各駒の数を数えてdictで返す。(王を除く) Args: board (cshogi.Board): cshogiでの局面 Returns: koma_cnt (dict): 各駒とその数 例: {'先手の歩': 7, '先手の香': 2, '先手の桂': 1, '先手の銀': 3, '先手の角': 0, '先手の飛': 1, '先手の金': 1, '先手のと': 1, '先手の杏': 0, '先手の圭': 0, '先手の全': 0, '先手の馬': 0, '先手の竜': 0, '先手持ち駒の歩': 0, '先手持ち駒の香': 0, '先手持ち駒の桂': 0, '先手持ち駒の銀': 0, '先手持ち駒の角': 0, '先手持ち駒の飛': 1, '先手持ち駒の金': 1, '後手の歩': 5, '後手の香': 2, '後手の桂': 2, '後手の銀': 0, '後手の角': 2, '後手の飛': 0, '後手の金': 1, '後手のと': 0, '後手の杏': 0, '後手の圭': 0, '後手の全': 0, '後手の馬': 0, '後手の竜': 0, '後手持ち駒の歩': 5, '後手持ち駒の香': 0, '後手持ち駒の桂': 1, '後手持ち駒の銀': 1, '後手持ち駒の角': 0, '後手持ち駒の飛': 0, '後手持ち駒の金': 1} """ board_str = str(board) # 局面の文字列化 # cshogiのソースコードposition.cppに駒の種類が書いている。 koma_cnt = {"先手の歩": 0} # 初期化 # 先手 koma_cnt["先手の歩"] = board_str.count("+FU") koma_cnt["先手の香"] = board_str.count("+KY") koma_cnt["先手の桂"] = board_str.count("+KE") koma_cnt["先手の銀"] = board_str.count("+GI") koma_cnt["先手の角"] = board_str.count("+KA") koma_cnt["先手の飛"] = board_str.count("+HI") koma_cnt["先手の金"] = board_str.count("+KI") koma_cnt["先手のと"] = board_str.count("+TO") koma_cnt["先手の杏"] = board_str.count("+NY") koma_cnt["先手の圭"] = board_str.count("+NK") koma_cnt["先手の全"] = board_str.count("+NG") koma_cnt["先手の馬"] = board_str.count("+UM") koma_cnt["先手の竜"] = board_str.count("+RY") # 先手の持ち駒 line_str = str(re.search(r"P\+00FU.*\n", board_str)) koma_cnt["先手持ち駒の歩"] = line_str.count("FU") line_str = str(re.search(r"P\+00KY.*\n", board_str)) koma_cnt["先手持ち駒の香"] = line_str.count("KY") line_str = str(re.search(r"P\+00KE.*\n", board_str)) koma_cnt["先手持ち駒の桂"] = line_str.count("KE") line_str = str(re.search(r"P\+00GI.*\n", board_str)) koma_cnt["先手持ち駒の銀"] = line_str.count("GI") line_str = str(re.search(r"P\+00KA.*\n", board_str)) koma_cnt["先手持ち駒の角"] = line_str.count("KA") line_str = str(re.search(r"P\+00HI.*\n", board_str)) koma_cnt["先手持ち駒の飛"] = line_str.count("HI") line_str = str(re.search(r"P\+00KI.*\n", board_str)) koma_cnt["先手持ち駒の金"] = line_str.count("KI") # 後手 koma_cnt["後手の歩"] = board_str.count("-FU") koma_cnt["後手の香"] = board_str.count("-KY") koma_cnt["後手の桂"] = board_str.count("-KE") koma_cnt["後手の銀"] = board_str.count("-GI") koma_cnt["後手の角"] = board_str.count("-KA") koma_cnt["後手の飛"] = board_str.count("-HI") koma_cnt["後手の金"] = board_str.count("-KI") koma_cnt["後手のと"] = board_str.count("-TO") koma_cnt["後手の杏"] = board_str.count("-NY") koma_cnt["後手の圭"] = board_str.count("-NK") koma_cnt["後手の全"] = board_str.count("-NG") koma_cnt["後手の馬"] = board_str.count("-UM") koma_cnt["後手の竜"] = board_str.count("-RY") # 後手の持ち駒 line_str = str(re.search(r"P-00FU.*\n", board_str)) koma_cnt["後手持ち駒の歩"] = line_str.count("FU") line_str = str(re.search(r"P-00KY.*\n", board_str)) koma_cnt["後手持ち駒の香"] = line_str.count("KY") line_str = str(re.search(r"P-00KE.*\n", board_str)) koma_cnt["後手持ち駒の桂"] = line_str.count("KE") line_str = str(re.search(r"P-00GI.*\n", board_str)) koma_cnt["後手持ち駒の銀"] = line_str.count("GI") line_str = str(re.search(r"P-00KA.*\n", board_str)) koma_cnt["後手持ち駒の角"] = line_str.count("KA") line_str = str(re.search(r"P-00HI.*\n", board_str)) koma_cnt["後手持ち駒の飛"] = line_str.count("HI") line_str = str(re.search(r"P-00KI.*\n", board_str)) koma_cnt["後手持ち駒の金"] = line_str.count("KI") return koma_cnt def komadoku_evaluate(koma_cnt): """ その局面の駒得による評価値を計算する。 Args: koma_cnt(dict): 各駒とその数 Returns: phase_value (int): その局面の駒得による評価値 """ # ★変更の必要あり? 1手先読みだと、駒割り評価だと打たない。 koma_value = {"先手の歩": 75, "先手の香": 175, "先手の桂": 275, "先手の銀": 475, "先手の角": 775, "先手の飛": 875, "先手の金": 575, "先手のと": 575, "先手の杏": 575, "先手の圭": 575, "先手の全": 575, "先手の馬": 1175, "先手の竜": 1275, "先手持ち駒の歩": 125, "先手持ち駒の香": 225, "先手持ち駒の桂": 325, "先手持ち駒の銀": 525, "先手持ち駒の角": 825, "先手持ち駒の飛": 925, "先手持ち駒の金": 625, "後手の歩": -75, "後手の香": -175, "後手の桂": -275, "後手の銀": -475, "後手の角": -775, "後手の飛": -875, "後手の金": -575, "後手のと": -575, "後手の杏": -575, "後手の圭": -575, "後手の全": -575, "後手の馬": -1175, "後手の竜": -1275, "後手持ち駒の歩": -125, "後手持ち駒の香": -225, "後手持ち駒の桂": -325, "後手持ち駒の銀": -525, "後手持ち駒の角": -825, "後手持ち駒の飛": -925, "後手持ち駒の金": -625} phase_value = 0 # 出力する評価値を初期化 for key in koma_cnt.keys(): # 駒の種類 * その駒の数 phase_value += koma_value[key] * koma_cnt[key] return phase_value def order_list(move_lst): """ cshogiから返ってきた合法手のリストをオーダリングする。 今のところ、とりあえずランダム。 これで、アルファベータカットしても 事前にシャッフルしているので、ランダムで指す。 Args: move_lst(int list): 合法手のリスト Returns: int list: オーダリング(並べ替えた)した合法手のリスト """ random.shuffle(move_lst) return move_lst def evaluation(board): """ 評価関数。 Args: board (cshogi.Board): cshogiでの局面 Returns: evaluation_value (int): 評価値 """ # 駒割りの評価値計算。 koma_cnt = koma_count(board) evaluation_value = komadoku_evaluate(koma_cnt) return evaluation_value def serch_next(board, move_lst): """ 探索。合法手リストを評価順に並べ替える。 今のところ、とりあえず駒割り評価値の最高値順。 Args: board (cshogi.Board): cshogiでの局面 move_lst (int list): 合法手のリスト。cshogiの数字。 Returns: evaluation_phase (int list): 合法手(数字)と評価値のリスト。評価値の降順。 (例) [ [524418, 650], [1252401, 450], [347398, 250] ] """ global global_my_turn # グローバル変数。手番。 # 各合法手(数字)と評価値(正の数字だと先手有利)のリスト # (例) # [ [524418, 650], [1252401, 450], [347398, 250] ] evaluation_phase = [] # 合法手を順番に指す。 for push_num in move_lst: board.push(push_num) # 合法手リストの0番目から指す。 # 詰みがないか、先読み相手番(2手先)の合法手をリスト化する。 move2_lst = list(board.legal_moves) # 先読み相手番の指し手がない、詰み(mate)のとき勝ち。 if len(move2_lst) == 0 and global_my_turn == "先手": evaluation_value = TSUMI_SCORE # 評価値の最大値。 elif len(move2_lst) == 0 and global_my_turn == "後手": evaluation_value = -TSUMI_SCORE else: # 評価関数を使い、局面から評価値を計算する。(今は駒割りのみ) evaluation_value = evaluation(board) # 局面と評価値を戻り値用のリストに加える evaluation_phase.append([push_num, evaluation_value]) # 局面を元に戻す。 board.pop() # リストを評価値の降順で並べ替える。 if global_my_turn == "先手": # リストの評価値でソートする。 # itemgetter(1)の1は、各要素のリストの1番目の要素という意味。 # 0番目は指し手、1番目は評価値。 # 参考: ソート HOW TO — Python 3.10.4 ドキュメント # https://docs.python.org/ja/3/howto/sorting.html evaluation_phase = sorted( evaluation_phase, key=itemgetter(1), reverse=True) else: evaluation_phase = sorted(evaluation_phase, key=itemgetter(1)) return evaluation_phase def main(): """ ここから処理を開始する。 Returns: None. """ # ★改修・追加 global global_my_turn # グローバル変数。手番。 while True: input_cmd = input() # USIプロトコル対応の将棋GUI(将棋所)に # エンジン登録するときに通信する処理。 # # 普通にprintを使うと、改行はしてくれるので # 将棋エンジンを作る場合、問題なし。 # バッファリングさせない(フラッシュさせる)ために # printの引数に「flush=True」を使う。 if input_cmd == "usi": # ソフト名 print("id name 16-168ui", flush=True) # 開発者名 print("id author R.Sueyoshi", flush=True) print("usiok", flush=True) # 対局準備 if input_cmd == "isready": # 設定の読み込みが必要ならここで処理する。 print("readyok", flush=True) # 対局開始の合図 if input_cmd == "usinewgame": pass # 局面の受け取り # 平手はstartpos # 例) # position startpos moves 7g7f 3c3d 2g2f # 最初の8文字を使う。 if input_cmd[0:8] == "position" or input_cmd[0:1] == "a" or input_cmd[0:1] == "w" or input_cmd[0:1] == "b": # startposで局面が送られてくるとき cmd_lst = list(input_cmd.split(" ")) # スペース区切りのリスト。 # 例1)position startpos if len(cmd_lst) == 2: global_my_turn = "先手" # 例2)position startpos moves 7g7f 8d8e else: if (len(cmd_lst)-2) % 2 == 1: global_my_turn = "先手" else: global_my_turn = "後手" # 局面にセットする。 # cmd_lst[-1:][0]は最後の文字列(相手の手等) if cmd_lst[0] == "a": # 裏コマンド、平手をセットする。 board = cshogi.Board() elif cmd_lst[0] == "w": # 裏コマンドその2。指し手生成祭(後手) # 「指し手生成祭」の局面 b_phase = "l6nl/5+P1gk/2np1S3/p1p4Pp/3P2Sp1/1PPb2P1P/P5GS1/R8/LN4bKL w GR5pnsg 1" b_cmd_lst = list(b_phase.split(" ")) # スペース区切りのリスト。 board = cshogi.Board(b_phase) # 指定局面のセット # 手番の確認 if b_cmd_lst[1] == "b": global_my_turn = "先手" else: global_my_turn = "後手" elif cmd_lst[1] == "startpos": # 平手 board = cshogi.Board() # 送られてきた局面まで局面をセットしなおす。 if len(cmd_lst) > 2: for i in range(3, len(cmd_lst)): board.push_usi(cmd_lst[i]) # 指す # sfenで局面が送られてくるとき elif cmd_lst[1] == "sfen": # 指定局面 sfen_str = (cmd_lst[2] + " " + cmd_lst[3] + " " + cmd_lst[4] + " " + cmd_lst[5]) # 例1)position sfen lnsgkgsnl/1r5b1/p1ppppppp/1p7/9/7P1/PPPPPPP1P/1B5R1/LNSGKGSNL b - 1 if len(cmd_lst) == 6: if cmd_lst[3] == "b": global_my_turn = "先手" else: global_my_turn = "後手" # 例2)position sfen lnsgkgsnl/1r5b1/p1ppppppp/1p7/9/7P1/PPPPPPP1P/1B5R1/LNSGKGSNL b - 1 moves 7g7f 8d8e else: if (len(cmd_lst)-6) % 2 == 1 and cmd_lst[3] == "b": global_my_turn = "先手" else: global_my_turn = "後手" board = cshogi.Board(sfen_str) # 送られてきた局面まで局面をセットしなおす。 if len(cmd_lst) > 6: for i in range(7, len(cmd_lst)): board.push_usi(cmd_lst[i]) # 指す else: # ★追加 # 想定外の時はエンジンを終了する。 print("エラー:受信コマンド想定外") break # 思考開始の合図 # 最初の3文字を使う。 if input_cmd[0:2] == "go": # 自分の次の手を考えさせる。 # board.legal_movesを使うと # 合法手がリストで戻り値として返ってくる。 move_lst = list(board.legal_moves) # 合法手をリスト化する move_lst = order_list(move_lst) # 合法手をオーダリングする # 王手をかけられたとき # 詰みのとき(負け) if len(move_lst) == 0: print("bestmove resign", flush=True) else: # 次の一手を将棋所等の将棋GUIに伝える。 # 探索。次の一手を探索する。 # 合法手リストに評価値を付けて、評価順に並べ替える。 # move_lst_searchedは、指し手と評価値のリスト。 move_lst_searched = serch_next(board, move_lst) # ★追加 # このエンジンが優勢と思えば、先手でも後手でも評価値は正の値 # 劣勢と思えば、先手でも後手でも評価値は負の値 # だから、後手の時は評価値を反転させる必要がある。 # また評価値が最大値の時「mate」を伝える。 if global_my_turn == "先手": if move_lst_searched[0][1] == TSUMI_SCORE: evaluation_value = "mate +" # ★追加 elif move_lst_searched[0][1] == -TSUMI_SCORE: evaluation_value = "mate -" else: evaluation_value = "cp " + str(move_lst_searched[0][1]) else: # 後手 # ★改修 if move_lst_searched[0][1] == -TSUMI_SCORE: evaluation_value = "mate +" # ★追加 elif move_lst_searched[0][1] == TSUMI_SCORE: evaluation_value = "mate -" else: evaluation_value = "cp " + \ str(-move_lst_searched[0][1]) # 確認のときに、速く指すことを防ぐため #time.sleep(1) print( f"info time 0 depth 1 nodes {len(move_lst)} currmove {cshogi.move_to_usi(move_lst_searched[0][0])} score {evaluation_value} pv {cshogi.move_to_usi(move_lst_searched[0][0])}", flush=True) print("bestmove", cshogi.move_to_usi( move_lst_searched[0][0]), flush=True) # 将棋エンジン内の局面を変更する。 board.push(move_lst[0]) # 指す # 1ゲームが終わった場合 if input_cmd[:8] == "gameover": # コマンド受付を終了する。 # ★変更 #break continue # エンジン停止の合図 if input_cmd == "quit": # コマンド受付を終了する。 break # エンジンを終了させる。 quit() # main関数の実行 main() quit()