0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonで〇×ゲームのAIを一から作成する その123 Dropdown による最善手・評価値の対応表の変更

Last updated at Posted at 2024-10-10

目次と前回の記事

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

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

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

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

Dropdown による最善手・評価値の対応表の変更

前回の記事で、Mbtree_GUI クラスのインスタンスを作成する際に、実引数で局面と最善手・評価値の対応表を選択 できるように修正しました。今回の記事では、Mbtree_GUI クラスのインスタンスを 作成した後で、局面と最善手・評価値の対応表を 変更できるようにします

具体的には、Marubatsu_GUI クラスで AI の選択を Dropdown で行えるようにしたのと同様に、Dropdown を使って 局面と最善手・評価値の対応表を 変更できるようにします

Mbtree_GUI クラスの __init__ メソッドの修正

Marubatsu_GUI クラスの __init__ メソッドでは、下記のプログラムのように 仮引数 ai_dict に Dropdown に登録する AI の一覧を表すデータを dict の形式で代入しました

def __init__(self, mb, params, names, ai_dict, seed, size):

そこで、Mbtree_GUI クラスの __init__ メソッドも同様に、scoretable_dict という名前の仮引数に Dropdown に登録する 局面と最善手・評価値の対応表の一覧を表すデータを代入 することにします。なお、仮引数の名前を bestmoves_and_score_by_board_dict とするのはさすがに長すぎる気がしたので、scoretable_dict としました。

scoretable_dict には、Dropdown を作成する際にキーワード引数 options に代入するデータ を代入します。具体的には、下記のプログラムのような、キー に Dropdown に表示する 項目名 を、キーの値局面と最善手・評価値の対応表のデータ を代入します。

なお、基本となる bestmoves_and_score_by_board.dat に対応する項目名は「標準」を表す Standard としました。また、Dropdown に表示する項目名の末尾には () の中にその対応表を利用する AI の名前を記述しました。

from util import load_bestmoves

scoretable_dict = {
    "Standard (ai_gt6)": load_bestmoves("../data/bestmoves_and_score_by_board.dat"),
    "Shortest victory (ai_gtsv)": load_bestmoves("../data/bestmoves_and_score_by_board_shortest_victory.dat"),
}

下記は、__init__ メソッドを修正したプログラムです。scoretable_dict に対応するキーワード引数を省略できるように None をデフォルト値とする仮引数とし、None が代入されている場合は上記のデータを代入することにします。

  • 4 行目:仮引数 bestmoves_and_score_by_board を削除し、デフォルト値を None とする仮引数 scoretable_dict を追加する
  • 5 ~ 11 行目scoretable_dictNone の場合に、上記のデータを scoretable_dict に代入する
  • 6 行目:9、10 行目の処理で load_bestmoves が必要になるが、mbtree.py の先頭で load_bestmoves を util.py からインポートすると循環参照が発生するので、ローカルなインポートload_bestmoves をインポートする必要がある
  • 12 行目bestmoves_and_score_by_board を同名の属性に代入する処理を削除し、代わりにscoretable_dict を同名の属性に代入する処理を記述する
 1  from tree import Node, Mbtree_GUI
 2  from marubatsu import Marubatsu
 3
 4  def __init__(self, scoretable_dict=None, show_score=True, size=0.15):
 5      if scoretable_dict is None:
 6          from util import load_bestmoves
 7
 8          scoretable_dict = {
 9              "Standard (ai_gt6)": load_bestmoves("../data/bestmoves_and_score_by_board.dat"),
10              "Shortest victory (ai_gtsv)": load_bestmoves("../data/bestmoves_and_score_by_board_shortest_victory.dat"),
11          }
12      self.scoretable_dict = scoretable_dict
元と同じなので省略
13    
14  Mbtree_GUI.__init__ = __init__
行番号のないプログラム
from tree import Node, Mbtree_GUI
from marubatsu import Marubatsu

