将棋ソフト作成の連続企画の一つです。
上記の動画で作成したソースコードです。
モンテカルロ法で実装したものです。
main.py (シンプルな計算版)
import cshogi import random LIMIT_PLAY_OUT = 5000 # プレイアウト数の上限 MAX_MOVES_TO_DRAW = 150 # プレイアウト中の上限手数。これに達したら引き分け。 def order_list(move_lst): """ cshogiから返ってきた合法手のリストをオーダリングする。 今のところ、とりあえずランダム。 これで、アルファベータカットしても 事前にシャッフルしているので、ランダムで指す。 Args: move_lst(int list): 合法手のリスト Returns: int list: オーダリング(並べ替えた)した合法手のリスト """ random.shuffle(move_lst) return move_lst def main(): while True: input_cmd = input() # USIプロトコル対応の将棋GUI(将棋所)に # エンジン登録するときに通信する処理。 # # 普通にprintを使うと、改行はしてくれるので # 将棋エンジンを作る場合、問題なし。 # バッファリングさせない(フラッシュさせる)ために # printの引数に「flush=True」を使う。 if input_cmd == "usi": # ソフト名 print("id name 16-168ui-mc1", 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": cmd_lst = list(input_cmd.split(" ")) # スペース区切りのリスト。 # 局面にセットする。 # 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" board = cshogi.Board(b_phase) # 指定局面のセット 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]) # 指す elif cmd_lst[1] == "sfen": # 指定局面 sfen_str = (cmd_lst[2] + " " + cmd_lst[3] + " " + cmd_lst[4] + " " + cmd_lst[5]) 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: # 想定外の時はエンジンを終了する。 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_list_count[i] = [指し手cshogiの数字、勝ち数、負け数、訪問数] move_list_count = [] for moves in move_lst: move_list_count.append([moves, 0, 0, 0]) # 初期化 # 次の一手を思考する。 # モンテカルロ法 # ここからプレイアウトを何度も行う。 for i in range(LIMIT_PLAY_OUT): # 送られてきた局面を、思考用の局面に上書きする。 thinking_board = board.copy() # 自分の次の一手を、仮にランダムで決める。 move_num = random.randint(0, len(move_lst) - 1) # 思考用の局面で決めた手を指す。 thinking_board.push(move_lst[move_num]) # 1プレイアウト内の思考 # jは現状からの1手先の手数。奇数なら相手の手番。偶数ならエンジンの手番。 for j in range(1, MAX_MOVES_TO_DRAW+1): tinking_move_lst = list(thinking_board.legal_moves) # 合法手をリスト化する # 詰みのとき、1プレイアウトを終える。 if len(tinking_move_lst) == 0: # 勝敗をmove_list_countに記録する # 手数が奇数(相手の手番)で詰みのとき勝ち。勝ち数を増やす。 if j % 2 == 1: move_list_count[move_num][1] += 1 # エンジンの手番で詰みのとき負け。 else: move_list_count[move_num][2] += 1 # 勝っても負けても訪問数をループを抜けた先で増やす。 break # 詰みではないとき else: # 思考用の局面の次の一手を、仮にランダムで決める。 tinking_move_num = random.randint(0, len(tinking_move_lst) - 1) # 思考用の局面で決めた手を指す。 thinking_board.push(tinking_move_lst[tinking_move_num]) # プレイアウト中の上限手数でループを抜けたら引き分け。 # 勝っても負けても引き分けでも、ここで訪問数を増やす。 move_list_count[move_num][3] += 1 # 検討中の次の一手を将棋所等の将棋のGUIに伝える。 # 合法手リストの各確率で最高のものを求める。 max = [0, 0, 0, 0] # 最高勝率[指し手の数字、勝率、勝ち数、負け数] for moves_list in move_list_count: if moves_list[1] > 0 or moves_list[2] > 0: # 最高勝率の更新 if moves_list[1]/moves_list[3] > max[1]: max = [moves_list[0], moves_list[1]/(moves_list[1]+moves_list[2]), moves_list[1], moves_list[2]] # 評価値の計算 # ランダムだと引き分けが多くなる。勝率は5%前後。 evaluation_value = int((max[1]*2000)-1000) # 評価値 # プレイアウト数が少ない等でmaxにデータがない時がランダム指し。 if max[0] == 0: bestmove_usi = cshogi.move_to_usi(move_list_count[move_num][0]) else: bestmove_usi = cshogi.move_to_usi(max[0]) print(f"info depth 1 score cp {evaluation_value} currmove {bestmove_usi} pv {bestmove_usi}", flush=True) # 次の一手を出力する。 print("bestmove", bestmove_usi, flush=True) # 1ゲームが終わった場合 if input_cmd[:8] == "gameover": # コマンド受付を終了する。 break # エンジン停止の合図 if input_cmd == "quit": # コマンド受付を終了する。 break # エンジンを終了させる。 quit() main() quit()
main.py(計算が少し複雑版)
(評価値を歩1枚100点に近づけて計算するようにした版 )
# 次の一手をモンテカルロ法で思考する将棋エンジン import cshogi import random LIMIT_PLAY_OUT = 5000 # プレイアウト数の上限 MAX_MOVES_TO_DRAW = 150 # プレイアウト中の上限手数。これに達したら引き分け。 def order_list(move_lst): """ cshogiから返ってきた合法手のリストをオーダリングする。 今のところ、とりあえずランダム。 これで、アルファベータカットしても 事前にシャッフルしているので、ランダムで指す。 Args: move_lst(int list): 合法手のリスト Returns: int list: オーダリング(並べ替えた)した合法手のリスト """ random.shuffle(move_lst) return move_lst def main(): while True: input_cmd = input() # USIプロトコル対応の将棋GUI(将棋所)に # エンジン登録するときに通信する処理。 # # 普通にprintを使うと、改行はしてくれるので # 将棋エンジンを作る場合、問題なし。 # バッファリングさせない(フラッシュさせる)ために # printの引数に「flush=True」を使う。 if input_cmd == "usi": # ソフト名 print("id name 16-168ui-mc1", 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": cmd_lst = list(input_cmd.split(" ")) # スペース区切りのリスト。 # 局面にセットする。 # 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" board = cshogi.Board(b_phase) # 指定局面のセット 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]) # 指す elif cmd_lst[1] == "sfen": # 指定局面 sfen_str = (cmd_lst[2] + " " + cmd_lst[3] + " " + cmd_lst[4] + " " + cmd_lst[5]) 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: # 想定外の時はエンジンを終了する。 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_list_count[i] = [指し手cshogiの数字、勝ち数、負け数、訪問数] move_list_count = [] for moves in move_lst: move_list_count.append([moves, 0, 0, 0]) # 初期化 # 次の一手を思考する。 # モンテカルロ法 # ここからプレイアウトを何度も行う。 for i in range(LIMIT_PLAY_OUT): # 送られてきた局面を、思考用の局面に上書きする。 thinking_board = board.copy() # 自分の次の一手を、仮にランダムで決める。 move_num = random.randint(0, len(move_lst) - 1) # 思考用の局面で決めた手を指す。 thinking_board.push(move_lst[move_num]) # 1プレイアウト内の思考 # jは現状からの1手先の手数。奇数なら相手の手番。偶数ならエンジンの手番。 for j in range(1, MAX_MOVES_TO_DRAW+1): tinking_move_lst = list(thinking_board.legal_moves) # 合法手をリスト化する # 詰みのとき、1プレイアウトを終える。 if len(tinking_move_lst) == 0: # 勝敗をmove_list_countに記録する # 手数が奇数(相手の手番)で詰みのとき勝ち。勝ち数を増やす。 if j % 2 == 1: move_list_count[move_num][1] += 1 # エンジンの手番で詰みのとき負け。 else: move_list_count[move_num][2] += 1 # 勝っても負けても訪問数をループを抜けた先で増やす。 break # 詰みではないとき else: # 思考用の局面の次の一手を、仮にランダムで決める。 tinking_move_num = random.randint(0, len(tinking_move_lst) - 1) # 思考用の局面で決めた手を指す。 thinking_board.push(tinking_move_lst[tinking_move_num]) # プレイアウト中の上限手数でループを抜けたら引き分け。 # 勝っても負けても引き分けでも、ここで訪問数を増やす。 move_list_count[move_num][3] += 1 # 検討中の次の一手を将棋所等の将棋のGUIに伝える。 # 合法手リストの各確率で最高のものを求める。 max = [0, 0, 0, 0] # 最高勝率[指し手の数字、勝率、勝ち数、負け数] for moves_list in move_list_count: if moves_list[1] > 0 or moves_list[2] > 0: # 最高勝率の更新 if moves_list[1]/moves_list[3] > max[1]: max = [moves_list[0], moves_list[1]/moves_list[3], moves_list[1], moves_list[2]] # 評価値の計算 # ランダムだと引き分けが多くなる。勝率は5%前後。 # 詰めそうだと、勝ちのプレイアウトが増える。 # 詰まれそうだと、負けのプレイアウトが増える。 # LIMIT_PLAY_OUT、MAX_MOVES_TO_DRAWを増やすほど # 評価値が増えるので、変数weightで抑制する。 # weightを100*5000*128にする。理由は # LIMIT_PLAY_OUT=5000、MAX_MOVES_TO_DRAW=64のとき # 良い感じの評価値になったので。 evaluation = (max[2]+max[3]) * (max[2]-max[3]) * max[1] weight = 100*5000*128/(LIMIT_PLAY_OUT * MAX_MOVES_TO_DRAW) print("max, weight, evaluation: ", max, weight, evaluation) evaluation_value = int(evaluation * weight) # 評価値 # プレイアウト数が少ない等でmaxにデータがない時がランダム指し。 if max[0] == 0: bestmove_usi = cshogi.move_to_usi(move_list_count[move_num][0]) else: bestmove_usi = cshogi.move_to_usi(max[0]) print(f"info depth 1 score cp {evaluation_value} currmove {bestmove_usi} pv {bestmove_usi}", flush=True) # 次の一手を出力する。 print("bestmove", bestmove_usi, flush=True) # 1ゲームが終わった場合 if input_cmd[:8] == "gameover": # コマンド受付を終了する。 break # エンジン停止の合図 if input_cmd == "quit": # コマンド受付を終了する。 break # エンジンを終了させる。 quit() # main関数の実行 main() quit()