0
0

Pythonで〇×ゲームのAIを一から作成する その117 GUI の部分木への最善手の表示と最善手を着手し続けた場合の局面の表示

Posted at

目次と前回の記事

これまでに作成したモジュール

以下のリンクから、これまでに作成したモジュールを見ることができます。

リンク 説明
marubatsu.py Marubatsu、Marubatsu_GUI クラスの定義
ai.py AI に関する関数
test.py テストに関する関数
util.py ユーティリティ関数の定義。現在は gui_play のみ定義されている
tree.py ゲーム木に関する Node、Mbtree クラスの定義
gui.py GUI に関する処理を行う基底クラスとなる GUI クラスの定義

AI の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。

循環参照の問題の修正

前回の記事で修正したプログラムを marubatsu.py と tree.py に反映させた後で、下記のプログラムを実行すると、実行結果のように 循環インポート(circular import)の エラーが発生 します。また、エラーメッセージから、from util import gui_play を実行した際にこのエラーが発生したことが確認できます。

なお、今回の記事の github の marubatsu.py は、このエラーが発生しないように 原因となる from tree import Mbtree, Mbtree_GUI をコメントにしています。このエラーを確認したい人はそのコメントを外してから下記のプログラムを実行して下さい。

from util import gui_play

gui_play()

実行結果

---------------------------------------------------------------------------
ImportError                               Traceback (most recent call last)
Cell In[1], line 1
----> 1 from util import gui_play
      3 gui_play()
略
File c:\Users\ys\ai\marubatsu\117\tree.py:4
      2 import matplotlib.pyplot as plt
      3 import matplotlib.patches as patches
----> 4 from marubatsu import Marubatsu, Marubatsu_GUI
      5 from gui import GUI
      6 import ipywidgets as widgets

ImportError: cannot import name 'Marubatsu' from partially initialized module 'marubatsu' (most likely due to a circular import) (c:\Users\ys\ai\marubatsu\117\marubatsu.py)

循環参照が起きる原因は以下の通りです。

  • 前回の記事で Marubatsu_GUI クラスの __init__ メソッド内で、mbtree.py で定義された Mbtree と Mbtree_GUI クラスを利用することになったので、marubatsu.py の先頭で from tree import Mbtree, Mbtree_GUI を実行 してそれらのクラスを mbtree モジュールからインポートするように修正した
  • util.py では、先頭の行の from marubatsu import Marubatsu で marubatsu モジュールから Marubatsu クラスがインポートされているので marubatsu.py が実行 される
  • marubatsu.pyfrom tree import Mbtree, Mbtree_GUI が実行された結果、tree.py が実行される
  • tree.py には from marubatsu import Marubatsu, Marubatsu_GUI が記述されているので、marubatsu.py が再び実行 され、循環インポートが発生する

marubatu.py と mbtree.py を一つのファイルに統合することで循環インポートが発生しないようにすることができますが、それでは 〇×ゲーム と、ゲーム木の処理 を別のファイルで 分けて記述することができなくなってしまいます

ファイルを統合せずに この問題を 解決するの方法の一つ に、下記のプログラムのように Marubatsu_GUI クラスの __init__ メソッドの中Mbtree と Mbtree_GUI クラスをインポートする という方法があります。

  • 6 行目__init__ メソッドの中で Mbtree と Mbtree_GUI クラスをインポートする
 1  from marubatsu import Marubatsu_GUI
 2  from tkinter import Tk, filedialog
 3  import os
 4
 5  def __init__(self, mb, params, names, ai_dict, seed, size):
元と同じなので省略
 6      from tree import Mbtree, Mbtree_GUI
 7      if Marubatsu_GUI.mbtree is None:
 8          Marubatsu_GUI.mbtree = Mbtree.load("../data/aidata")
 9      self.mbtree_gui = Mbtree_GUI(Marubatsu_GUI.mbtree, size=0.1)
10    
11  Marubatsu_GUI.__init__ = __init__
行番号のないプログラム
from marubatsu import Marubatsu_GUI
from tkinter import Tk, filedialog
import os

def __init__(self, mb, params, names, ai_dict, seed, size):
    if params is None:
        params = [{}, {}]
    if ai_dict is None:
        ai_dict = {}
    if names is None:
        names = [None, None]
    for i in range(2):
        if names[i] is None:
            if mb.ai[i] is None:
                names[i] = "人間"
            else:
                names[i] = mb.ai[i].__name__
    
    # JupyterLab からファイルダイアログを開く際に必要な前処理
    root = Tk()
    root.withdraw()
    root.call('wm', 'attributes', '.', '-topmost', True)  

    # save フォルダが存在しない場合は作成する
    if not os.path.exists("save"):
        os.mkdir("save")        
    
    self.mb = mb
    self.ai_dict = ai_dict
    self.params = params
    self.names = names
    self.seed = seed
    self.size = size
    
    super(Marubatsu_GUI, self).__init__()
    
    from tree import Mbtree, Mbtree_GUI

    if Marubatsu_GUI.mbtree is None:
        Marubatsu_GUI.mbtree = Mbtree.load("../data/aidata")
    self.mbtree_gui = Mbtree_GUI(Marubatsu_GUI.mbtree, size=0.1)
    
Marubatsu_GUI.__init__ = __init__
修正箇所
from marubatsu import Marubatsu_GUI
from tkinter import Tk, filedialog
import os

def __init__(self, mb, params, names, ai_dict, seed, size):
元と同じなので省略
    from tree import Mbtree, Mbtree_GUI
    if Marubatsu_GUI.mbtree is None:
        Marubatsu_GUI.mbtree = Mbtree.load("../data/aidata")
    self.mbtree_gui = Mbtree_GUI(Marubatsu_GUI.mbtree, size=0.1)
    
Marubatsu_GUI.__init__ = __init__

この方法で循環インポートが起きなくなる理由は以下の通りです。

  • 関数やメソッドの中に記述したモジュールのインポート は、その関数やメソッドが 呼び出されるまでは行われない
  • Marubatsu_GUI クラスの __init__ メソッドが呼び出される のは、from util import gui_play を実行した後 なので、from util import gui_play を実行した際に 循環インポートは発生しない
  • Marubatsu_GUI クラスの __init__ メソッドが実行され、その中で from tree import Mbtree, Mbtree_GUI を実行した際 には、既にそれらのモジュールインポートされている ので、循環インポートにはならない

このような 関数やメソッドのブロックの中に記述されたインポート のことを、ローカルなインポート と呼びます。ローカルなインポートは ローカル名前空間によって管理される ので、ローカル変数と同様に、その関数やメソッドの ブロックの中でしか利用できない という性質があります。

実行結果は省略しますが、上記の修正を行った後で下記のプログラムを実行すると、エラーが発生しなくなったことが確認できます。なお、marubatsu.py の先頭の from tree import Mbtree, Mbtree_GUI のコメントを外した方は、元に戻した後で JupyterLab を再起動してからから下記のプログラムを実行して下さい。

gui_play()

最善手の表示

今回の記事では、前回の記事で開始した Mbtree_GUI クラスの改良の続きを行います。

現状の GUI の部分木1の表示 では、色によって局面の評価値がわかるようになっていますが、〇 の手番と × の手番によって評価値が大きいほうが最善手であるか、小さいほうが最善手であるかが変わるので、どの合法手が最善手であるかが直観的にわかりにくい という問題があります。そこで、最善手が着手されていない局面を暗い色で表示する ようにすることで、どの合法手が最善手であるか を一目でわかるようにすることにします。

まず、それぞれの局面が、最善手を着手した局面であるかを判定する 必要があります。どのように判定すればよいかについて少し考えてみて下さい。

最善手を着手した局面であるかの判定方法

aidata.mbtree に保存されたゲーム木のデータには、それぞれのノードの bestmoves 属性に最善手の一覧のデータが記録 されています。また、直前に行われた着手 は、ノードの mb 属性の lastmove 属性に記録 されています。そのため、下記の手順でそれぞれのノードの局面が最善手を着手した局面であるかを判定することができます。なお、着手が行われていない、ゲーム開始時の局面 であるルートノードは 例外として最善手を着手した局面であると判定する ことにします。

  1. 親ノードが存在しない場合 はルートノードなので 最善手を着手した と判定する
  2. lastmove 属性が、親ノードの bestmoves 属性の要素に 含まれている場合最善手を着手した と判定する
  3. 上記のいずれでもない 場合は 最善手を着手していない と判定する

ゲーム木の ノードの描画 は Node クラスの draw_node メソッド内で行われるので、下記の処理を記述することで最善手を着手していない局面を暗く表示することができます。

  1. ノードを描画 する処理を 行った後 で、最善手を着手した局面であるかを判定 する
  2. 最善手を着手した局面でないと判定された場合は、以前の記事で説明した方法で、その上に 半透明な黒い正方形の画像を描画する ことでその局面を 暗く表示する

draw_board の修正

以前の記事でノードを暗く表示する処理を実装する際に、「draw_board を修正するということを続けていくと、draw_board の定義が複雑になるという問題が発生する」という理由でノードを暗く表示する処理を、draw_node とは別に実装しました。

その時はノードを暗くする処理を他の場所で行うことはないだろうと思っていたのですが、今回再び ノードを暗く表示する処理を新たに記述する ことになり、そのような 同じ処理を何度も記述するのは良くないと思い直しました ので、今回の記事では draw_node でゲーム盤を暗く表示する 処理を記述する ように修正することにします。

今回の記事では行いませんが、今後ゲーム盤の 外枠別の色や太さ描画したくなることがあるかもしれない ことを考慮して、draw_node下記の仮引数を追加する ことにします。また、これまでは仮引数 emphasize によって外枠を赤く表示していましたが、外枠の描画を下記の仮引数で指定できるようになるので仮引数 emphasize は削除 します。

仮引数 意味 デフォルト値
bc2 ゲーム盤の外枠の色。None の場合は外枠を描画しない None
bw3 ゲーム盤の外枠の幅 1
darkness ゲーム盤の表示の暗さ(darkness)を表す 0 ~ 1 の数値
数値が大きい程暗くなる
0