def __init__(self, scoretable_dict=None, show_score=True, size=0.15):
    if scoretable_dict is None:
        scoretable_dict = {
            "Standard (ai_gt6)": load_bestmoves("../data/bestmoves_and_score_by_board.dat"),
            "Shortest victory (ai_gtsv)": load_bestmoves("../data/bestmoves_and_score_by_board_shortest_victory.dat"),
        }
    self.scoretable_dict = scoretable_dict
    self.show_score = show_score
    self.size = size
    self.width = 50
    self.height = 65
    self.selectednode = Node(Marubatsu())
    super(Mbtree_GUI, self).__init__()
    
Mbtree_GUI.__init__ = __init__
修正箇所
from tree import Node, Mbtree_GUI
from marubatsu import Marubatsu

-def __init__(self, bestmoves_and_score_by_board, show_score=True, size=0.15):
+def __init__(self, scoretable_dict=None, show_score=True, size=0.15):
+   if scoretable_dict is None:
+       scoretable_dict = {
+           "Standard (ai_gt6)": load_bestmoves("../data/bestmoves_and_score_by_board.dat"),
+           "Shortest victory(ai_gtsv)": load_bestmoves("../data/bestmoves_and_score_by_board_shortest_victory.dat"),
+       }
-   self.bestmoves_and_score_by_board = bestmoves_and_score_by_board
+   self.scoretable_dict = scoretable_dict
    self.scoretable_dict = scoretable_dict
元と同じなので省略
    
Mbtree_GUI.__init__ = __init__

Mbtree_GUI クラスの create_widgets メソッドの修正

次に、scoretable_dict の値を元に、Dropdown のウィジェットを作成 する必要があります。下記は、そのように create_widgets メソッドを修正したプログラムです。

  • 5 ~ 8 行目scoretable_dict 属性の値を使って Dropdown を作成する。なお、Dropdown の説明文(description)は score table とした
  • 9 行目:先程 __init__ メソッド内で bestmoves_and_score_by_board 属性に値を代入する処理を削除したが、局面と最善手・評価値の対応表を表す この属性は update_gui で部分木を描画する際に必要 となる。Dropdown の項目の値には局面と最善手・評価値の対応表のデータが代入されているので、Dropdown で 選択されている項目の値(value 属性)を その属性に代入する
 1  import ipywidgets as widgets
 2  import matplotlib.pyplot as plt
 3
 4  def create_widgets(self):
元と同じなので省略
 5      self.dropdown = widgets.Dropdown(
 6          options=self.scoretable_dict,
 7          description="score table",
 8      )
 9      self.bestmoves_and_score_by_board = self.dropdown.value
10
11  Mbtree_GUI.create_widgets = create_widgets
行番号のないプログラム
import ipywidgets as widgets
import matplotlib.pyplot as plt

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.score_button = self.create_button("評価値の表示", 100)
    self.size_slider = widgets.FloatSlider(min=0.05, max=0.25, step=0.01, description="size", value=self.size)
    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    
    
    self.dropdown = widgets.Dropdown(
        options=self.scoretable_dict,
        description="score table",
    )
    self.bestmoves_and_score_by_board = self.dropdown.value

Mbtree_GUI.create_widgets = create_widgets
修正箇所
import ipywidgets as widgets
import matplotlib.pyplot as plt

def create_widgets(self):
元と同じなので省略
+   self.dropdown = widgets.Dropdown(
+       options=self.scoretable_dict,
+       description="score table",
+   )
+   self.bestmoves_and_score_by_board = self.dropdown.value

Mbtree_GUI.create_widgets = create_widgets

Mbtree_GUI クラスの display_widgets メソッドの修正

次に、作成した Dropdown を表示 するようにする必要があります。本記事では、下記のプログラムの 5 行目のように、部分木の大きさを変更する IntSlider の下に Dropdown を配置して表示 することにしました。

