「十六式いろは」のあれこれ

コンピュータ将棋ソフトのエンジン「十六式いろは」の開発者のブログです。

(連続企画)Pythonで将棋ソフト制作2のその3-MC法の補足

youtu.be

将棋ソフト作成の連続企画の一つです。
上記の動画で作成したソースコードです。
モンテカルロ法で実装したものです。

シンプルな計算版(このすぐ下)と計算が少し複雑版(下の方)

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()