下記はそのように draw_board を修正したプログラムです。

  • 5 行目:上記の仮引数を追加し、仮引数 emphasize を削除する
  • 途中にあった emphasizeTrue の場合に外枠を描画する処理を削除する
  • 7 ~ 9 行目:ゲーム盤の描画を行った後で、darkness0 より大きい場合に、ゲーム盤の上に半透明の黒い正方形を描画して暗くする
  • 12 ~ 15 行目bcNone 出ない場合は、ゲーム盤の外枠を bc の色で、bw の太さで描画する。この処理は、ゲーム盤を暗く表示する処理を行った後で行わないと枠の色まで暗くなってしまう点に注意すること
 1  from marubatsu import Marubatsu
 2  import matplotlib.patches as patches
 3
 4  @staticmethod
 5  def draw_board(ax, mb, show_result=False, score=None, bc=None, bw=1, darkness=0, dx=0, dy=0, lw=2): 
元と同じなので省略
 6      # darkness 0 より大きい場合は、半透明の黒い正方形を描画して暗くする
 7      if darkness > 0:
 8          ax.add_artist(patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
 9                                          height=mb.BOARD_SIZE, fc="black", alpha=darkness))
10
11      # bc が None でない場合はその色で bw の太さで外枠を描画する
12      if bc is not None:
13          frame = patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
14                                    height=mb.BOARD_SIZE, ec=bc, fill=False, lw=bw)
15          ax.add_patch(frame)
16        
17  Marubatsu_GUI.draw_board = draw_board
行番号のないプログラム
from marubatsu import Marubatsu
import matplotlib.patches as patches

@staticmethod
def draw_board(ax, mb, show_result=False, score=None, bc=None, bw=1, darkness=0, dx=0, dy=0, lw=2): 
    # 結果によってゲーム盤の背景色を変更する
    if show_result:
        if score is None and mb.status == Marubatsu.PLAYING:
            bgcolor = "white"
        elif score == 1 or mb.status == Marubatsu.CIRCLE:
            bgcolor = "lightcyan"
        elif score == -1 or mb.status == Marubatsu.CROSS:
            bgcolor = "lavenderblush"
        else:
            bgcolor = "lightyellow"
        rect = patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
                                height=mb.BOARD_SIZE, fc=bgcolor)
        ax.add_patch(rect)
  
    # ゲーム盤の枠を描画する
    for i in range(1, mb.BOARD_SIZE):
        ax.plot([dx, dx + mb.BOARD_SIZE], [dy + i, dy + i], c="k", lw=lw) # 横方向の枠線
        ax.plot([dx + i, dx + i], [dy, dy + mb.BOARD_SIZE], c="k", lw=lw) # 縦方向の枠線

    # ゲーム盤のマークを描画する
    for y in range(mb.BOARD_SIZE):
        for x in range(mb.BOARD_SIZE):
            color = "red" if (x, y) == mb.last_move else "black"
            Marubatsu_GUI.draw_mark(ax, dx + x, dy + y, mb.board[x][y], color, lw=lw)

    # darkness 0 より大きい場合は、半透明の黒い正方形を描画して暗くする
    if darkness > 0:
        ax.add_artist(patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
                                        height=mb.BOARD_SIZE, fc="black", alpha=darkness))

    # bc が None でない場合はその色で bw の太さで外枠を描画する
    if bc is not None:
        frame = patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
                                  height=mb.BOARD_SIZE, ec=bc, fill=False, lw=bw)
        ax.add_patch(frame)
        
Marubatsu_GUI.draw_board = draw_board
修正箇所
from marubatsu import Marubatsu
import matplotlib.patches as patches

@staticmethod
-def draw_board(ax, mb, show_result=False, score=None, dx=0, dy=0, lw=2): 
+def draw_board(ax, mb, show_result=False, score=None, bc=None, bw=1, darkness=None, dx=0, dy=0, lw=2): 
元と同じなので省略
    # emphasize が True の場合は赤色の外枠を描画する
-   if emphasize:
-       frame = patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
-                                 height=mb.BOARD_SIZE, ec="red", fill=False, lw=lw)
-       ax.add_patch(frame)
元と同じなので省略
    # darkness 0 より大きい場合は、半透明の黒い正方形を描画して暗くする
+   if darkness > 0:
+       ax.add_artist(patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
+                                       height=mb.BOARD_SIZE, fc="black", alpha=darkness))

    # bc が None でない場合はその色で bw の太さで外枠を描画する
+   if bc is not None:
+       frame = patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
+                                 height=mb.BOARD_SIZE, ec=bc, fill=False, lw=bw)
+       ax.add_patch(frame)
        
Marubatsu_GUI.draw_board = draw_board

上記の修正後に、下記のプログラムで draw_board を新しい仮引数に対応する実引数を記述せずに呼び出すと、実行結果のようにこれまで通りの描画が行われることが確認できます。

import matplotlib.pyplot as plt

mb = Marubatsu()
fig, ax = plt.subplots(figsize=(1,1))
ax.axis("off")
Marubatsu_GUI.draw_board(ax, mb)

実行結果

また、下記のプログラムで外枠の色を赤に、暗さを 0.3 に設定して draw_board を呼び出すと、実行結果のように指定した設定で描画が行われることが確認できます。

fig, ax = plt.subplots(figsize=(1,1))
ax.axis("off")
Marubatsu_GUI.draw_board(ax, mb, bc="red", darkness=0.3)

draw_node の修正

mbtree.py の中で、draw_board を呼び出している のは、Node クラスの draw_node メソッドなので、draw_node を下記のプログラムのように正する必要があります。

  • 3 行目:デフォルト値を 0 とする仮引数 darkness を追加する。なお、ノードを強調して表示すかどうかを表す仮引数 emphasize はそのままで良い
  • 6 行目emphasis の値によって、draw_board を呼び出す際のキーワード引数 bc の値を計算する
  • 7、8 行目draw_board を呼び出す際に、キーワード引数 bcdarkness を追加する

なお、draw_node 内では maxdepthNone の場合 に子ノードを draw_board を呼び出して描画 する処理を行っていますが、そちらはゲーム木の部分木を描画する draw_subtree から draw_board を呼び出す際には実行されない ので、修正していません

 1  from tree import Node, Rect
 2
 3  def draw_node(self, ax=None, maxdepth=None, emphasize=False, darkness=0, size=0.25, lw=0.8, dx=0, dy=0):
元と同じなので省略
 4      # 自分自身のノードを真ん中の位置になるように (dx, dy) からずらして描画する
 5      y = dy + (self.height - 3) / 2
 6      bc = "red" if emphasize else None
 7      Marubatsu_GUI.draw_board(ax, self.mb, show_result=True, 
 8                               score=getattr(self, "score", None), bc=bc, darkness=darkness, lw=lw, dx=dx, dy=y)
元と同じなので省略
 9
10  Node.draw_node = draw_node
行番号のないプログラム
from tree import Node, Rect

def draw_node(self, ax=None, maxdepth=None, emphasize=False, darkness=0, size=0.25, lw=0.8, dx=0, dy=0):
    width = 8
    if ax is None:
        height = len(self.children) * 4
        fig, ax = plt.subplots(figsize=(width * size, height * size))
        ax.set_xlim(0, width)
        ax.set_ylim(0, height)   
        ax.invert_yaxis()
        ax.axis("off")
        for childnode in self.children:
            childnode.height = 4
        self.height = height         
        
    # 自分自身のノードを真ん中の位置になるように (dx, dy) からずらして描画する
    y = dy + (self.height - 3) / 2
    bc = "red" if emphasize else None
    Marubatsu_GUI.draw_board(ax, self.mb, show_result=True, 
                            score=getattr(self, "score", None), bc=bc, darkness=darkness, lw=lw, dx=dx, dy=y)
    rect = Rect(dx, y, 3, 3)
    # 子ノードが存在する場合に、エッジの線と子ノードを描画する
    if len(self.children) > 0:
        if maxdepth != self.depth:   
            ax.plot([dx + 3.5, dx + 4], [y + 1.5, y + 1.5], c="k", lw=lw)
            prevy = None
            for childnode in self.children:
                childnodey = dy + (childnode.height - 3) / 2
                if maxdepth is None:
                    Marubatsu_GUI.draw_board(ax, childnode.mb, show_result=True,
                                            score=getattr(childnode, "score", None), dx=dx+5, dy=childnodey, lw=lw)
                edgey = childnodey + 1.5
                ax.plot([dx + 4 , dx + 4.5], [edgey, edgey], c="k", lw=lw)
                if prevy is not None:
                    ax.plot([dx + 4 , dx + 4], [prevy, edgey], c="k", lw=lw)
                prevy = edgey
                dy += childnode.height
        else:
            ax.plot([dx + 3.5, dx + 4.5], [y + 1.5, y + 1.5], c="k", lw=lw)
            
    return rect

Node.draw_node = draw_node
修正箇所
from tree import Node, Rect

-def draw_node(self, ax=None, maxdepth=None, emphasize=False, size=0.25, lw=0.8, dx=0, dy=0):
+def draw_node(self, ax=None, maxdepth=None, emphasize=False, darkness=0, size=0.25, lw=0.8, dx=0, dy=0):
元と同じなので省略
    # 自分自身のノードを真ん中の位置になるように (dx, dy) からずらして描画する
    y = dy + (self.height - 3) / 2
+   bc = "red" if emphasize else None
    Marubatsu_GUI.draw_board(ax, self.mb, show_result=True, 
-                            score=getattr(self, "score", None), lw=lw,
+                            score=getattr(self, "score", None), bc=bc, darkness=darkness, lw=lw, dx=dx, dy=y)
元と同じなので省略

Node.draw_node = draw_node

上記の修正後に、下記のプログラムで draw_node を新しい仮引数に対応する実引数を記述せずに 呼び出すと、実行結果のようにこれまで通りの描画が行われることが確認できます。