1  def display_widgets(self):
2      hbox1 = widgets.HBox([self.label, self.up_button, self.label, self.label, self.score_button])
3      hbox2 = widgets.HBox([self.left_button, self.label, self.right_button,
4                          self.size_slider, self.help_button])
5      hbox3 = widgets.HBox([self.label, self.down_button, self.label, self.dropdown])
6      self.vbox = widgets.VBox([self.output, hbox1, hbox2, hbox3, self.fig.canvas])
7      display(self.vbox)  
8    
9  Mbtree_GUI.display_widgets = display_widgets
行番号のないプログラム
def display_widgets(self):
    hbox1 = widgets.HBox([self.label, self.up_button, self.label, self.label, self.score_button])
    hbox2 = widgets.HBox([self.left_button, self.label, self.right_button,
                        self.size_slider, self.help_button])
    hbox3 = widgets.HBox([self.label, self.down_button, self.label, self.dropdown])
    self.vbox = widgets.VBox([self.output, hbox1, hbox2, hbox3, self.fig.canvas])
    display(self.vbox)  
    
Mbtree_GUI.display_widgets = display_widgets
修正箇所
def display_widgets(self):
    hbox1 = widgets.HBox([self.label, self.up_button, self.label, self.label, self.score_button])
    hbox2 = widgets.HBox([self.left_button, self.label, self.right_button,
                        self.size_slider, self.help_button])
-   hbox3 = widgets.HBox([self.label, self.down_button, self.label])
+   hbox3 = widgets.HBox([self.label, self.down_button, self.label, self.dropdown])
    self.vbox = widgets.VBox([self.output, hbox1, hbox2, hbox3, self.fig.canvas])
    display(self.vbox)  
    
Mbtree_GUI.display_widgets = display_widgets

上記の修正後に下記のプログラムを実行すると、実行結果のように 2 つの項目が登録された Dropdown が表示されるようになります。なお、実引数 scoretable_dict を省略できる ように __init__ メソッドを定義したので、以後は省略することにします。

Mbtree_GUI(scoretable_dict)

実行結果

Mbtree_GUI クラスの create_event_handler メソッドの修正

次に、Dropdown の項目を変更した際 に、部分木を表示する際に利用する 局面と最善手・評価値の対応表が変わる ようにする必要があります。そのためには、下記のプログラムのように、Dropdown の項目が変更された場合の イベントハンドラcreate_event_handler の中に 記述する必要 があります。

  • 4 ~ 6 行目:Dropdown を変更した際に実行するイベントハンドラを定義する
  • 5 行目bestmoves_and_score_by_board 属性の値を、Dropdown で選択中の項目の値で更新する(この部分は、changed["new"] を代入しても良い)
  • 6 行目update_gui メソッドを呼び出して、部分木の表示を更新する
  • 8 行目:Dropdown とイベントハンドラを結び付ける
 1  def create_event_handler(self):
元と同じなので省略
 2      self.help_button.on_click(on_help_button_clicked)
 3
 4      def on_dropdown_changed(changed):
 5          self.bestmoves_and_score_by_board = self.dropdown.value
 6          self.update_gui()
 7
 8      self.dropdown.observe(on_dropdown_changed, names="value")
 9
10      def on_key_press(event):
元と同じなので省略
11
12  Mbtree_GUI.create_event_handler = create_event_handler
行番号のないプログラム
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_score_button_clicked(b=None):
        self.show_score = not self.show_score
        self.update_gui()
                
    def on_size_slider_changed(changed):
        self.size = changed["new"]
        self.fig.set_figwidth(self.width * self.size)
        self.fig.set_figheight(self.height * self.size)
        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.update_gui()
                        
    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.score_button.on_click(on_score_button_clicked)
    self.size_slider.observe(on_size_slider_changed, names="value")
    self.help_button.on_click(on_help_button_clicked)

    def on_dropdown_changed(changed):
        self.bestmoves_and_score_by_board = self.dropdown.value
        self.update_gui()

    self.dropdown.observe(on_dropdown_changed, names="value")

    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
