評価値バーを将棋のGUIに表示する（ShogiEvalBar）

情報企画部の尾崎です。
AIの進化が目覚ましい昨今ですが、将棋の世界でも、
今やプロ棋士の方もAI将棋ソフトを使って研究をするのが当たり前になりつつあります。
また、順位戦や名人戦、朝日杯などの棋戦でも、対局者の映像とともにAI将棋ソフトを使って、候補手だったり、評価値を表示したりするいわゆる評価値配信がされております。
[囲碁将棋TV -朝日新聞社-](https://www.youtube.com/@igoshogitv)

AI将棋ソフトの検討モードを使うと、
どちらが優勢か、次の候補手は何かというのは、見ればわかるのですが、
評価値バーがあることで、見た目にもどちらが優勢なのかわかりやすくなります。
このあたりがしっくりくるのは、１対１というところから、「ストⅡ」のような格闘ゲームのパワーゲージを見て育った影響なのでしょうか（笑）

水匠というAI将棋ソフトの開発者である たややん([@tayayan_ts](https://twitter.com/tayayan_ts))さんが、
最近、評価値バーを以下の動画で公開していたので、この動画に基づいて解説します。

https://www.youtube.com/watch?app=desktop&v=32WBQUs4u-U

# ShogiGUIのダウンロード
Windows環境で、
将棋のGUIを表示するソフトで、今回はShogiGUIを使います。
以下からダウンロードします。

[ShogiGUI](http://shogigui.siganus.com/download.html) ←2023/3/27現在ver 0.0.8.0が最新
.NET framework 4.8.1が必要です。

今回は、zip版をダウンロードします。
デスクトップなどに解凍します。
「ShogiGUI」というフォルダができると思います。

その中に「ShogiGUI.exe」というファイルがあり、それがShogiGUIの本体になります。
最初の１回だけですが、ダブルクリックして実行してみると、以下のような警告画面が出てアプリが起動できないので、「詳細情報」をクリックして、「実行」をクリックしておきましょう（２回目以降は聞かれません）。

<img width="400" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2747663/fd94932c-9012-a838-497c-e250ed7e6f80.png">

# 評価値バーのダウンロード
評価値バーは、たややん([@tayayan_ts](https://twitter.com/tayayan_ts))さんが
以下のGoogle Driveに公開してくれていますので、
ここからダウンロードします。
https://drive.google.com/file/d/1Thdyd9ITxQf-AiqNMoEnM9ClzGIqHL9y/view

ShogiEvalBar.zip
をダウンロードして、解凍します。
「ShogiEvalBar」というフォルダの中にあるShogibar.exeが評価値バーの本体になります。

# 評価値バーの設定
ShogiGUI.exeをダブルクリックで実行すると、ShogiGUIが立ち上がるので、
[ツール]－[エンジン設定]を選択します。

<img width="600" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2747663/ba359e2a-a9f8-c413-e474-9917d7ea2670.png">

追加をクリックします。

<img width="400" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2747663/5db50013-560e-410a-e3ee-77fdb55444d5.png">

先ほど、ダウンロードした評価値バーの本体（Shogibar.exe）を選択し、開くをクリックします。

<img width="400" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2747663/745339a7-60ba-4331-ab8c-c4742bd56fa6.png">

すると、ShogiGUIのエンジン設定の画面になります。
この評価値バーには、デフォルトで水匠５というAI将棋ソフトが同梱されています。
エンジン設定については、インストールするパソコンのスペックにもよるので、ググるなりして調べてみてください。
なお、USI(Universal Shogi Interface)プロトコル対応の将棋ソフトであれば、別のエンジンに変更することも可能です。
詳細は、上記たややんさんの動画をご確認ください。

<img width="400" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2747663/2df9b739-ea4c-27df-8d00-0b0824ef3fef.png">

エンジンが追加されたら、OKをクリックします。

<img width="400" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2747663/a0b032b8-ac9a-5985-af4f-1bf9e9c313ef.png">

エンジン設定を追加できたら、
ShogiGUIの画面に戻るので、
「検討」ボタンをクリックすると、登録されているエンジンをプルダウンで選べますが、
先ほどのShogiBarが選ばれていることを確認して、「検討開始」をクリックします。
候補手の数を増やすと、最善手だけでなく、候補手を複数表示することができます。

<img width="600" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2747663/446bdd58-d2dd-be73-535c-673df732b8b5.png">

しばらくすると、評価値バーのダイアログが立ち上がり、検討モードが開始されます。

<img width="600" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2747663/172114eb-e2fa-e727-d1eb-59f64b9513f9.png">

あとは、駒を動かしていくと、評価値に連動して、評価値バーのゲージが変化してきます。
評価値バーには、ゲージのほかに勝率（評価値）、将棋エンジン名、最善手、探索深度、探索局面数、手番などが表示されています。

<img width="600" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2747663/9f46e4ce-2eac-4bde-78f3-6cbcf836281a.png">

また、評価値バーのダイアログを、マウスでドラッグしながら下に延ばすと、
Reverseのチェックボックスが出てくるので、先手と後手の左右を入れ替えることができます。

<img width="600" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2747663/40149f48-bdee-eb04-b11a-6fb792119047.png">

評価値バーのプログラムの中身は、Pythonのプログラムで書かれており、
ShogiEvalBar\sourceフォルダの下にある、ShogiBar.pyになります。
こちらをいじると、例えばエンジンをUSI（Universal Shogi Interface）プロトコルに対応した他のAI将棋ソフトに変更したり、評価値バーの色だったり、評価値‐勝率変換のシグモイド関数のパラメーターを変更するといったことも可能です。

```ShogiBar.py
from cshogi import *
from cshogi import KI2
import math
import sys
import numpy as np
import threading
import subprocess
import tkinter as tk


#エンジン名・フォント・文字色・背景色
engine = "水匠5"            # 水匠5
barfont = "BIZ UDGothic"    # BIZ UDGothic
bgcolor = "white"           # white
fgcolor = "black"           # black
turnfgcolor = "#00007f"     # #00007f
leftgraphbg = "#aa0000"     # #aa0000
rightgraphbg = '#ffff7f'    # #ffff7f


#外部エンジン起動        
shogi = subprocess.Popen("./Suisho5.exe", stdin=subprocess.PIPE,
                                          stdout=subprocess.PIPE,
                                          encoding="UTF-8")
#局面初期化
board = Board()


#コマンド受付
def command():
    while True:
        cmdline = input()
        # 局面設定
        if cmdline[:8] == "position":
            board.set_position(cmdline[9:])
        usi(cmdline)

#コマンド入力処理
def usi(command):
    #終了処理
    if command == "quit":
        root.destroy()
        sys.exit()
    shogi.stdin.write(command+"\n")
    shogi.stdin.flush()

#コマンド出力処理
def output():
    while True:
        #エンジンからの出力を受け取る
        line = shogi.stdout.readline()
        #評価値バー処理
        shogibar(line)
        #標準出力
        sys.stdout.write(line)
        sys.stdout.flush()

#評価値バー情報更新
def shogibar(line):
    if line[:4] == "info":
        sfen = line.split()
        #評価値表示列かチェック
        if "multipv" not in sfen and "pv" in sfen:
            pass
        elif "multipv" in sfen and int(sfen[sfen.index("multipv")+1]) == 1:
            pass
        else:
            return
        #左右反転チェックを受け取る
        if bln.get():
            reverse = -1
        else:
            reverse = 1
        #データ処理
        turn = -(board.turn * 2 - 1) * reverse
        move_count = board.move_number
        depth = int(sfen[sfen.index("depth")+1])
        nodes = int(sfen[sfen.index("nodes")+1])
        if nodes < 10000:
            nodes = str(nodes) + "局面"
        elif nodes < 100000000:
            nodes = str(int(nodes/10000)) + "万局面"
        else:
            nodes = str(int(nodes/100000000)) + "億局面"                    
        pv = sfen[sfen.index("pv")+1]
        if "cp" in sfen:
            cp = int(sfen[sfen.index("cp")+1]) * turn
            #勝率変換(Ponanza定数 = 1200)
            winrate = int(round(100 / (1 + (math.exp(-cp/1200)))))
            lwinratelabel["text"] = str(winrate) + "%(" + "{:+}".format(cp) + ")"
            rwinratelabel["text"] = "(" + "{:+}".format(-cp) + ")" + str(100 - winrate) + "%"
            leftgraph.place(x=50, y=40, width=winrate * 10, height=20)
        if "mate" in sfen:
            mate = int(sfen[sfen.index("mate")+1]) * turn
            if mate > 0:
                lwinratelabel["text"] = "100%(" + str(mate) + "手詰)"
                rwinratelabel["text"] = "(" + str(-mate) + "手詰)0%"
                leftgraph.place(x = 50, y = 40, width = 1000, height = 20)
            elif mate < 0:
                lwinratelabel["text"] = "0%(" + str(mate) + "手詰)"
                rwinratelabel["text"] = "(" + str(-mate) + "手詰)100%"
                leftgraph.place(x = 50, y = 40, width = 0, height = 20)            
        saizen["text"] = engine + " " + str(move_count) + "手目検討中：最善手" + KI2.move_to_ki2(board.move_from_usi(pv), board)
        tansaku["text"] = "探索深度：" + str(depth) + "手 探索局面数：" + nodes
        if turn == 1 and ltebanlabel["text"] != "手番":
            ltebanlabel["text"] = "手番"
            rtebanlabel["text"] = ""
        elif turn == -1 and rtebanlabel["text"] != "手番":
            ltebanlabel["text"] = ""
            rtebanlabel["text"] = "手番"        

    

#コマンド受付と出力は並列処理(Tkinterとは別に動かす必要があるため)
t = threading.Thread(target=output, daemon=True)
t.start()

#初期設定(isreadyまで)
while True:
    cmdline = input()
    if cmdline[:7] == "isready":
        usi(cmdline)
        break
    elif cmdline[:4] == "quit":
        sys.exit()
    usi(cmdline)
    
#isready後は並列処理
t2 = threading.Thread(target=command, daemon=True)
t2.start()


#Tkinter表示
root = tk.Tk()
root.geometry("1100x108")
root.minsize(width=1100, height=108)
root.title("評価値バー")
root.configure(bg = bgcolor)

#勝率ラベル
lwinratelabel = tk.Label(root, text="50%(0)", font=(barfont, 25), bg=bgcolor, fg=fgcolor)
lwinratelabel.place(x = 50, y = 20, anchor=tk.W)
rwinratelabel = tk.Label(root, text="(0)50%", font=(barfont, 25), bg=bgcolor, fg=fgcolor)
rwinratelabel.place(x = 1050, y = 20, anchor=tk.E)

#手番ラベル
ltebanlabel = tk.Label(root, text="", font=(barfont, 25), bg=bgcolor, fg=turnfgcolor)
ltebanlabel.place(x = 50, y = 80, anchor=tk.W)
rtebanlabel = tk.Label(root, text="", font=(barfont, 25), bg=bgcolor, fg=turnfgcolor)
rtebanlabel.place(x=1050, y=80, anchor=tk.E)

#最善手ラベル
saizen = tk.Label(root, text=engine + " 0手目検討中：最善手", font=(barfont, 20), bg=bgcolor, fg=fgcolor)
saizen.place(x = 547, y = 18, anchor=tk.CENTER)

#探索ラベル
tansaku = tk.Label(root, text="探索深度：0手 探索局面数：0局面", font=(barfont, 20), bg=bgcolor, fg=fgcolor)
tansaku.place(x=547, y=82, anchor=tk.CENTER)

#勝率目盛り
label50 = tk.Label(root, text="", bg="black")
label50.place(x=547, y=35, width=3, height=30)

#評価値バー描画
rightgraph = tk.Label(root, text="", bg=rightgraphbg, relief=tk.SOLID, bd=3)
rightgraph.place(x=50, y=40, width=1000, height=20)
leftgraph = tk.Label(root, text="", bg=leftgraphbg, relief=tk.SOLID, bd=3)
leftgraph.place(x = 50, y = 40, width = 500, height = 20)


#左右反転チェック
bln = tk.BooleanVar()
bln.set(False)
check = tk.Checkbutton(root, variable=bln, text="Reverse", font=(barfont, 15), bg=bgcolor, fg=fgcolor, activeforeground = fgcolor)
check.place(x = 50, y = 150)

root.mainloop()
```

試してみてはいかがでしょうか。