from tree import Mbtree

mbtree = Mbtree.load("../data/aidata")
mbtree.root.draw_node()

実行結果

 

また、下記のプログラムで draw_node を実引数に emphasize=Truedarkness=0.3 を記述して呼び出すと、実行結果のように指定した設定で描画が行われることが確認できます。なお、上記で説明したように実引数 maxdepth を記述せずに draw_node を呼び出した際に表示される子ノードには、emphasizedrakness の設定は影響を与えません。

mbtree.root.draw_node(emphasize=True, darkness=0.3)

実行結果
 

また、実行結果はこれまでと同じなので省略しますが、下記のプログラムで Marubatsu_GUI のインスタンスを作成すると、これまでと同様に GUI でゲーム木の部分木が表示されることが確認できます。実際に確認してみて下さい。

from tree import Mbtree_GUI

Mbtree_GUI(mbtree)

Mbtree_Anim クラスに対応した修正の必要性

Mbtree_GUI クラスによるゲーム木の部分木の表示では、ノードを暗く表示する処理は行っていません が、Mbtree_Anim クラス でゲーム木の生成過程や、評価値の計算過程を表示する際には、まだ作成されていないノード評価値が計算されていないノード暗く表示する処理 が下記の update_gui メソッド内の 5 ~ 8 行目に記述されています

  • 5 行目:ノードが作成された順番または、評価値が計算された順番を計算して index に代入する
  • 6 ~ 8 行目index表示中のアニメーションのフレーム数 を表す self.play.value より大きい場合 にそのノードを 暗く表示する
1  def update_gui(self):

2      self.mbtree.draw_subtree(centernode=self.centernode, selectednode=self.selectednode,
3                               ax=self.ax, maxdepth=maxdepth)
4      for rect, node in self.mbtree.nodes_by_rect.items():
5          index = node.score_index if self.isscore else node.id
6          if index > self.play.value:
7              self.ax.add_artist(patches.Rectangle(xy=(rect.x, rect.y), width=rect.width,
8                                                   height=rect.height, fc="black", alpha=0.5))
lw=2))

ゲーム盤を暗く表示する処理 は先ほど draw_node で行えるようにした ので、この処理を draw_node で行うように修正する ことにします。どのように修正すれば良いかについて少し考えてみて下さい。

draw_subtree の修正

上記のプログラムでは下記の手順で処理が行われます。

  1. 2 行目で、draw_subtree を呼び出してゲーム木の部分木を描画する
  2. draw_subtree で描画した各ノードに対して、そのノードを暗く表示するかどうかを判定し、暗く表示する必要がある場合に半透明な正方形を上から描画して暗く表示する

手順 2 の処理を draw_board で行うようにする ためには、draw_subtreedraw_node を呼び出す際に そのノードを暗く表示する必要があるかどうかを判定し、暗く表示する必要がある場合はキーワード引数 darkness を記述して draw_node を呼び出すようにする必要があります。そのためには、draw_subtree にノードを暗く表示するかどうかを 判定するために必要なデータ を代入する 仮引数を追加 する必要があります。

ノードを暗く表示するかどうかの 判定に必要なデータ は、先程のプログラムの 5、6 行目から self.isscoreself.play.value の 2 つです。そこで、それらの値を代入する isscoreanim_frame という仮引数を draw_subtree に追加 することにします。ただし、anim_frame をデフォルト値を None とするデフォルト引数とし、None が代入されている場合は暗くする処理を行わないものとします。

draw_subtree のブロックには、draw_node を呼び出す処理が 4 箇所 あり、それぞれに対して仮引数 isscoreanim_frame を使って暗く表示するかどうかを判定する必要があります。同じ処理を 4 回も記述するのは無駄 なので、draw_subtree の中に 表示するノードの暗さを計算する calc_darkness というローカル関数を定義する ことにします。この関数は他の関数やメソッドから利用されることはないと思いましたので、ローカル関数としました。

下記はそのように draw_subtree を修正したプログラムです。

  • 1 行目:デフォルト値を None とする仮引数 anim_frame と、デフォルト値を False とする 仮引数 isscore を追加する
  • 2 ~ 6 行目:仮引数 node に代入されたノードを表示する暗さを計算して返り値として返すローカル関数 calc_darkness を定義する
  • 3、4 行目anim_frameNone の場合は暗く表示する必要がないので、暗く表示しないことを表す 0 を返り値として返す
  • 5、6 行目:Marubatsu_Anim クラスの update_gui と同じ方法でノードを暗く表示するかどうかを判定し、暗く表示する場合は 0.5 を、そうでない場合は 0 を返す
  • 9 ~ 16 行目draw_node を呼び出す際に、calc_darkness を呼び出して表示するノードの暗さを計算し、その返り値をキーワード引数 darkness に記述して呼び出すようにする
 1  def draw_subtree(self, centernode=None, selectednode=None, anim_frame=None, isscore=False, ax=None, size=0.25, lw=0.8, maxdepth=2):
 2      def calc_darkness(node):
 3          if anim_frame is None:
 4              return 0
 5          index = node.score_index if isscore else node.id
 6          return 0.5 if index > anim_frame else 0
 7   
 8      self.nodes_by_rect = {}
元と同じなので省略
 9                  darkness = calc_darkness(node)
10                  rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness, size=size, lw=lw, dx=dx, dy=dy)
元と同じなので省略

11                  darkness = calc_darkness(sibling)
12                  rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, darkness=darkness, lw=lw, dx=dx, dy=dy)
元と同じなので省略
13      darkness = calc_darkness(parent)
14      rect = parent.draw_node(ax, maxdepth=maxdepth, darkness=darkness, size=size, lw=lw, dx=dx, dy=0)
元と同じなので省略
15              darkness = calc_darkness(node)
16              rect = node.draw_node(ax, maxdepth=node.depth, darkness=darkness, size=size, lw=lw, dx=dx, dy=0)
17              self.nodes_by_rect[rect] = node
18            
19  Mbtree.draw_subtree = draw_subtree
行番号のないプログラム
def draw_subtree(self, centernode=None, selectednode=None, anim_frame=None, isscore=False, ax=None, size=0.25, lw=0.8, maxdepth=2):
    def calc_darkness(node):
        if anim_frame is None:
            return 0
        index = node.score_index if isscore else node.id
        return 0.5 if index > anim_frame else 0
    
    self.nodes_by_rect = {}

    if centernode is None:
        centernode = self.root
    self.calc_node_height(N=centernode, maxdepth=maxdepth)
    width = 5 * (maxdepth + 1)
    height = centernode.height
    parent = centernode.parent
    if parent is not None:
        height += (len(parent.children) - 1) * 4
        parent.height = height
    if ax is None:
        fig, ax = plt.subplots(figsize=(width * size, height * size))
        ax.set_xlim(0, width)
        ax.set_ylim(0, height)   
        ax.invert_yaxis()
        ax.axis("off")        
    
    nodelist = [centernode]
    depth = centernode.depth
    while len(nodelist) > 0 and depth <= maxdepth:        
        dy = 0
        if parent is not None:
            dy = parent.children.index(centernode) * 4
        childnodelist = []
        for node in nodelist:
            if node is None:
                dy += 4
                childnodelist.append(None)
            else:
                dx = 5 * node.depth
                emphasize = node is selectednode
                darkness = calc_darkness(node)
                rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness, size=size, lw=lw, dx=dx, dy=dy)
                self.nodes_by_rect[rect] = node
                dy += node.height
                if len(node.children) > 0:  
                    childnodelist += node.children
                else:
                    childnodelist.append(None)
        depth += 1
        nodelist = childnodelist
        
    if parent is not None:
        dy = 0
        for sibling in parent.children:
            if sibling is not centernode:
                sibling.height = 4
                dx = 5 * sibling.depth
                darkness = calc_darkness(sibling)
                rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, darkness=darkness, lw=lw, dx=dx, dy=dy)
                self.nodes_by_rect[rect] = sibling
            dy += sibling.height
        dx = 5 * parent.depth
        darkness = calc_darkness(parent)
        rect = parent.draw_node(ax, maxdepth=maxdepth, darkness=darkness, size=size, lw=lw, dx=dx, dy=0)
        self.nodes_by_rect[rect] = parent
    
        node = parent
        while node.parent is not None:
            node = node.parent
            node.height = height
            dx = 5 * node.depth
            darkness = calc_darkness(node)
            rect = node.draw_node(ax, maxdepth=node.depth, darkness=darkness, size=size, lw=lw, dx=dx, dy=0)
            self.nodes_by_rect[rect] = node
            
Mbtree.draw_subtree = draw_subtree
修正箇所
-def draw_subtree(self, centernode=None, selectednode=None, ax=None, size=0.25, lw=0.8, maxdepth=2):
+def draw_subtree(self, centernode=None, selectednode=None, anim_frame=None, isscore=False, ax=None, size=0.25, lw=0.8, maxdepth=2):
+   def calc_darkness(node):
+       if anim_frame is None:
+           return 0
+       index = node.score_index if isscore else node.id
+       return 0.5 if index > anim_frame else 0
    
    self.nodes_by_rect = {}
元と同じなので省略
+               darkness = calc_darkness(node)
-               rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, size=size, lw=lw, dx=dx, dy=dy)
+               rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness, size=size, lw=lw, dx=dx, dy=dy)
元と同じなので省略
+               darkness = calc_darkness(sibling)
-               rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, lw=lw, dx=dx, dy=dy)
+               rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, darkness=darkness, lw=lw, dx=dx, dy=dy)
元と同じなので省略
+       darkness = calc_darkness(parent)
-       rect = parent.draw_node(ax, maxdepth=maxdepth, size=size, lw=lw, dx=dx, dy=0)
+       rect = parent.draw_node(ax, maxdepth=maxdepth, darkness=darkness, size=size, lw=lw, dx=dx, dy=0)
元と同じなので省略
+           darkness = calc_darkness(node)
-           rect = node.draw_node(ax, maxdepth=node.depth, size=size, lw=lw, dx=dx, dy=0)
+           rect = node.draw_node(ax, maxdepth=node.depth, darkness=darkness, size=size, lw=lw, dx=dx, dy=0)
            self.nodes_by_rect[rect] = node
            