修正箇所
def create_event_handler(self):
元と同じなので省略
    self.help_button.on_click(on_help_button_clicked)

+   def on_dropdown_changed(changed):
+       self.bestmoves_and_score_by_board = self.dropdown.value
+       self.update_gui()

+   self.dropdown.observe(on_dropdown_changed, names="value")

    def on_key_press(event):
元と同じなので省略

Mbtree_GUI.create_event_handler = create_event_handler

上記の修正後に下記のプログラムを実行し、(0, 0) に着手を行った局面を選択した後で Dropdown で項目を変更すると、実行結果の左図のように部分木の評価値の表示が変更されます。Dropdown の項目を変更する前の右図と見比べて下さい。

Mbtree_GUI()

実行結果

 

これで、Mbtree_GUI クラスのインスタンスを作成した後で、局面と最善手・評価値の対応表をいつでも変更できるようになりました。

Marubatsu クラスの修正

Marubatsu_GUI クラスを修正した結果、下記のプログラムで gui_play で対戦を行おうとすると実行結果のような エラーが発生 するようになります。

from util import gui_play

gui_play()

実行結果

略
File c:\Users\ys\ai\marubatsu\123\marubatsu.py:604, in Marubatsu_GUI.__init__(self, mb, params, names, ai_dict, seed, size)
    602 if Marubatsu_GUI.mbtree is None:
    603     Marubatsu_GUI.mbtree = Mbtree.load("../data/aidata")
--> 604 self.mbtree_gui = Mbtree_GUI(Marubatsu_GUI.mbtree, size=0.1)
略
File c:\Users\ys\Anaconda3\envs\marubatsu\Lib\site-packages\ipywidgets\widgets\widget_selection.py:121, in _make_options(x)
    118     x = x.items()
    120 # only iterate once through the options.
--> 121 xlist = tuple(x)
    123 # Check if x is an iterable of (label, value) pairs
    124 if all((isinstance(i, (list, tuple)) and len(i) == 2) for i in xlist):

TypeError: 'Mbtree' object is not iterable

最後のエラーメッセージは、widget_selection.py という、ipywidgets モジュールの中 で記述されている プログラムで発生 しています。このプログラムは自分で記述したものではなく、エラーメッセージを見ても エラーの原因がよくわかりません。そこで、エラーメッセージをさかのぼっていくと、Marubatsu_GUI クラスの __init__ メソッド内の self.mbtree_gui = Mbtree_GUI(Marubatsu_GUI.mbtree, size=0.1) というプログラムを実行した結果エラーが発生していることが確認できます。

先程 Marubatsu_GUI クラスの __init__ メソッドの 最初の仮引数 を、局面と最善手・評価値の 対応表 を代入する bestmoves_and_score_by_board から、Dropdown を登録する際に必要となる局面と最善手・評価値の 対応表の一覧 を代入する scoretable_dict に変更 しましたが、上記の Marubatsu_GUI クラスの __init__ メソッドの処理では その変更が反映されていない ことがこのエラーが発生する原因です。

Marubatsu_GUI クラスの __init__ メソッドの修正

まず、Marubatsu_GUI クラスの __init__ メソッドを下記のプログラムのように修正する必要があります。

  • 5 行目:仮引数 scoretable_dict を追加する。なお、Marubatsu_GUI クラスのインスタンスは Marubatsu クラスの play メソッド内でのみ作成され、その際に必ず scoretable_dict に対応する実引数を記述して呼び出すことにするので、デフォルト引数にする必要はない
  • 8 行目Mbtree はインポートする必要がなくなったのでインポートしないようにした
  • 10 行目に記述されていた、ゲーム木のデータをファイルから読み込んでクラス属性 mbtree に代入する処理は削除した。その理由はこの後で説明する
  • 10 行目:最初の実引数を scoretable_dict に修正する
 1  from marubatsu import Marubatsu_GUI
 2  from tkinter import Tk
 3  import os
 4
 5  def __init__(self, mb, params, names, ai_dict, scoretable_dict, seed, size):
