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

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

(連続企画)Pythonで将棋ソフト制作2のその1-1手先読みの補足

www.youtube.com

www.youtube.com

将棋ソフト作成の連続企画の一つです。
上記の動画で作成したソースコードです。

ソースコード内の「★」の部分は、上記の動画で説明できていません。
今後、ライブ配信で説明する予定です。
ライブ配信をしたら、この部分の文章を書き直します)

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