Mbtree.draw_subtree = draw_subtree

update_gui の修正

次に、Marubatsu_Anim クラスの update_gui を下記のプログラムのように修正します。

  • 4 行目draw_subtree を呼び出す際に、キーワード引数 anim_frameisscore を記述するように修正する
  • 5 行目の下にあった、ノードを暗くする処理と赤い外枠を改めて描画する処理を削除する。なお、赤い外枠を改めて描画する処理 は、元のプログラム では暗くする処理によって 赤い外枠も暗くなってしまうため行っていた が、draw_node では 暗く表示する処理の後で外枠を表示する ように修正したので その処理は必要がなくなっている
1  from tree import Mbtree_Anim
2
3  def update_gui(self):
元と同じなので省略
4      self.mbtree.draw_subtree(centernode=self.centernode, selectednode=self.selectednode,
5                               anim_frame=self.play.value, isscore=self.isscore, ax=self.ax, maxdepth=maxdepth)
元と同じなので省略
6    
7   Mbtree_Anim.update_gui = update_gui
行番号のないプログラム
from tree import Mbtree_Anim

def update_gui(self):
    self.ax.clear()
    self.ax.set_xlim(-1, self.width - 1)
    self.ax.set_ylim(0, self.height)   
    self.ax.invert_yaxis()
    self.ax.axis("off")   
    
    self.selectednode = self.nodelist[self.play.value]
    self.centernode = self.selectednode
    if self.mbtree.algo == "bf":
        if self.centernode.depth > 0:
            self.centernode = self.centernode.parent
    while self.centernode.depth > 6:
        self.centernode = self.centernode.parent
    if self.centernode.depth <= 4:
        maxdepth = self.centernode.depth + 1
    elif self.centernode.depth == 5:
        maxdepth = 7
    else:
        maxdepth = 9
    self.mbtree.draw_subtree(centernode=self.centernode, selectednode=self.selectednode,
                             anim_frame=self.play.value, isscore=self.isscore, ax=self.ax, maxdepth=maxdepth)
                            
    disabled = self.play.value == 0
    self.set_button_status(self.prev_button, disabled=disabled)
    disabled = self.play.value == self.nodenum - 1
    self.set_button_status(self.next_button, disabled=disabled)
    
Mbtree_Anim.update_gui = update_gui
修正箇所
from tree import Mbtree_Anim

def update_gui(self):
元と同じなので省略
    self.mbtree.draw_subtree(centernode=self.centernode, selectednode=self.selectednode,
-                            ax=self.ax, maxdepth=maxdepth)
+                            anim_frame=self.play.value, isscore=self.isscore, ax=self.ax, maxdepth=maxdepth)
-   for rect, node in self.mbtree.nodes_by_rect.items():
-       index = node.score_index if self.isscore else node.id
-       if index > self.play.value:
-           self.ax.add_artist(patches.Rectangle(xy=(rect.x, rect.y), width=rect.width,
-                                                height=rect.height, fc="black", alpha=0.5))
-       if node is self.selectednode:
-           self.ax.add_artist(patches.Rectangle(xy=(rect.x, rect.y), width=rect.width,
-                                                height=rect.height, ec="red", fill=False, lw=2))

元と同じなので省略
    
Mbtree_Anim.update_gui = update_gui

実行結果はいずれも省略しますが、下記のプログラムを実行して正しい処理が行われることを確認して下さい。

下記は深さ優先アルゴリズムでゲーム木が作成される様子を表示するアニメーションです。

Mbtree_Anim(mbtree)

下記は幅優先アルゴリズムでゲーム木が作成される様子を表示するアニメーションです。

bftree = Mbtree.load("../data/bftree")
Mbtree_Anim(bftree)

下記は評価値が計算される様子を表示するアニメーションです。

Mbtree_Anim(mbtree, isscore=True)

最善手以外の局面を暗く表示する処理の実装

話がかなり脱線しましたので、先程説明した 最善手を表示する手順 を再掲します。

  1. ノードを描画する処理を行った後で、最善手を着手した局面であるかを判定する
  2. 最善手を着手した局面でないと判定された場合は、以前の記事で説明した方法で、その上に半透明な黒い正方形の画像を描画することでその局面を暗く表示する

また、最善手を着手した局面であるかの判定方法 を再掲します。

  1. 親ノードが存在しない場合はルートノードなので最善手を着手したと判定する
  2. lastmove 属性が、親ノードの bestmoves 属性の要素に含まれている場合は最善手を着手したと判定する
  3. そうでなければ最善手を着手していないと判定する

draw_subtree の修正

上記の処理を行うためには、最善手以外の着手が行われたノードを暗く表示することができるよう に、draw_subtree を修正する 必要があります。そこで、下記のプログラムのように draw_subtreeTrue が代入されていた場合に最善手以外の着手が行われたノードを暗く表示する仮引数 show_bestmove を追加 することにします。なお、下記では show_bestmoveanim_frame の両方が None ではない場合は、show_bestmove を優先することにしました。

  • 2 行目:デフォルト値を False とする仮引数 show_bestmove を追加する
  • 4 ~ 10 行目:ノードを表示する暗さを計算する cacl_darkness の中で、show_bestmoveTrue の場合の暗さを計算する処理を記述する
  • 5、6 行目:親ノードが存在しない場合は最善手を着手したと判定し、暗く表示しないことを表す 0 を返す
  • 7、8 行目:直前の着手が親ノードの最善手の一覧に含まれている場合は、最善手が着手されているので 0 を返す
  • 9、10 行目:上記のどちらでもなければ最善手が着手されていないので、少し暗く表示することを表す 0.2 を返す。なお、anim_frame が設定されている際に暗く表示する場合は 0.5 を返していたが、0.5 だと暗すぎる感じがしたので 0.2 とした。別の暗さで表示したい場合は自由に変更すること
 1  def draw_subtree(self, centernode=None, selectednode=None, anim_frame=None, isscore=False,
 2                   ax=None, show_bestmove=False, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
 3      def calc_darkness(node):
 4          if show_bestmove:
 5              if node.parent is None:
 6                  return 0
 7              elif node.mb.last_move in node.parent.bestmoves:
 8                  return 0
 9              else:
10                  return 0.2
11          if anim_frame is None:
12              return 0
13          index = node.score_index if isscore else node.id
14          return 0.5 if index > anim_frame else 0
元と同じなので省略    
15            
16  Mbtree.draw_subtree = draw_subtree
行番号のないプログラム
def draw_subtree(self, centernode=None, selectednode=None, anim_frame=None, isscore=False,
                 ax=None, show_bestmove=False, size=0.25, lw=0.8, maxdepth=2):
    def calc_darkness(node):
        if show_bestmove:
            if node.parent is None:
                return 0
            elif node.mb.last_move in node.parent.bestmoves:
                return 0
            else:
                return 0.2
            
        if anim_frame is None:
            return 0
        index = node.score_index if isscore else node.id
        return 0.5 if index > anim_frame else 0
    
    self.nodes_by_rect = {}

    if centernode is None:
        centernode = self.root
    self.calc_node_height(N=centernode, maxdepth=maxdepth)
    width = 5 * (maxdepth + 1)
    height = centernode.height
    parent = centernode.parent
    if parent is not None:
        height += (len(parent.children) - 1) * 4
        parent.height = height
    if ax is None:
        fig, ax = plt.subplots(figsize=(width * size, height * size))
        ax.set_xlim(0, width)
        ax.set_ylim(0, height)   
        ax.invert_yaxis()
        ax.axis("off")        
    
    nodelist = [centernode]
    depth = centernode.depth
    while len(nodelist) > 0 and depth <= maxdepth:        
        dy = 0
        if parent is not None:
            dy = parent.children.index(centernode) * 4
        childnodelist = []
        for node in nodelist:
            if node is None:
                dy += 4
                childnodelist.append(None)
            else:
                dx = 5 * node.depth
                emphasize = node is selectednode
                darkness = calc_darkness(node)
                rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness, size=size, lw=lw, dx=dx, dy=dy)
                self.nodes_by_rect[rect] = node
                dy += node.height
                if len(node.children) > 0:  
                    childnodelist += node.children
                else:
                    childnodelist.append(None)
        depth += 1
        nodelist = childnodelist
        
    if parent is not None:
        dy = 0
        for sibling in parent.children:
            if sibling is not centernode:
                sibling.height = 4
                dx = 5 * sibling.depth
                darkness = calc_darkness(sibling)
                rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, darkness=darkness, lw=lw, dx=dx, dy=dy)
                self.nodes_by_rect[rect] = sibling
            dy += sibling.height
        dx = 5 * parent.depth
        darkness = calc_darkness(parent)
        rect = parent.draw_node(ax, maxdepth=maxdepth, darkness=darkness, size=size, lw=lw, dx=dx, dy=0)
        self.nodes_by_rect[rect] = parent
    
        node = parent
        while node.parent is not None:
            node = node.parent
            node.height = height
            dx = 5 * node.depth
            darkness = calc_darkness(node)
            rect = node.draw_node(ax, maxdepth=node.depth, darkness=darkness, size=size, lw=lw, dx=dx, dy=0)
            self.nodes_by_rect[rect] = node
            
Mbtree.draw_subtree = draw_subtree
修正箇所
def draw_subtree(self, centernode=None, selectednode=None, anim_frame=None, isscore=False,
-                ax=None, size=0.25, lw=0.8, maxdepth=2):
+                ax=None, show_bestmove=False, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
    def calc_darkness(node):
+       if show_bestmove:
+           if node.parent is None:
+               return 0
+           elif node.mb.last_move in node.parent.bestmoves:
+               return 0
+           else:
+               return 0.2
        if anim_frame is None:
            return 0
        index = node.score_index if isscore else node.id
        return 0.5 if index > anim_frame else 0