元と同じなので省略
 6      super(Marubatsu_GUI, self).__init__()
 7    
 8      from tree import Mbtree_GUI   
 9
10      self.mbtree_gui = Mbtree_GUI(scoretable_dict, size=0.1)
11    
12  Marubatsu_GUI.__init__ = __init__               
行番号のないプログラム
from marubatsu import Marubatsu_GUI
from tkinter import Tk
import os

def __init__(self, mb, params, names, ai_dict, scoretable_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_GUI
        
    self.mbtree_gui = Mbtree_GUI(scoretable_dict, size=0.1)
    
Marubatsu_GUI.__init__ = __init__
修正箇所
from marubatsu import Marubatsu_GUI
from tkinter import Tk
import os

-def __init__(self, mb, params, names, ai_dict, seed, size):
+def __init__(self, mb, params, names, ai_dict, scoretable_dict, seed, size):
元と同じなので省略
    super(Marubatsu_GUI, self).__init__()
    
-   from tree import Mbtree, Mbtree_GUI
+   from tree import 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)
+   self.mbtree_gui = Mbtree_GUI(scoretable_dict, size=0.1)
    
Marubatsu_GUI.__init__ = __init__               

修正前のプログラムでは、__init__ メソッド内で 〇×ゲームのゲーム木のデータをファイルから読み込む必要 があり、その 所要時間が長い ので、同じデータを何度もファイルから読み込まなくても済むように クラス属性 mbtree に読み込んだデータを代入するという工夫を行っていました。一方、局面と最善手・評価値の対応表の データは小さく、ファイルからそのデータを ほぼ一瞬で読み込むことができる のでそのような工夫を行う必要はありません。そのため、8 行目に記述されていたその処理を削除しました。

また、クラス属性 mbtree はもう必要が無くなったので、marubatsu.py の Marubatsu_GUI クラスの定義の中の下記の 2 行目のプログラムは削除します。

class Marubatsu_GUI:
    mbtree = None

Marubatsu_GUI クラスの update_gui メソッドの修正

Marubatsu_GUI クラスの mbtree 属性が無くなった ので、Marubatsu_GUI クラスの中で、mbtree 属性を利用するプログラムを修正する必要 があります。

mbtree 属性は、ゲーム盤に着手を行った際に ゲーム盤の表示を更新するために 呼び出される update_gui メソッドの下記の部分で利用されています

def update_gui(self):

    if hasattr(self, "mbtree_gui"):
        key = tuple(self.mb.records[:self.mb.move_count + 1])
        self.mbtree_gui.selectednode = self.mbtree.nodelist_by_mb[key]
        self.mbtree_gui.update_gui()

上記のプログラムでは、下記の手順でゲーム盤の下に表示する GUI の部分木の 選択されたノードを現在の局面を表すノードに変更 し、表示を更新するという処理を行います。

  • ゲーム木を表す self.mbtree の中から、現在の局面を表すノードを探す
  • そのノードを Mbtree_GUI クラスのインスタンスの selectednode 属性に代入 する
  • GUI の部分木の表示を更新する

mbtree 属性がなくなったので、この処理を self.mbtree 利用せずに行う 必要があります。前回の記事で、selectednode 属性に代入する ノード のデータには、下記の属性のデータのみが必要 であることがわかっています。

  • ノードの深さを表す depth 属性
  • 親ノードを表す parent 属性

上記のうち、ノードの深さself.mb.move_count によって計算できますが、局面を表す self.mb のデータから、親ノードを計算するのは少々面倒 です。

親ノードのデータ は、下記のプログラムの Mbtree_GUI クラスの update_gui メソッドの中で、深さが 6 より大きい場合 に中心となるノードを計算するために 必要 なので、深さが 6 以下 のノードの場合は 必要がありません

def update_gui(self):

     centernode = self.selectednode
     while centernode.depth > 6:
         centernode = centernode.parent