元と同じなので省略    
            
Mbtree.draw_subtree = draw_subtree

update_gui メソッドの修正

次に、Mbtree_GUI クラスの update_gui の中で、下記のプログラムの 3 行目のように draw_subtree を呼び出す際に show_bestmove=True を記述するように修正します。

1  def update_gui(self):
元と同じなので省略
2      self.mbtree.draw_subtree(centernode=centernode, selectednode=self.selectednode,
3                               show_bestmove=True, ax=self.ax, maxdepth=maxdepth)
元と同じなので省略
4    
5  Mbtree_GUI.update_gui = update_gui
行番号のないプログラム
def update_gui(self):
    self.ax.clear()
    self.ax.set_xlim(-1, self.width - 1)
    self.ax.set_ylim(0, self.height)   
    self.ax.invert_yaxis()
    self.ax.axis("off")   
    
    if self.selectednode.depth <= 4:
        maxdepth = self.selectednode.depth + 1
    elif self.selectednode.depth == 5:
        maxdepth = 7
    else:
        maxdepth = 9
    centernode = self.selectednode
    while centernode.depth > 6:
        centernode = centernode.parent
    self.mbtree.draw_subtree(centernode=centernode, selectednode=self.selectednode,
                             show_bestmove=True, ax=self.ax, maxdepth=maxdepth)
    
    disabled = self.selectednode.parent is None
    self.set_button_status(self.left_button, disabled=disabled)
    disabled = self.selectednode.depth >= 6 or len(self.selectednode.children) == 0
    self.set_button_status(self.right_button, disabled=disabled)
    disabled = self.selectednode.parent is None or self.selectednode.parent.children.index(self.selectednode) == 0
    self.set_button_status(self.up_button, disabled=disabled)
    disabled = self.selectednode.parent is None or self.selectednode.parent.children[-1] is self.selectednode
    self.set_button_status(self.down_button, disabled=disabled)
    
Mbtree_GUI.update_gui = update_gui
修正箇所
def update_gui(self):
元と同じなので省略
    self.mbtree.draw_subtree(centernode=centernode, selectednode=self.selectednode,
-                            ax=self.ax, maxdepth=maxdepth)
+                            show_bestmove=True, ax=self.ax, maxdepth=maxdepth)
元と同じなので省略
    
Mbtree_GUI.update_gui = update_gui

上記の修正後に下記のプログラムを実行し、別のノードをクリックして選択する4と実行結果の左図ように最善手が着手されていない局面のノードが暗く表示されます。また、実行結果の右図では過去の 3 手分の着手がいずれも最善手ではないことがわかるようになります。

gui_play()

実行結果(GUI の部分木のみを表示します)

 

以上で、最善手を明確にする処理の実装は完了です。

最善手のみを着手した場合の部分木の表示

前回の記事では、将棋や囲碁の中継などで、プロ棋士の対戦中の局面に対して下図の上部のような「現在の局面から数手先までの最善手の一覧」という工夫がされているという話をしました。そこで、〇×ゲームでも、GUI の部分木に同様の表示を行う ことにします。どのように表示を行えば良いかについて少し考えてみて下さい。

表示の仕様

現状の GUI の部分木では、現在の局面の 深さが浅い場合 は、下図のように現在の局面の 次の深さのノードまでしか表示されません が、その後に そこから決着がつくまで 最善手のみを着手した局面を表示する という方法が考えられます。

ピンとこない人が多いのではないかと思いますので、完成版の GUI の部分木の表示例を下図に示します。下図では、ゲーム開始時の局面に対する 9 つの それぞれの合法手を着手した局面 に対して、その 右の背景が灰色になっている部分最善手を着手し続けた場合の局面 を並べて表示しています。このような表示を行うことで、それぞれの合法手を着手した後試合の展開と結果が一目でわかる ようになります。

なお、先程の図の将棋というゲームは決着がつくまでに平均約 100 手程かかるので、3 手先までの最善手の一覧を表示していますが、〇×ゲームは最大でも 9 手までなので、ゲームの決着がつくまでの最善手のを着手した局面の一覧を表示 することにしました。

また、最善手が複数ある場合 は、bestmoves 属性の 最初の最善手のみ を着手することにします。そうすることで、着手の組み合わせ が必ず 1 通りになる ので、最善手のみを着手した局面を表示しても、GUI の部分木の 縦幅が広がることはありません

上記の処理をどのように実装すればよいかについて少し考えてみて下さい。

draw_subtree の修正

GUI の部分木draw_subtree メソッドによって描画を行います。その中で 中心となる centernode から maxdepth までの部分木を表する という処理を行っていますが、その際に maxdepth の深さの子ノードを描画した後 で、ゲームの決着がつくまで最善手を着手したノードを表示し続ける という処理を記述する必要があります。また、これまでと同様の方法で部分木を表示できるようにする ために、その処理は先ほど追加した 仮引数 show_bestmoveTrue が代入されている場合に行う ことにします。

下記はそのように draw_subtree を修正したプログラムです。

  • 4 ~ 12 行目:2 行目で node に代入されたノードを draw_node で描画した後で、show_bestmoveTrue が代入されており、node の深さが maxdepth と等しい場合に node から最善手のみを着手した場合の局面を表示する処理を行う
  • 5 行目node に代入された値 は、13 行目のように この処理が行われた後で利用する ので、node の値を bestnode という変数に代入 し、その変数を使って処理を行う
  • 6 行目bestnode の最善手を表す bestmoves 属性に要素が存在する間繰り返し処理を行う。ゲーム中であるばあいは最善手が必ず存在することから、この条件式を bestnode.mb.status == Marubatsu.PLAYING としても良い
  • 7、8 行目bestmoves 属性の最初の要素を着手する最善手とし、children_by_move 属性を利用してその最善手を着手した子ノードを計算して bestnode に代入する
  • 9 行目bestnode を表示する位置の x 座標をノードの深さから計算する
  • 10 行目draw_subtree では、最初に self.calc_node_height(N=centernode, maxdepth=maxdepth) を実行することで、深さ maxdepth までのノードの表示の高さを計算している が、これから表示する bestnode の深さは maxdepth よりも深い ので height 属性の計算は行っていない。そのため、height 属性 に表示するノードの高さを表す 4 を代入しておく必要がある5点に注意すること
  • 11 行目draw_node メソッドを使って bestnode を表示する。その際に、maxdepthbestnode の深さを代入することで bestnode に子ノードが存在する場合に右に 1 本だけ線を描画するようにする。また、bestnode は最善手を着手した局面 なので、暗く表示する必要はない ので、実引数 darkness を記述する必要はない
  • 12 行目nodes_by_rect属性に代入された dict に bestnode の情報を登録する6
 1  def draw_subtree(self, centernode=None, selectednode=None, anim_frame=None, isscore=False, ax=None, show_bestmove=False, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
 2                  rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness, size=size, lw=lw, dx=dx, dy=dy)
 3                  self.nodes_by_rect[rect] = node
 4                  if show_bestmove and depth == maxdepth:
 5                      bestnode = node
 6                      while len(bestnode.bestmoves) > 0:
 7                          bestmove = bestnode.bestmoves[0]
 8                          bestnode = bestnode.children_by_move[bestmove]
 9                          dx = 5 * bestnode.depth
10                          bestnode.height = 4
11                          rect = bestnode.draw_node(ax=ax, maxdepth=bestnode.depth, emphasize=emphasize, size=size, lw=lw, dx=dx, dy=dy)
12                          self.nodes_by_rect[rect] = bestnode
13                  dy += node.height
元と同じなので省略                    
13            
14  Mbtree.draw_subtree = draw_subtree
行番号のないプログラム
def draw_subtree(self, centernode=None, selectednode=None, anim_frame=None, isscore=False, ax=None, show_bestmove=False, size=0.25, lw=0.8, maxdepth=2):
    def calc_darkness(node):
        if show_bestmove:
            if node.parent is None:
                return 0
            elif node.mb.last_move in node.parent.bestmoves:
                return 0
            else:
                return 0.2
            
        if anim_frame is None:
            return 0
        index = node.score_index if isscore else node.id
        return 0.5 if index > anim_frame else 0
    
    self.nodes_by_rect = {}

    if centernode is None:
        centernode = self.root
    self.calc_node_height(N=centernode, maxdepth=maxdepth)
    width = 5 * (maxdepth + 1)
    height = centernode.height
    parent = centernode.parent
    if parent is not None:
        height += (len(parent.children) - 1) * 4
        parent.height = height
    if ax is None:
        fig, ax = plt.subplots(figsize=(width * size, height * size))
        ax.set_xlim(0, width)
        ax.set_ylim(0, height)   
        ax.invert_yaxis()
        ax.axis("off")        
    
    nodelist = [centernode]
    depth = centernode.depth
    while len(nodelist) > 0 and depth <= maxdepth:        
        dy = 0
        if parent is not None:
            dy = parent.children.index(centernode) * 4
        childnodelist = []
        for node in nodelist:
            if node is None:
                dy += 4
                childnodelist.append(None)
            else:
                dx = 5 * node.depth
                emphasize = node is selectednode
                darkness = calc_darkness(node)
                rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness, size=size, lw=lw, dx=dx, dy=dy)
                self.nodes_by_rect[rect] = node
                if show_bestmove and depth == maxdepth:
                    bestnode = node
                    while len(bestnode.bestmoves) > 0:
                        bestmove = bestnode.bestmoves[0]
                        bestnode = bestnode.children_by_move[bestmove]
                        dx = 5 * bestnode.depth
                        bestnode.height = 4
                        rect = bestnode.draw_node(ax=ax, maxdepth=bestnode.depth, emphasize=emphasize, size=size, lw=lw, dx=dx, dy=dy)
                        self.nodes_by_rect[rect] = bestnode
                    
                dy += node.height
                if len(node.children) > 0:  
                    childnodelist += node.children
                else:
                    childnodelist.append(None)
        depth += 1
        nodelist = childnodelist
        
    if parent is not None:
        dy = 0
        for sibling in parent.children:
            if sibling is not centernode:
                sibling.height = 4
                dx = 5 * sibling.depth
                darkness = calc_darkness(sibling)
                rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, darkness=darkness, lw=lw, dx=dx, dy=dy)
                self.nodes_by_rect[rect] = sibling
            dy += sibling.height
        dx = 5 * parent.depth
        darkness = calc_darkness(parent)
        rect = parent.draw_node(ax, maxdepth=maxdepth, darkness=darkness, size=size, lw=lw, dx=dx, dy=0)
        self.nodes_by_rect[rect] = parent
    
        node = parent
        while node.parent is not None:
            node = node.parent
            node.height = height
            dx = 5 * node.depth
            darkness = calc_darkness(node)
            rect = node.draw_node(ax, maxdepth=node.depth, darkness=darkness, size=size, lw=lw, dx=dx, dy=0)
            self.nodes_by_rect[rect] = node
            