そこで、下記のプログラムのように、親ノードを計算する処理は一旦保留 し、親ノードを持たない self.mbノードを作成 して selectednode 属性に代入することにします。

  • 5 行目:7 行目の処理で Node クラスが必要になるが、marubatsu.py の先頭で Node クラスを mbtree.py からインポートすると循環参照が発生するので、ローカルなインポートで Node クラスをインポートする必要がある
  • 7 行目:現在の局面を表すノードを作成し、selectednode 属性に代入する。ただし、現時点では親ノードは存在しないものとしてノードを作成する
  • 8 行目:部分木の表示を更新する
 1  def update_gui(self):
元と同じなので省略
 2      self.update_widgets_status()
 3      
 4      if hasattr(self, "mbtree_gui"):
 5          from tree import Node
 6
 7          self.mbtree_gui.selectednode = Node(self.mb, depth=self.mb.move_count)
 8          self.mbtree_gui.update_gui()
 9        
10  Marubatsu_GUI.update_gui = update_gui
行番号のないプログラム
def update_gui(self):
    ax = self.ax
    ai = self.mb.ai
    
    # Axes の内容をクリアして、これまでの描画内容を削除する
    ax.clear()
    
    # y 軸を反転させる
    ax.invert_yaxis()
    
    # 枠と目盛りを表示しないようにする
    ax.axis("off")   
    
    # リプレイ中、ゲームの決着がついていた場合は背景色を変更する
    is_replay =  self.mb.move_count < len(self.mb.records) - 1 
    if self.mb.status == Marubatsu.PLAYING:
        facecolor = "lightcyan" if is_replay else "white"
    else:
        facecolor = "lightyellow"

    ax.figure.set_facecolor(facecolor)
        
    # 上部のメッセージを描画する
    # 対戦カードの文字列を計算する
    ax.text(1.5, 3.5, f"{self.dropdown_list[0].label} VS {self.dropdown_list[1].label}", fontsize=20, ha="center")   
    
    # ゲームの決着がついていない場合は、手番を表示する
    if self.mb.status == Marubatsu.PLAYING:
        text = "Turn " + self.mb.turn
    # 引き分けの場合
    elif self.mb.status == Marubatsu.DRAW:
        text = "Draw game"
    # 決着がついていれば勝者を表示する
    else:
        text = "Winner " + self.mb.status
    # リプレイ中の場合は "Replay" を表示する
    if is_replay:
        text += " Replay"
    ax.text(0, -0.2, text, fontsize=20)
    
    self.draw_board(ax, self.mb)
    
    self.update_widgets_status()
    
    if hasattr(self, "mbtree_gui"):
        from tree import Node

        self.mbtree_gui.selectednode = Node(self.mb, depth=self.mb.move_count)
        self.mbtree_gui.update_gui()
        
Marubatsu_GUI.update_gui = update_gui
修正箇所
def update_gui(self):
元と同じなので省略
    self.update_widgets_status()
    
    if hasattr(self, "mbtree_gui"):
+       from tree import Node

-       key = tuple(self.mb.records[:self.mb.move_count + 1])
-       self.mbtree_gui.selectednode = self.mbtree.nodelist_by_mb[key]
+       self.mbtree_gui.selectednode = Node(self.mb, depth=self.mb.move_count)
+       self.mbtree_gui.update_gui()
        
Marubatsu_GUI.update_gui = update_gui

Marubatsu クラスの play メソッドの修正

次に、Marubatsu_GUI クラスの インスタンスを作成 する処理を行う play メソッドを下記のプログラムのように修正する必要があります。

  • 3 行目:デフォルト値を None とする仮引数 scoretable_dict を追加する
  • 7 行目:Marubatsu_GUI クラスのインスタンスを作成する際に、キーワード引数 scoretable_dict=scoretable_dict を記述するように修正する