Mbtree.draw_subtree = draw_subtree
修正箇所
def draw_subtree(self, centernode=None, selectednode=None, anim_frame=None, isscore=False, ax=None, show_bestmove=False, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
                rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness, size=size, lw=lw, dx=dx, dy=dy)
                self.nodes_by_rect[rect] = node
+               if show_bestmove and depth == maxdepth:
+                   bestnode = node
+                   while len(bestnode.bestmoves) > 0:
+                       bestmove = bestnode.bestmoves[0]
+                       bestnode = bestnode.children_by_move[bestmove]
+                       dx = 5 * bestnode.depth
+                       bestnode.height = 4
+                       rect = bestnode.draw_node(ax=ax, maxdepth=bestnode.depth, emphasize=emphasize, size=size, lw=lw, dx=dx, dy=dy)
+                       self.nodes_by_rect[rect] = bestnode
                dy += node.height
元と同じなので省略                    
            
Mbtree.draw_subtree = draw_subtree

上記の修正後に下記のプログラムを実行すると、実行結果の左図のように、ゲーム開始時の局面のそれぞれの子ノードの後ろに、決着がつくまで最善手を着手し続けた場合の局面が表示されるようになります。右図は、別のノードをクリックして選択した場合のものです。最善手である (1, 1) を着手した場合に引き分けになる着手の例と、それ以外を着手した場合に 〇 が勝利する着手の例が表示されます。

gui_play()

実行結果(GUI の部分木のみを表示します)

 

最善手を着手した局面の区別

上図では どこからが最善手を着手し続けた局面であるかがわかりづらい という欠点があります。そこで、最善手を着手し続けた局面 の部分の 背景色を灰色で表示 することで 区別できるようにする という工夫を行うことにします。

下記はそのように draw_subtree を修正したプログラムです。

  • 2 ~ 6 行目show_bestmoveTrue の場合に、maxdepth より深いノードの部分の背景色を灰色で表示するようにする
  • 3 行目:灰色の長方形の x 座標を計算して bestx に代入する。この式は、深さ maxdepth のノードの x 座標を表す 5 * maxdepth に様々な数値を足した式を記述してプログラムを実行して表示を確認するという、試行錯誤によって決めた式 である
  • 4 行目Mbtree_GUI では、Figure の幅を 50 に設定している ので、灰色の長方形の表示幅は 50 から bestx を引いた値で計算できる
  • 5、6 行目:Figure の中で、bestx から右の長方形の範囲を灰色で塗りつぶす
 1  def draw_subtree(self, centernode=None, selectednode=None, anim_frame=None, isscore=False, ax=None, show_bestmove=False, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
 2      if show_bestmove:
 3          bestx = 5 * maxdepth + 4
 4          bestwidth = 50 - bestx
 5          ax.add_artist(patches.Rectangle(xy=(bestx, 0), width=bestwidth,
 6                                          height=height, fc="lightgray"))
 7    
 8      nodelist = [centernode]
 9      depth = centernode.depth
10      while len(nodelist) > 0 and depth <= maxdepth:        
元と同じなので省略
11            
12  Mbtree.draw_subtree = draw_subtree
行番号のないプログラム
def draw_subtree(self, centernode=None, selectednode=None, anim_frame=None, isscore=False, ax=None, show_bestmove=False, size=0.25, lw=0.8, maxdepth=2):
    def calc_darkness(node):
        if show_bestmove:
            if node.parent is None:
                return 0
            elif node.mb.last_move in node.parent.bestmoves:
                return 0
            else:
                return 0.2
            
        if anim_frame is None:
            return 0
        index = node.score_index if isscore else node.id
        return 0.5 if index > anim_frame else 0
    
    self.nodes_by_rect = {}

    if centernode is None:
        centernode = self.root
    self.calc_node_height(N=centernode, maxdepth=maxdepth)
    width = 5 * (maxdepth + 1)
    height = centernode.height
    parent = centernode.parent
    if parent is not None:
        height += (len(parent.children) - 1) * 4
        parent.height = height
    if ax is None:
        fig, ax = plt.subplots(figsize=(width * size, height * size))
        ax.set_xlim(0, width)
        ax.set_ylim(0, height)   
        ax.invert_yaxis()
        ax.axis("off")        
    
    if show_bestmove:
        bestx = 5 * maxdepth + 4
        bestwidth = 50 - bestx
        ax.add_artist(patches.Rectangle(xy=(bestx, 0), width=bestwidth,
                                        height=height, fc="lightgray"))
    
    nodelist = [centernode]
    depth = centernode.depth
    while len(nodelist) > 0 and depth <= maxdepth:        
        dy = 0
        if parent is not None:
            dy = parent.children.index(centernode) * 4
        childnodelist = []
        for node in nodelist:
            if node is None:
                dy += 4
                childnodelist.append(None)
            else:
                dx = 5 * node.depth
                emphasize = node is selectednode
                darkness = calc_darkness(node)
                rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness, size=size, lw=lw, dx=dx, dy=dy)
                self.nodes_by_rect[rect] = node
                if show_bestmove and depth == maxdepth:
                    bestnode = node
                    while len(bestnode.bestmoves) > 0:
                        bestmove = bestnode.bestmoves[0]
                        bestnode = bestnode.children_by_move[bestmove]
                        dx = 5 * bestnode.depth
                        bestnode.height = 4
                        rect = bestnode.draw_node(ax=ax, maxdepth=bestnode.depth, emphasize=emphasize, size=size, lw=lw, dx=dx, dy=dy)
                        self.nodes_by_rect[rect] = bestnode                                          
                    
                dy += node.height
                if len(node.children) > 0:  
                    childnodelist += node.children
                else:
                    childnodelist.append(None)
        depth += 1
        nodelist = childnodelist
        
    if parent is not None:
        dy = 0
        for sibling in parent.children:
            if sibling is not centernode:
                sibling.height = 4
                dx = 5 * sibling.depth
                darkness = calc_darkness(sibling)
                rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, darkness=darkness, lw=lw, dx=dx, dy=dy)
                self.nodes_by_rect[rect] = sibling
            dy += sibling.height
        dx = 5 * parent.depth
        darkness = calc_darkness(parent)
        rect = parent.draw_node(ax, maxdepth=maxdepth, darkness=darkness, size=size, lw=lw, dx=dx, dy=0)
        self.nodes_by_rect[rect] = parent
    
        node = parent
        while node.parent is not None:
            node = node.parent
            node.height = height
            dx = 5 * node.depth
            darkness = calc_darkness(node)
            rect = node.draw_node(ax, maxdepth=node.depth, darkness=darkness, size=size, lw=lw, dx=dx, dy=0)
            self.nodes_by_rect[rect] = node
            
Mbtree.draw_subtree = draw_subtree
修正箇所
def draw_subtree(self, centernode=None, selectednode=None, anim_frame=None, isscore=False, ax=None, show_bestmove=False, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
+   if show_bestmove:
+       bestx = 5 * maxdepth + 4
+       bestwidth = 50 - bestx
+       ax.add_artist(patches.Rectangle(xy=(bestx, 0), width=bestwidth,
+                                       height=height, fc="lightgray"))
    
    nodelist = [centernode]
    depth = centernode.depth
    while len(nodelist) > 0 and depth <= maxdepth:        
元と同じなので省略
            
Mbtree.draw_subtree = draw_subtree

上記の修正後に下記のプログラムを実行すると、実行結果の左図のように、最善手を着手し続けた部分の背景色が灰色で表示されるようになります。右図は別の局面を中心とした場合の図です。

gui_play()

実行結果(GUI の部分木のみを表示します)

 

これで、GUI のゲーム木から以下の事が簡単にわかるようになりました。

  • 現在の局面に対する 合法手の中の最善手
  • それぞれの合法手を着手した局面から、最善手を着手し続けた場合の決着までの経過

ヘルプの表示の ON/OFF

現状では、〇×ゲームの GUI? ボタンのヘルプ に関して 下記のような問題 があります。

  • ヘルプを表示すると、ゲームをリセットするまでヘルプの表示が消えない
  • ヘルプの表示 と、既にマークが配置されたマスをクリックした際の メッセージの表示同じ場所に行われる

また、また、GUI の部分木 の ? ボタンの場合は、一度クリックすると ヘルプの表示が二度と消えなくなる という問題があります。

そこで、? ボタン によって、ヘルプの表示の有無を切り替える ことができるように修正することにします。表示の切り替えは、前回の記事で説明したように、ウィジェットの layout.display 属性を利用することで行えます。

Marubatsu_GUI クラスのヘルプの表示の修正

まず、ヘルプの表示と、既にマークが配置されたマスをクリックした際のメッセージの 表示を分けるため に、下記のプログラムのように create_widgets を修正します。

  • 8 行目:ヘルプを表示する Output を作成し、help 属性に代入する
  • 9 行目:ヘルプの表示の有無は、layout.display 属性を使って切り替えることにしたので、ヘルプのメッセージは最初に表示しておけばよい。ヘルプに表示するメッセージを修正するたびに create_widgets メソッドを修正するのは面倒なので、メッセージを表示する print_helpmessage メソッドをこの後で定義 し、そのメソッドを呼び出すことにする
  • 10 行目layout.display 属性に "none" を代入して 最初はヘルプを非表示状態にする
 1  import ipywidgets as widgets 
 2
 3  def create_widgets(self):
元と同じなので省略
 4      # print による文字列を表示する Output を作成する
 5      self.output = widgets.Output()  
 6    
 7      # ヘルプを表示する Output を作成し、表示の設定を行う
 8      self.help = widgets.Output()
 9      self.print_helpmessage()
10      self.help.layout.display = "none"
11    
12  Marubatsu_GUI.create_widgets = create_widgets
行番号のないプログラム
import ipywidgets as widgets 

def create_widgets(self):
    # 乱数の種の Checkbox と IntText を作成する
    self.checkbox = widgets.Checkbox(value=self.seed is not None, description="乱数の種",
                                    indent=False, layout=widgets.Layout(width="100px"))
    self.inttext = widgets.IntText(value=0 if self.seed is None else self.seed,
                                layout=widgets.Layout(width="80px"))   

    # 読み書き、ヘルプのボタンを作成する
    self.load_button = self.create_button("開く", 50)
    self.save_button = self.create_button("保存", 50)
    self.show_tree_button = self.create_button("", 34)
    self.reset_tree_button = self.create_button("", 34)
    self.help_button = self.create_button("", 34)
    
    # AI を選択する Dropdown を作成する
    self.create_dropdown()
    # 変更、リセット、待ったボタンを作成する
    self.change_button = self.create_button("変更", 50)
    self.reset_button = self.create_button("リセット", 80)
    self.undo_button = self.create_button("待った", 60)    
    
    # リプレイのボタンとスライダーを作成する
    self.first_button = self.create_button("<<", 50)
    self.prev_button = self.create_button("<", 50)
    self.next_button = self.create_button(">", 50)
    self.last_button = self.create_button(">>", 50)     
    self.slider = widgets.IntSlider(layout=widgets.Layout(width="200px"))
    # ゲーム盤の画像を表す figure を作成する
    self.create_figure()

    # print による文字列を表示する Output を作成する
    self.output = widgets.Output()  
    
    # ヘルプを表示する Output を作成し、表示の設定を行う
    self.help = widgets.Output()
    self.print_helpmessage()
    self.help.layout.display = "none"
    
Marubatsu_GUI.create_widgets = create_widgets
修正箇所
import ipywidgets as widgets 

def create_widgets(self):
元と同じなので省略
    # print による文字列を表示する Output を作成する
    self.output = widgets.Output()  
    
    # ヘルプを表示する Output を作成し、表示の設定を行う
+   self.help = widgets.Output()
+   self.print_helpmessage()
+   self.help.layout.display = "none"
    
Marubatsu_GUI.create_widgets = create_widgets

print_helpmessage メソッドの定義

次に、下記のプログラムのように print_helpmessage メソッドを定義します。行う処理は、with self.outputwith self.help に変更した以外は create_eventhandler 内でヘルプを表示する処理と同じです。なお、下記のプログラムには、前回の記事で追加した「木」と「リ」ボタンの説明を追記しました。

def print_helpmessage(self):
    with self.help:
        print("""操作説明

    マスの上でクリックすることで着手を行う。
    下記の GUI で操作を行うことができる。
    ()が記載されているものは、キー入力で同じ操作を行うことができることを意味する。
    なお、キー入力の操作は、ゲーム盤をクリックして選択状態にする必要がある。

    乱数の種\tチェックボックスを ON にすると、右のテキストボックスの乱数の種が適用される
    開く(-,L)\tファイルから対戦データを読み込む
    保存(+,S)\tファイルに対戦データを保存する
    木\t下部の GUI の部分木を表示の有無を切り替える
    リ\r株の GUI の部分木の中心となるノードを、現在の局面にリセットする
    ?(*,H)\t\tこの操作説明を表示する
    手番の担当\tメニューからそれぞれの手番の担当を選択する
    \t\tメニューから選択しただけでは担当は変更されず、変更またはリセットボタンによって担当が変更される
    変更\t\tゲームの途中で手番の担当を変更する
    リセット\t手番の担当を変更してゲームをリセットする
    待った(0)\t1つ前の自分の着手をキャンセルする
    <<(↑)\t\t最初の局面に移動する
    <(←)\t\t1手前の局面に移動する
    >(→)\t\t1手後の局面に移動する
    >>(↓)\t\t最後の着手が行われた局面に移動する
    スライダー\t現在の手数を表す。ドラッグすることで任意の手数へ移動する

    手数を移動した場合に、最後の着手が行われた局面でなければ、リプレイモードになる。
    リプレイモード中に着手を行うと、リプレイモードが解除され、その着手が最後の着手になる。""")
        
Marubatsu_GUI.print_helpmessage = print_helpmessage

display_widgets の修正

次に、display_widgets を下記のプログラムのように修正します。

  • 3 行目:VBox の最後に self.help を配置するように修正する
1  def display_widgets(self):
元と同じなので省略
2      # hbox1 ~ hbox3、Figure、Output を縦に配置した VBox を作成し、表示する
3      display(widgets.VBox([hbox1, hbox2, hbox3, self.fig.canvas, self.output, self.help]))
4    
5  Marubatsu_GUI.display_widgets = display_widgets 
行番号のないプログラム
def display_widgets(self):
    # 乱数の種のウィジェット、読み書き、ヘルプのボタンを横に配置した HBox を作成する
    hbox1 = widgets.HBox([self.checkbox, self.inttext, self.load_button, self.save_button,
                        self.show_tree_button, self.reset_tree_button, self.help_button])
    # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
    hbox2 = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1], self.change_button, self.reset_button, self.undo_button])
    # リプレイ機能のボタンを横に配置した HBox を作成する
    hbox3 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button, self.slider]) 
    # hbox1 ~ hbox3、Figure、Output を縦に配置した VBox を作成し、表示する
    display(widgets.VBox([hbox1, hbox2, hbox3, self.fig.canvas, self.output, self.help]))
    
Marubatsu_GUI.display_widgets = display_widgets 
修正箇所
def display_widgets(self):
元と同じなので省略
    # hbox1 ~ hbox3、Figure、Output を縦に配置した VBox を作成し、表示する
    display(widgets.VBox([hbox1, hbox2, hbox3, self.fig.canvas, self.output, self.help]))
    
Marubatsu_GUI.display_widgets = display_widgets 

create_event_handler の修正

最後に、create_event_handler 内で ? ボタンをクリックした際のイベントハンドラを下記のプログラムのように修正します。

  • 5 行目前回の記事で GUI の部分木の表示の有無を切り替えたのと同じ方法で、ヘルプメッセージの Ouput の表示を切り替える処理を記述する
  • 5 行目の下にあった、ヘルプの Output の表示内容をクリアする処理と、ヘルプメッセージを表示する処理は必要が無くなったので削除する
1  import math
2
3  def create_event_handler(self):
元と同じなので省略
4      def on_help_button_clicked(b=None):
5          self.help.layout.display = "none" if self.help.layout.display is None else None
元と同じなので省略           
6        
7  Marubatsu_GUI.create_event_handler = create_event_handler    
行番号のないプログラム
import math