1  from random import random
2
3  def play(self, ai, ai_dict=None, params=None, names=None, scoretable_dict=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
4      # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
5      if gui:
6          mb_gui = Marubatsu_GUI(self, params=params, names=names, ai_dict=ai_dict,
7                                 scoretable_dict=scoretable_dict, seed=seed, size=size)
元と同じなので省略
8
9   Marubatsu.play = play
行番号のないプログラム
from random import random

def play(self, ai, ai_dict=None, params=None, names=None, scoretable_dict=None, verbose=True, seed=None, gui=False, size=3):
    # params が None の場合のデフォルト値を設定する
    if params is None:
        params = [{}, {}]
        
    # 一部の仮引数をインスタンスの属性に代入する
    self.ai = ai
    self.verbose = verbose
    self.gui = gui
    
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)

    # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
    if gui:
        mb_gui = Marubatsu_GUI(self, params=params, names=names, ai_dict=ai_dict,
                               scoretable_dict=scoretable_dict, seed=seed, size=size)
    else:
        mb_gui = None
        
    self.restart()
    return self.play_loop(mb_gui, params=params)

Marubatsu.play = play
修正箇所
from random import random

-def play(self, ai, ai_dict=None, params=None, names=None, verbose=True, seed=None, gui=False, size=3):
+def play(self, ai, ai_dict=None, params=None, names=None, scoretable_dict=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
    # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
    if gui:
        mb_gui = Marubatsu_GUI(self, params=params, names=names, ai_dict=ai_dict,
-                              seed=seed, size=size)  
+                              scoretable_dict=scoretable_dict, seed=seed, size=size)  
元と同じなので省略

Marubatsu.play = play

上記の修正後に下記のプログラムを実行して、ゲーム盤に着手を行ってください。6 手目まで はゲーム盤の下の GUI の部分木が正しく更新 されますが、7 手目の着手を行うと 実行結果のような エラーが発生 します。

gui_play の中ではキーワード引数 scoretable_dict を記述せずに Marubatsu クラスのインスタンスの play メソッドを呼び出しているので、仮引数 scoretable_dict にはデフォルト値である None が代入されます。そのため、その後の処理で Mbtree_GUI クラスのインスタンスを作成する際にはキーワード引数 scoretable_dict には None が代入されることになり、今回の記事で修正した Mbtree_GUI クラスの __init__ メソッドの最初の処理によって GUI の部分木には 2 つの項目を持つ Dropdown が作成されます。

gui_play()

実行結果

略
File c:\Users\ys\ai\marubatsu\123\tree.py:909, in Mbtree_GUI.update_gui(self)
    907     maxdepth = 9
    908 centernode = self.selectednode
--> 909 while centernode.depth > 6:
    910     centernode = centernode.parent
    911 self.mbtree = Mbtree(subtree={"centermb": centernode.mb, "selectedmb": self.selectednode.mb, "maxdepth": maxdepth, 
    912                     "bestmoves_and_score_by_board": self.bestmoves_and_score_by_board})

AttributeError: 'NoneType' object has no attribute 'depth'

エラーの検証と修正方法

エラーメッセージから、Mbtree_GUI クラスの update_gui メソッド内で、while centernode.depth > 6: を実行した際に、centernodeNone が代入されている ことがわかります。先ほど説明したように、selectednode 属性に代入する ノードを作成する際に親ノードを作成しなかった ので、parent 属性には None が代入 されます。そのため、深さが 7 以上のノードが centernode 属性に代入された場合は、while 文のブロックの centernode = centernode.parent が実行 されて centernodeNone が代入 され、次の while 文の条件式が計算されると上記のエラーが発生します。

この問題を解決する方法としては、以下の 2 種類の方法が考えられます。

  • selectednode 属性に代入するノードを計算する際に、その 親ノードを作成する
  • 上記の update_gui で、親ノードの情報を使わずに 中心となるノードを計算するように修正する

後者のほうがプログラムの記述が簡単 なので、本記事では後者の方法で修正することにしますが、前者の方法が良いと思った方は実際に実装してみて下さい。

Mbtree_GUI クラスの update_gui メソッドの修正

update_gui メソッドでは、親ノードを辿ることで深さが 6 のノードを計算していますが、別の方法として、下記のプログラムのように records 属性に代入された棋譜のデータを利用 して、ゲーム開始時の局面から 6 手目までの着手を行う ことで、中心となるノードの局面を計算することができます。

  • 2、3 行目:選択されたノードの深さが 6 以下の場合は、中心となるノードの局面を、選択されたノードの局面とする
  • 4 ~ 7 行目:深さが 7 以上の場合は、5 行目でゲーム開始時の局面を表すデータを作成して centermb に代入し、選択された局面の棋譜のデータを利用して 6 手目までの着手を行い、その局面を中心となるノードの局面とする
  • 8 行目:中心となる局面のデータを centernode.mb から centermb に修正する
 1  def update_gui(self):
元と同じなので省略
 2      if self.selectednode.depth <= 6:
 3          centermb = self.selectednode.mb
 4      else:
 5          centermb = Marubatsu()
 6          for x, y in self.selectednode.mb.records[1:7]:
 7              centermb.move(x, y)
 8      self.mbtree = Mbtree(subtree={"centermb": centermb, "selectedmb": self.selectednode.mb, "maxdepth": maxdepth, 
 9                           "bestmoves_and_score_by_board": self.bestmoves_and_score_by_board})
元と同じなので省略
10
11  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(-1, self.height - 1)   
    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
    if self.selectednode.depth <= 6:
        centermb = self.selectednode.mb
    else:
        centermb = Marubatsu()
        for x, y in self.selectednode.mb.records[1:7]:
            centermb.move(x, y)
    self.mbtree = Mbtree(subtree={"centermb": centermb, "selectedmb": self.selectednode.mb, "maxdepth": maxdepth, 
                          "bestmoves_and_score_by_board": self.bestmoves_and_score_by_board})
    self.selectednode = self.mbtree.selectednode
    self.mbtree.draw_subtree(centernode=self.mbtree.centernode, selectednode=self.selectednode,
                            show_bestmove=True, show_score=self.show_score,
                            ax=self.ax, maxdepth=maxdepth, size=self.size)
    
    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)
    self.set_button_color(self.score_button, value=self.show_score)
    
Mbtree_GUI.update_gui = update_gui
修正箇所
def update_gui(self):
元と同じなので省略
-   centernode = self.selectednode
-   while centernode.depth > 6:
-       centernode = centernode.parent
+   if self.selectednode.depth <= 6:
+       centermb = self.selectednode.mb
+   else:
+       centermb = Marubatsu()
+       for x, y in self.selectednode.mb.records[1:7]:
+           centermb.move(x, y)
-   self.mbtree = Mbtree(subtree={"centermb": centernode.mb, "selectedmb": self.selectednode.mb, "maxdepth": maxdepth, 
+   self.mbtree = Mbtree(subtree={"centermb": centermb, "selectedmb": self.selectednode.mb, "maxdepth": maxdepth, 
                         "bestmoves_and_score_by_board": self.bestmoves_and_score_by_board})
元と同じなので省略

Mbtree_GUI.update_gui = update_gui

上記の修正後に下記のプログラムを実行して 9 手目までの着手を行うと、実行結果のようにエラーが発生しなくなったことが確認できます。

gui_play()

実行結果

上記の実行結果に対して 様々な操作を行うと、上記のプログラムには リプレイ機能を用いるとエラーが発生する という問題があることがわかります。その問題については次回の記事で修正することするので、余裕がある方はそのエラーの原因と修正方法について考えておいてください。

今回の記事のまとめ

今回の記事では、Mbtree_GUI クラスのインスタンスを作成した後で、局面と最善手・評価値の対応表Dropdown で変更できるよう修正 しました。また、それにあわせて Marubatsu クラスの play メソッドが動作する ように修正しました。

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

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

次回の記事

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?