def create_event_handler(self):
    # 乱数の種のチェックボックスのイベントハンドラを定義する
    def on_checkbox_changed(changed):
        self.update_widgets_status()
        
    self.checkbox.observe(on_checkbox_changed, names="value")

    # 開く、保存ボタンのイベントハンドラを定義する
    def on_load_button_clicked(b=None):
        path = filedialog.askopenfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
                                        initialdir="save")
        if path != "":
            with open(path, "rb") as f:
                data = pickle.load(f)
                self.mb.records = data["records"]
                self.mb.ai = data["ai"]
                self.params = data["params"] if "params" in data else [ {}, {} ]
                if "names" in data:
                    names = data["names"]
                else:
                    names = [ "人間" if mb.ai[i] is None else mb.ai[i].__name__ for i in range(2)]                       
                options = self.dropdown_list[0].options.copy()
                for i in range(2):
                    value = (self.mb.ai[i], self.params[i]) 
                    if not value in options.values():
                        options[names[i]] = value
                for i in range(2):
                    self.dropdown_list[i].options = options
                    self.dropdown_list[i].value = (self.mb.ai[i], self.params[i])            
                change_step(data["move_count"])
                if data["seed"] is not None:                   
                    self.checkbox.value = True
                    self.inttext.value = data["seed"]
                else:
                    self.checkbox.value = False
                    
    def on_save_button_clicked(b=None):
        names = [ self.dropdown_list[i].label for i in range(2) ]     
        timestr = datetime.now().strftime("%Y年%m月%d日 %H時%M分%S秒")
        fname = f"{names[0]} VS {names[1]} {timestr}"
        path = filedialog.asksaveasfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
                                            initialdir="save", initialfile=fname,
                                            defaultextension="mbsav")
        if path != "":
            with open(path, "wb") as f:
                data = {
                    "records": self.mb.records,
                    "move_count": self.mb.move_count,
                    "ai": self.mb.ai,
                    "params": self.params,
                    "names": names,
                    "seed": self.inttext.value if self.checkbox.value else None
                }
                pickle.dump(data, f)
                
    def on_show_tree_button_clicked(b=None):
        self.mbtree_gui.vbox.layout.display = "none" if self.mbtree_gui.vbox.layout.display is None else None
        
    def on_reset_tree_button_clicked(b=None):
        self.update_gui()
                
    def on_help_button_clicked(b=None):
        self.help.layout.display = "none" if self.help.layout.display is None else None
    
    self.load_button.on_click(on_load_button_clicked)
    self.save_button.on_click(on_save_button_clicked)
    self.show_tree_button.on_click(on_show_tree_button_clicked)
    self.reset_tree_button.on_click(on_reset_tree_button_clicked)
    self.help_button.on_click(on_help_button_clicked)
    
    # 変更ボタンのイベントハンドラを定義する
    def on_change_button_clicked(b):
        for i in range(2):
            self.mb.ai[i], self.params[i] = self.dropdown_list[i].value
        self.mb.play_loop(self, self.params)

    # リセットボタンのイベントハンドラを定義する
    def on_reset_button_clicked(b=None):
        # 乱数の種のチェックボックスが ON の場合に、乱数の種の処理を行う
        if self.checkbox.value:
            random.seed(self.inttext.value)
        self.mb.restart()
        self.output.clear_output()
        on_change_button_clicked(b)

    # 待ったボタンのイベントハンドラを定義する
    def on_undo_button_clicked(b=None):
        if self.mb.move_count >= 2 and self.mb.move_count == len(self.mb.records) - 1:
            self.mb.move_count -= 2
            self.mb.records = self.mb.records[0:self.mb.move_count+1]
            self.mb.change_step(self.mb.move_count)
            self.update_gui()
        
    # イベントハンドラをボタンに結びつける
    self.change_button.on_click(on_change_button_clicked)
    self.reset_button.on_click(on_reset_button_clicked)   
    self.undo_button.on_click(on_undo_button_clicked)   
    
    # step 手目の局面に移動する
    def change_step(step):
        self.mb.change_step(step)
        # 描画を更新する
        self.update_gui()        

    def on_first_button_clicked(b=None):
        change_step(0)

    def on_prev_button_clicked(b=None):
        change_step(self.mb.move_count - 1)

    def on_next_button_clicked(b=None):
        change_step(self.mb.move_count + 1)
        
    def on_last_button_clicked(b=None):
        change_step(len(self.mb.records) - 1)

    def on_slider_changed(changed):
        if self.mb.move_count != changed["new"]:
            change_step(changed["new"])
        
    self.first_button.on_click(on_first_button_clicked)
    self.prev_button.on_click(on_prev_button_clicked)
    self.next_button.on_click(on_next_button_clicked)
    self.last_button.on_click(on_last_button_clicked)
    self.slider.observe(on_slider_changed, names="value")
    
    # ゲーム盤の上でマウスを押した場合のイベントハンドラ
    def on_mouse_down(event):
        # Axes の上でマウスを押していた場合のみ処理を行う
        if event.inaxes and self.mb.status == Marubatsu.PLAYING:
            x = math.floor(event.xdata)
            y = math.floor(event.ydata)
            with self.output:
                self.mb.move(x, y)                
            # 次の手番の処理を行うメソッドを呼び出す
                self.mb.play_loop(self, self.params)

    # ゲーム盤を選択した状態でキーを押した場合のイベントハンドラ
    def on_key_press(event):
        keymap = {
            "up": on_first_button_clicked,
            "left": on_prev_button_clicked,
            "right": on_next_button_clicked,
            "down": on_last_button_clicked,
            "0": on_undo_button_clicked,
            "enter": on_reset_button_clicked,            
            "-": on_load_button_clicked,            
            "l": on_load_button_clicked,            
            "+": on_save_button_clicked,            
            "s": on_save_button_clicked,            
            "*": on_help_button_clicked,            
            "h": on_help_button_clicked,            
        }
        if event.key in keymap:
            keymap[event.key]()
        else:
            try:
                num = int(event.key) - 1
                event.inaxes = True
                event.xdata = num % 3
                event.ydata = 2 - (num // 3)
                on_mouse_down(event)
            except:
                pass
            
    # fig の画像イベントハンドラを結び付ける
    self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)     
    self.fig.canvas.mpl_connect("key_press_event", on_key_press)             
        
Marubatsu_GUI.create_event_handler = create_event_handler       

修正箇所
import math

def create_event_handler(self):
元と同じなので省略
    def on_help_button_clicked(b=None):
+       self.help.layout.display = "none" if self.help.layout.display is None else None
-       self.output.clear_output()
-       with self.output:
-           print("""操作説明
-           以下の表示の部分は長いので省略
元と同じなので省略           
        
Marubatsu_GUI.create_event_handler = create_event_handler       

実行結果は省略しますが、下記のプログラムを実行して ? ボタンをクリックすることで、ヘルプの表示の切り替えを行うことができるようになったことを確認して下さい。また、既にマークが配置されたマスをクリックした際のメッセージが、ヘルプとは別の Output に表示されるようになったことも確認して下さい。

gui_play()

Mbtree_GUI のヘルプの修正

Mbtree_GUI のヘルプも同様の方法で修正することができます。行う処理は上記と同様なので、修正したプログラムのみを下記に記述します。なお、self.outputself.help に修正すると、修正箇所が増えてしまうので、self.output のままとしました。

def create_widgets(self):
    self.output = widgets.Output()  
    self.print_helpmessage()
    self.output.layout.display = "none"
    self.left_button = self.create_button("", 50)
    self.up_button = self.create_button("", 50)
    self.right_button = self.create_button("", 50)
    self.down_button = self.create_button("", 50)
    self.help_button = self.create_button("", 50)
    self.label = widgets.Label(value="", layout=widgets.Layout(width=f"50px"))
    
    with plt.ioff():
        self.fig = plt.figure(figsize=[self.width * self.size,
                                        self.height * self.size])
        self.ax = self.fig.add_axes([0, 0, 1, 1])
    self.fig.canvas.toolbar_visible = False
    self.fig.canvas.header_visible = False
    self.fig.canvas.footer_visible = False
    self.fig.canvas.resizable = False   
    
Mbtree_GUI.create_widgets = create_widgets

def print_helpmessage(self):
    with self.output:
        print("""操作説明

    下記のキーとボタンで中心となるノードを移動できる。
    ←、0 キー:親ノードへ移動
    ↑:一つ前の兄弟ノードへ移動
    ↓:一つ後の兄弟ノードへ移動
    →:先頭の子ノードへ移動

    テンキーで、対応するマスに着手が行われた子ノードへ移動する
    ノードの上でマウスを押すことでそのノードへ移動する
    背景が灰色になっている部分のノードは、最善手を着手し続けた場合のノードを表す
    """)
        
Mbtree_GUI.print_helpmessage = print_helpmessage

def create_event_handler(self):   
    def on_left_button_clicked(b=None):
        if self.selectednode.parent is not None:
            self.selectednode = self.selectednode.parent
            self.update_gui()
            
    def on_right_button_clicked(b=None):
        if len(self.selectednode.children) > 0:
            self.selectednode = self.selectednode.children[0]
            self.update_gui()

    def on_up_button_clicked(b=None):
        if self.selectednode.parent is not None:
            index = self.selectednode.parent.children.index(self.selectednode)
            if index > 0:
                self.selectednode = self.selectednode.parent.children[index - 1]
                self.update_gui()
            
    def on_down_button_clicked(b=None):
        if self.selectednode.parent is not None:
            index = self.selectednode.parent.children.index(self.selectednode)
            if self.selectednode.parent.children[-1] is not self.selectednode:
                self.selectednode = self.selectednode.parent.children[index + 1]
                self.update_gui()            
                
    def on_help_button_clicked(b=None):
        self.output.layout.display = "none" if self.output.layout.display is None else None
                        
    self.left_button.on_click(on_left_button_clicked)
    self.right_button.on_click(on_right_button_clicked)
    self.up_button.on_click(on_up_button_clicked)
    self.down_button.on_click(on_down_button_clicked)
    self.help_button.on_click(on_help_button_clicked)

    def on_key_press(event):
        keymap = {
            "left": on_left_button_clicked,
            "0": on_left_button_clicked,
            "right": on_right_button_clicked,
            "up": on_up_button_clicked,
            "down": on_down_button_clicked,
        }
        if event.key in keymap:
            keymap[event.key]()
        else:
            try:
                num = int(event.key) - 1
                x = num % 3
                y = 2 - (num // 3)
                move = (x, y)
                if move in self.selectednode.children_by_move:
                    self.selectednode = self.selectednode.children_by_move[move]
                    self.update_gui()
            except:
                pass            
            
    def on_mouse_down(event):
        for rect, node in self.mbtree.nodes_by_rect.items():
            if rect.is_inside(event.xdata, event.ydata):
                self.selectednode = node
                self.update_gui()
                break               
            
    # fig の画像イベントハンドラを結び付ける
    self.fig.canvas.mpl_connect("key_press_event", on_key_press)
    self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)  

Mbtree_GUI.create_event_handler = create_event_handler

実行結果は省略しますが、下記のプログラムを実行して GUI の部分木の ? ボタンをクリックすることで、ヘルプの表示の切り替えを行うことができるようになったことを確認して下さい。

gui_play()

今回の記事のまとめ

今回の記事では、最初に循環参照の問題を解決する方法を紹介しました。

次に、合法手の中の最善手がわかるように GUI の部分木の表示を修正し、合法手を着手した局面から最善手を着手し続けた場合の局面を表示するように修正しました。

本記事で入力したプログラム

リンク 説明
marubatsu.ipynb 本記事で入力して実行した JupyterLab のファイル
marubatsu_new.py 今回の記事で更新した marubatsu.py
tree_new.py 今回の記事で更新した tree.py

次回の記事

近日公開予定です

  1. 前回の記事で、ゲーム盤の下に Mbtree_GUI クラスを使って表示する部分木のことを、GUI の部分木 と表記することにしました

  2. matplotlib では線(edge(辺))の色を edgecolor または ec という名前の仮引数に代入しますが、〇×ゲームには外枠の線(border(境界線))だけでなく、ゲーム盤の線も存在するので bordercolor の略の bc としました

  3. ゲーム盤の線の太さを表す仮引数 lw が既に存在するので、こちらは borderwidth の略の bw としました

  4. ゲーム開始時の局面は、すべての合法手が最善手なのでこれまでと表示がかわりません。そのため、別のノードを選択しました

  5. 筆者もこの処理を最初は記述し忘れてしまい、エラーが発生しました

  6. この処理は、ノードをクリックすることでそのノードを中心となるノードに移動する処理を行うためのものです

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0